diff --git a/src/cli/build.helpers.ts b/src/cli/build.helpers.ts index 217913943..fa7a5126a 100644 --- a/src/cli/build.helpers.ts +++ b/src/cli/build.helpers.ts @@ -1,3 +1,15 @@ +import { createDirectoryIfNotExists } from "../lib/filesystemService"; +import { sanitizeResourceName } from "../sdk/sdk"; +import { createDockerfile } from "../lib/included-files"; +import { execSync } from "child_process"; +import { CapabilityExport } from "../lib/types"; +import { validateCapabilityNames } from "../lib/helpers"; +import { BuildOptions, BuildResult, context, BuildContext } from "esbuild"; +import { Assets } from "../lib/assets"; +import { resolve } from "path"; +import { promises as fs } from "fs"; + +export type Reloader = (opts: BuildResult) => void | Promise; /** * Determine the RBAC mode based on the CLI options and the module's config * @param opts CLI options @@ -26,3 +38,171 @@ export function determineRbacMode( // if nothing is defined return admin, else return scoped return cfg.pepr.rbacMode || "admin"; } + +/** + * Handle the custom output directory + * @param outputDir the desired output directory + * @returns The desired output directory or the default one + */ + +export async function handleCustomOutputDir(outputDir: string): Promise { + const defaultOutputDir = "dist"; + if (outputDir) { + try { + await createDirectoryIfNotExists(outputDir); + return outputDir; + } catch (error) { + console.error(`Error creating output directory: ${error.message}`); + process.exit(1); + } + } + return defaultOutputDir; +} + +/** + * Check if the image is from Iron Bank and return the correct image + * @param registry The registry of the image + * @param image The image to check + * @param peprVersion The version of the PEPR controller + * @returns The image string + * @example + */ +export function checkIronBankImage(registry: string, image: string, peprVersion: string): string { + return registry === "Iron Bank" + ? `registry1.dso.mil/ironbank/opensource/defenseunicorns/pepr/controller:v${peprVersion}` + : image; +} + +/** + * Check if the image pull secret is a valid Kubernetes name + * @param imagePullSecret + * @returns boolean + */ +export function validImagePullSecret(imagePullSecretName: string): void { + if (imagePullSecretName) { + const error = "Invalid imagePullSecret. Please provide a valid name as defined in RFC 1123."; + if (sanitizeResourceName(imagePullSecretName) !== imagePullSecretName) { + // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names + console.error(error); + process.exit(1); + } + } +} + +/** + * Constraint to majke sure customImage and registry are not both used + * @param customImage + * @param registry + * @returns + */ +export function handleCustomImage(customImage: string, registry: string): string { + let defaultImage = ""; + if (customImage) { + if (registry) { + console.error(`Custom Image and registry cannot be used together.`); + process.exit(1); + } + defaultImage = customImage; + } + return defaultImage; +} + +/** + * Creates and pushes a custom image for WASM or any other included files + * @param includedFiles + * @param peprVersion + * @param description + * @param image + */ +export async function handleCustomImageBuild( + includedFiles: string[], + peprVersion: string, + description: string, + image: string, +): Promise { + if (includedFiles.length > 0) { + await createDockerfile(peprVersion, description, includedFiles); + execSync(`docker build --tag ${image} -f Dockerfile.controller .`, { + stdio: "inherit", + }); + execSync(`docker push ${image}`, { stdio: "inherit" }); + } +} + +/** + * Disables embedding of deployment files into output module + * @param embed + * @param path + * @returns + */ +export function handleEmbedding(embed: boolean, path: string): void { + if (!embed) { + console.info(`✅ Module built successfully at ${path}`); + return; + } +} + +/** + * Check if the capability names are valid + * @param capabilities The capabilities to check + */ +export function handleValidCapabilityNames(capabilities: CapabilityExport[]): void { + try { + // wait for capabilities to be loaded and test names + validateCapabilityNames(capabilities); + } catch (e) { + console.error(`Error loading capability:`, e); + process.exit(1); + } +} + +/** + * Watch for changes in the module + * @param ctxCfg The build options + * @param reloader The reloader function + * @returns The build context + */ +export async function watchForChanges( + ctxCfg: BuildOptions, + reloader: Reloader | undefined, +): Promise> { + const ctx = await context(ctxCfg); + + // If the reloader function is defined, watch the module for changes + if (reloader) { + await ctx.watch(); + } else { + // Otherwise, just build the module once + await ctx.rebuild(); + await ctx.dispose(); + } + + return ctx; +} + +export async function generateYamlAndWriteToDisk(obj: { + uuid: string; + imagePullSecret: string; + outputDir: string; + assets: Assets; + zarf: string; +}): Promise { + const { uuid, imagePullSecret, outputDir, assets, zarf } = obj; + const yamlFile = `pepr-module-${uuid}.yaml`; + const chartPath = `${uuid}-chart`; + const yamlPath = resolve(outputDir, yamlFile); + const yaml = await assets.allYaml(imagePullSecret); + const zarfPath = resolve(outputDir, "zarf.yaml"); + + let localZarf = ""; + if (zarf === "chart") { + localZarf = assets.zarfYamlChart(chartPath); + } else { + localZarf = assets.zarfYaml(yamlFile); + } + await fs.writeFile(yamlPath, yaml); + await fs.writeFile(zarfPath, localZarf); + + await assets.generateHelmChart(outputDir); + console.info(`✅ K8s resource for the module saved to ${yamlPath}`); +} diff --git a/src/cli/build.test.ts b/src/cli/build.test.ts index 1c310b55a..d0afc3de0 100644 --- a/src/cli/build.test.ts +++ b/src/cli/build.test.ts @@ -1,36 +1,292 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023-Present The Pepr Authors -import { determineRbacMode } from "./build.helpers"; +import { + determineRbacMode, + handleCustomOutputDir, + handleEmbedding, + handleValidCapabilityNames, + handleCustomImageBuild, + checkIronBankImage, + validImagePullSecret, + handleCustomImage, +} from "./build.helpers"; +import { createDirectoryIfNotExists } from "../lib/filesystemService"; +import { expect, describe, it, jest, beforeEach } from "@jest/globals"; +import { createDockerfile } from "../lib/included-files"; +import { execSync } from "child_process"; +import { CapabilityExport } from "../lib/types"; +import { Capability } from "../lib/capability"; -import { expect, describe, test } from "@jest/globals"; +jest.mock("child_process", () => ({ + execSync: jest.fn(), +})); + +jest.mock("../lib/included-files", () => ({ + createDockerfile: jest.fn(), +})); + +jest.mock("../lib/filesystemService", () => ({ + createDirectoryIfNotExists: jest.fn(), +})); describe("determineRbacMode", () => { - test("should allow CLI options to overwrite module config", () => { + it("should allow CLI options to overwrite module config", () => { const opts = { rbacMode: "admin" }; const cfg = { pepr: { rbacMode: "scoped" } }; const result = determineRbacMode(opts, cfg); expect(result).toBe("admin"); }); - test('should return "admin" when cfg.pepr.rbacMode is provided and not "scoped"', () => { + it('should return "admin" when cfg.pepr.rbacMode is provided and not "scoped"', () => { const opts = {}; const cfg = { pepr: { rbacMode: "admin" } }; const result = determineRbacMode(opts, cfg); expect(result).toBe("admin"); }); - test('should return "scoped" when cfg.pepr.rbacMode is "scoped"', () => { + it('should return "scoped" when cfg.pepr.rbacMode is "scoped"', () => { const opts = {}; const cfg = { pepr: { rbacMode: "scoped" } }; const result = determineRbacMode(opts, cfg); expect(result).toBe("scoped"); }); - test("should default to admin when neither option is provided", () => { + it("should default to admin when neither option is provided", () => { const opts = {}; const cfg = { pepr: {} }; const result = determineRbacMode(opts, cfg); expect(result).toBe("admin"); }); }); + +describe("handleCustomOutputDir", () => { + const mockedCreateDirectoryIfNotExists = jest.mocked(createDirectoryIfNotExists); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should return the provided output directory if it exists and is created successfully", async () => { + mockedCreateDirectoryIfNotExists.mockResolvedValueOnce(); + + const outputDir = "custom-output-dir"; + const result = await handleCustomOutputDir(outputDir); + + expect(mockedCreateDirectoryIfNotExists).toHaveBeenCalledWith(outputDir); + expect(result).toBe(outputDir); + }); + + it("should return the default output directory if no custom directory is provided", async () => { + const outputDir = ""; + const result = await handleCustomOutputDir(outputDir); + expect(result).toBe("dist"); + }); +}); + +describe("checkIronBankImage", () => { + it("should return the Iron Bank image if the registry is Iron Bank", () => { + const registry = "Iron Bank"; + const image = "ghcr.io/defenseunicorns/pepr/controller:v0.0.1"; + const peprVersion = "0.0.1"; + const result = checkIronBankImage(registry, image, peprVersion); + expect(result).toBe( + `registry1.dso.mil/ironbank/opensource/defenseunicorns/pepr/controller:v${peprVersion}`, + ); + }); + + it("should return the image if the registry is not Iron Bank", () => { + const registry = "GitHub"; + const image = "ghcr.io/defenseunicorns/pepr/controller:v0.0.1"; + const peprVersion = "0.0.1"; + const result = checkIronBankImage(registry, image, peprVersion); + expect(result).toBe(image); + }); +}); + +describe("validImagePullSecret", () => { + const mockExit = jest.spyOn(process, "exit").mockImplementation(() => { + return undefined as never; + }); + + const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + + beforeEach(() => { + jest.clearAllMocks(); + }); + it("should not throw an error if the imagePullSecret is valid", () => { + const imagePullSecret = "valid-secret"; + validImagePullSecret(imagePullSecret); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + expect(mockExit).not.toHaveBeenCalled(); + }); + it("should not throw an error if the imagePullSecret is empty", () => { + const imagePullSecret = ""; + validImagePullSecret(imagePullSecret); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + expect(mockExit).not.toHaveBeenCalled(); + }); + it("should throw an error if the imagePullSecret is invalid", () => { + const imagePullSecret = "invalid name"; + validImagePullSecret(imagePullSecret); + expect(consoleErrorSpy).toHaveBeenCalled(); + expect(mockExit).toHaveBeenCalled(); + }); +}); +describe("handleCustomImage", () => { + const mockExit = jest.spyOn(process, "exit").mockImplementation(() => { + return undefined as never; + }); + + const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should return the customImage if no registry is provided", () => { + const customImage = "custom-image"; + const registry = ""; + + const result = handleCustomImage(customImage, registry); + + expect(result).toBe(customImage); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + expect(mockExit).not.toHaveBeenCalled(); + }); + + it("should return an empty string if neither customImage nor registry is provided", () => { + const customImage = ""; + const registry = ""; + + const result = handleCustomImage(customImage, registry); + + expect(result).toBe(""); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + expect(mockExit).not.toHaveBeenCalled(); + }); + + it("should call process.exit with 1 and log an error if both customImage and registry are provided", () => { + const customImage = "custom-image"; + const registry = "registry"; + + handleCustomImage(customImage, registry); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Custom Image and registry cannot be used together.", + ); + expect(mockExit).toHaveBeenCalledWith(1); + }); +}); + +describe("handleCustomImageBuild", () => { + const mockedExecSync = jest.mocked(execSync); + const mockedCreateDockerfile = jest.mocked(createDockerfile); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should call createDockerfile and execute docker commands if includedFiles is not empty", async () => { + const includedFiles = ["file1", "file2"]; + const peprVersion = "1.0.0"; + const description = "Test Description"; + const image = "test-image"; + + await handleCustomImageBuild(includedFiles, peprVersion, description, image); + + expect(mockedCreateDockerfile).toHaveBeenCalledWith(peprVersion, description, includedFiles); + expect(mockedExecSync).toHaveBeenCalledWith( + `docker build --tag ${image} -f Dockerfile.controller .`, + { + stdio: "inherit", + }, + ); + expect(mockedExecSync).toHaveBeenCalledWith(`docker push ${image}`, { stdio: "inherit" }); + }); + + it("should not call createDockerfile or execute docker commands if includedFiles is empty", async () => { + const includedFiles: string[] = []; + const peprVersion = "1.0.0"; + const description = "Test Description"; + const image = "test-image"; + + await handleCustomImageBuild(includedFiles, peprVersion, description, image); + + expect(mockedCreateDockerfile).not.toHaveBeenCalled(); + expect(mockedExecSync).not.toHaveBeenCalled(); + }); +}); +describe("handleEmbedding", () => { + const consoleInfoSpy = jest.spyOn(console, "info").mockImplementation(() => {}); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should log success message if embed is false", () => { + const embed = false; + const path = "test/path"; + + handleEmbedding(embed, path); + + expect(consoleInfoSpy).toHaveBeenCalledWith(`✅ Module built successfully at ${path}`); + }); + + it("should not log success message if embed is true", () => { + const embed = true; + const path = "test/path"; + + handleEmbedding(embed, path); + + expect(consoleInfoSpy).not.toHaveBeenCalled(); + }); +}); + +describe("handleValidCapabilityNames", () => { + const mockExit = jest.spyOn(process, "exit").mockImplementation(() => { + return undefined as never; + }); + + const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + + it("should call validateCapabilityNames with capabilities", () => { + const capability = new Capability({ + name: "test", + description: "test", + }); + + const capabilityExports: CapabilityExport[] = [ + { + name: capability.name, + description: capability.description, + namespaces: capability.namespaces, + bindings: capability.bindings, + hasSchedule: capability.hasSchedule, + }, + ]; + + handleValidCapabilityNames(capabilityExports); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + expect(mockExit).not.toHaveBeenCalled(); + }); + it("should call validateCapabilityNames with capabilities", () => { + const capability = new Capability({ + name: "test $me", + description: "test", + }); + + const capabilityExports: CapabilityExport[] = [ + { + name: capability.name, + description: capability.description, + namespaces: capability.namespaces, + bindings: capability.bindings, + hasSchedule: capability.hasSchedule, + }, + ]; + + handleValidCapabilityNames(capabilityExports); + expect(consoleErrorSpy).toHaveBeenCalled(); + expect(mockExit).toHaveBeenCalled(); + }); +}); diff --git a/src/cli/build.ts b/src/cli/build.ts index 5ec58740e..c1eabd90f 100644 --- a/src/cli/build.ts +++ b/src/cli/build.ts @@ -1,25 +1,34 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023-Present The Pepr Authors -import { execSync, execFileSync } from "child_process"; -import { BuildOptions, BuildResult, analyzeMetafile, context } from "esbuild"; +import { execFileSync } from "child_process"; +import { BuildOptions, BuildResult, analyzeMetafile } from "esbuild"; import { promises as fs } from "fs"; import { basename, dirname, extname, resolve } from "path"; -import { createDockerfile } from "../lib/included-files"; import { Assets } from "../lib/assets"; import { dependencies, version } from "./init/templates"; import { RootCmd } from "./root"; -import { peprFormat } from "./format"; import { Option } from "commander"; -import { validateCapabilityNames, parseTimeout } from "../lib/helpers"; -import { sanitizeResourceName } from "../sdk/sdk"; -import { determineRbacMode } from "./build.helpers"; -import { createDirectoryIfNotExists } from "../lib/filesystemService"; +import { parseTimeout } from "../lib/helpers"; +import { peprFormat } from "./format"; +import { + watchForChanges, + determineRbacMode, + handleEmbedding, + handleCustomOutputDir, + handleValidCapabilityNames, + handleCustomImage, + handleCustomImageBuild, + checkIronBankImage, + validImagePullSecret, + generateYamlAndWriteToDisk, +} from "./build.helpers"; + const peprTS = "pepr.ts"; let outputDir: string = "dist"; export type Reloader = (opts: BuildResult) => void | Promise; -export default function (program: RootCmd) { +export default function (program: RootCmd): void { program .command("build") .description("Build a Pepr Module for deployment") @@ -73,13 +82,7 @@ export default function (program: RootCmd) { ) .action(async opts => { // assign custom output directory if provided - if (opts.outputDir) { - outputDir = opts.outputDir; - createDirectoryIfNotExists(outputDir).catch(error => { - console.error(`Error creating output directory: ${error.message}`); - process.exit(1); - }); - } + outputDir = await handleCustomOutputDir(opts.outputDir); // Build the module const buildModuleResult = await buildModule(undefined, opts.entryPoint, opts.embed); @@ -88,16 +91,7 @@ export default function (program: RootCmd) { // Files to include in controller image for WASM support const { includedFiles } = cfg.pepr; - let image: string = ""; - - // Build Kubernetes manifests with custom image - if (opts.customImage) { - if (opts.registry) { - console.error(`Custom Image and registry cannot be used together.`); - process.exit(1); - } - image = opts.customImage; - } + let image = handleCustomImage(opts.customImage, opts.registry); // Check if there is a custom timeout defined if (opts.timeout !== undefined) { @@ -111,25 +105,14 @@ export default function (program: RootCmd) { image = `${opts.registryInfo}/custom-pepr-controller:${cfg.pepr.peprVersion}`; // only actually build/push if there are files to include - if (includedFiles.length > 0) { - await createDockerfile(cfg.pepr.peprVersion, cfg.description, includedFiles); - execSync(`docker build --tag ${image} -f Dockerfile.controller .`, { - stdio: "inherit", - }); - execSync(`docker push ${image}`, { stdio: "inherit" }); - } + await handleCustomImageBuild(includedFiles, cfg.pepr.peprVersion, cfg.description, image); } // If building without embedding, exit after building - if (!opts.embed) { - console.info(`✅ Module built successfully at ${path}`); - return; - } + handleEmbedding(opts.embed, path); // set the image version if provided - if (opts.version) { - cfg.pepr.peprVersion = opts.version; - } + opts.version ? (cfg.pepr.peprVersion = opts.version) : null; // Generate a secret for the module const assets = new Assets( @@ -144,56 +127,22 @@ export default function (program: RootCmd) { ); // If registry is set to Iron Bank, use Iron Bank image - if (opts?.registry === "Iron Bank") { - console.info( - `\n\tThis command assumes the latest release. Pepr's Iron Bank image release cycle is dictated by renovate and is typically released a few days after the GitHub release.\n\tAs an alternative you may consider custom --custom-image to target a specific image and version.`, - ); - image = `registry1.dso.mil/ironbank/opensource/defenseunicorns/pepr/controller:v${cfg.pepr.peprVersion}`; - } + image = checkIronBankImage(opts.registry, image, cfg.pepr.peprVersion); // if image is a custom image, use that instead of the default - if (image !== "") { - assets.image = image; - } + image !== "" ? (assets.image = image) : null; // Ensure imagePullSecret is valid - if (opts.withPullSecret) { - if (sanitizeResourceName(opts.withPullSecret) !== opts.withPullSecret) { - // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names - console.error( - "Invalid imagePullSecret. Please provide a valid name as defined in RFC 1123.", - ); - process.exit(1); - } - } - - const yamlFile = `pepr-module-${uuid}.yaml`; - const chartPath = `${uuid}-chart`; - const yamlPath = resolve(outputDir, yamlFile); - const yaml = await assets.allYaml(opts.withPullSecret); - - try { - // wait for capabilities to be loaded and test names - validateCapabilityNames(assets.capabilities); - } catch (e) { - console.error(`Error loading capability:`, e); - process.exit(1); - } - - const zarfPath = resolve(outputDir, "zarf.yaml"); - - let zarf = ""; - if (opts.zarf === "chart") { - zarf = assets.zarfYamlChart(chartPath); - } else { - zarf = assets.zarfYaml(yamlFile); - } - await fs.writeFile(yamlPath, yaml); - await fs.writeFile(zarfPath, zarf); - - await assets.generateHelmChart(outputDir); - - console.info(`✅ K8s resource for the module saved to ${yamlPath}`); + validImagePullSecret(opts.withPullSecret); + + handleValidCapabilityNames(assets.capabilities); + await generateYamlAndWriteToDisk({ + uuid, + outputDir, + imagePullSecret: opts.withPullSecret, + zarf: opts.zarf, + assets, + }); } }); } @@ -252,15 +201,7 @@ export async function buildModule(reloader?: Reloader, entryPoint = peprTS, embe try { const { cfg, modulePath, path, uuid } = await loadModule(entryPoint); - const validFormat = await peprFormat(true); - - if (!validFormat) { - console.log( - "\x1b[33m%s\x1b[0m", - "Formatting errors were found. The build will continue, but you may want to run `npx pepr format` to address any issues.", - ); - } - + await checkFormat(); // Resolve node_modules folder (in support of npm workspaces!) const npmRoot = execFileSync("npm", ["root"]).toString().trim(); @@ -282,7 +223,7 @@ export async function buildModule(reloader?: Reloader, entryPoint = peprTS, embe plugins: [ { name: "reload-server", - setup(build) { + setup(build): void | Promise { build.onEnd(async r => { // Print the build size analysis if (r?.metafile) { @@ -322,53 +263,64 @@ export async function buildModule(reloader?: Reloader, entryPoint = peprTS, embe ctxCfg.treeShaking = false; } - const ctx = await context(ctxCfg); - - // If the reloader function is defined, watch the module for changes - if (reloader) { - await ctx.watch(); - } else { - // Otherwise, just build the module once - await ctx.rebuild(); - await ctx.dispose(); - } + const ctx = await watchForChanges(ctxCfg, reloader); return { ctx, path, cfg, uuid }; } catch (e) { - console.error(`Error building module:`, e); + handleModuleBuildError(e); + } +} - if (!e.stdout) process.exit(1); // Exit with a non-zero exit code on any other error +interface BuildModuleResult { + stdout?: Buffer; + stderr: Buffer; +} - const out = e.stdout.toString() as string; - const err = e.stderr.toString(); +function handleModuleBuildError(e: BuildModuleResult): void { + console.error(`Error building module:`, e); - console.log(out); - console.error(err); + if (!e.stdout) process.exit(1); // Exit with a non-zero exit code on any other error - // Check for version conflicts - if (out.includes("Types have separate declarations of a private property '_name'.")) { - // Try to find the conflicting package - const pgkErrMatch = /error TS2322: .*? 'import\("\/.*?\/node_modules\/(.*?)\/node_modules/g; - out.matchAll(pgkErrMatch); + const out = e.stdout.toString() as string; + const err = e.stderr.toString(); - // Look for package conflict errors - const conflicts = [...out.matchAll(pgkErrMatch)]; + console.log(out); + console.error(err); - // If the regex didn't match, leave a generic error - if (conflicts.length < 1) { - console.info( - `\n\tOne or more imported Pepr Capabilities seem to be using an incompatible version of Pepr.\n\tTry updating your Pepr Capabilities to their latest versions.`, - "Version Conflict", - ); - } + // Check for version conflicts + if (out.includes("Types have separate declarations of a private property '_name'.")) { + // Try to find the conflicting package + const pgkErrMatch = /error TS2322: .*? 'import\("\/.*?\/node_modules\/(.*?)\/node_modules/g; + out.matchAll(pgkErrMatch); - // Otherwise, loop through each conflicting package and print an error - conflicts.forEach(match => { - console.info( - `\n\tPackage '${match[1]}' seems to be incompatible with your current version of Pepr.\n\tTry updating to the latest version.`, - "Version Conflict", - ); - }); + // Look for package conflict errors + const conflicts = [...out.matchAll(pgkErrMatch)]; + + // If the regex didn't match, leave a generic error + if (conflicts.length < 1) { + console.info( + `\n\tOne or more imported Pepr Capabilities seem to be using an incompatible version of Pepr.\n\tTry updating your Pepr Capabilities to their latest versions.`, + "Version Conflict", + ); } + + // Otherwise, loop through each conflicting package and print an error + conflicts.forEach(match => { + console.info( + `\n\tPackage '${match[1]}' seems to be incompatible with your current version of Pepr.\n\tTry updating to the latest version.`, + "Version Conflict", + ); + }); + } +} + +export async function checkFormat() { + const validFormat = await peprFormat(true); + + if (!validFormat) { + console.log( + "\x1b[33m%s\x1b[0m", + "Formatting errors were found. The build will continue, but you may want to run `npx pepr format` to address any issues.", + ); } }