diff --git a/.changeset/two-spies-talk.md b/.changeset/two-spies-talk.md new file mode 100644 index 00000000..acf669fc --- /dev/null +++ b/.changeset/two-spies-talk.md @@ -0,0 +1,10 @@ +--- +"@codecov/bundler-plugin-core": patch +"@codecov/sveltekit-plugin": patch +"@codecov/webpack-plugin": patch +"@codecov/rollup-plugin": patch +"@codecov/nuxt-plugin": patch +"@codecov/vite-plugin": patch +--- + +When a user submits a invalid bundle name, we will hard fail and exit the bundle process now. diff --git a/integration-tests/fixtures/generate-bundle-stats/nuxt/nuxt-plugin.test.ts b/integration-tests/fixtures/generate-bundle-stats/nuxt/nuxt-plugin.test.ts index 0ff78067..4a1aff3c 100644 --- a/integration-tests/fixtures/generate-bundle-stats/nuxt/nuxt-plugin.test.ts +++ b/integration-tests/fixtures/generate-bundle-stats/nuxt/nuxt-plugin.test.ts @@ -147,5 +147,50 @@ describe("Generating nuxt stats", () => { { timeout: 25_000 }, ); }); + + describe("invalid bundle name is passed", () => { + beforeEach(async () => { + const config = new GenerateConfig({ + // nuxt uses vite under the hood + plugin: "nuxt", + configFileName: "nuxt", + format: "esm", + detectFormat: "esm", + version: `v3`, + detectVersion: "v3", + file_format: "ts", + enableSourceMaps: false, + overrideOutputPath: `${nuxtApp}/nuxt.config.ts`, + }); + + await config.createConfig(); + config.removeBundleName(`test-nuxt-v${version}`); + await config.writeConfig(); + }); + + afterEach(async () => { + await $`rm -rf ${nuxtApp}/nuxt.config.ts`; + await $`rm -rf ${nuxtApp}/distV${version}`; + }); + + it( + "warns users and exits process with a code 1", + async () => { + const id = `nuxt-v${version}-${Date.now()}`; + const API_URL = `http://localhost:8000/test-url/${id}/200/false`; + + // prepare and build the app + const { exitCode } = + await $`cd test-apps/nuxt && API_URL=${API_URL} pnpm run build`.nothrow(); + + expect(exitCode).toBe(1); + // for some reason this isn't being outputted in the test env + // expect(stdout.toString()).toContain( + // "[codecov] bundleName: `` does not match format: `/^[wd_:/@.{}[]$-]+$/`.", + // ); + }, + { timeout: 25_000 }, + ); + }); }); }); diff --git a/integration-tests/fixtures/generate-bundle-stats/rollup/rollup-plugin.test.ts b/integration-tests/fixtures/generate-bundle-stats/rollup/rollup-plugin.test.ts index 2232f3dc..76c3a175 100644 --- a/integration-tests/fixtures/generate-bundle-stats/rollup/rollup-plugin.test.ts +++ b/integration-tests/fixtures/generate-bundle-stats/rollup/rollup-plugin.test.ts @@ -125,5 +125,45 @@ describe("Generating rollup stats", () => { }); }); }); + + describe("passing invalid bundle name", () => { + beforeEach(async () => { + const config = new GenerateConfig({ + plugin: "rollup", + configFileName: "rollup", + format: "esm", + detectFormat: "esm", + version: `v${version}`, + detectVersion: "v3", + file_format: "cjs", + enableSourceMaps: true, + }); + + await config.createConfig(); + config.removeBundleName(`test-rollup-v${version}`); + await config.writeConfig(); + }); + + afterEach(async () => { + await $`rm -rf ${rollupConfig(version, "esm")}`; + await $`rm -rf ${rollupApp}/distV${version}`; + }); + + it("warns users and exits process with a code 1", async () => { + const id = `rollup-v${version}-sourcemaps-${Date.now()}`; + const rollup = rollupPath(version); + const configFile = rollupConfig(version, "esm"); + const API_URL = `http://localhost:8000/test-url/${id}/200/false`; + + // build the app + const { exitCode, stdout } = + await $`API_URL=${API_URL} node ${rollup} -c ${configFile}`.nothrow(); + + expect(exitCode).toBe(1); + expect(stdout.toString()).toContain( + "[codecov] bundleName: `` does not match format: `/^[wd_:/@.{}[]$-]+$/`.", + ); + }); + }); }); }); diff --git a/integration-tests/fixtures/generate-bundle-stats/sveltekit/sveltekit-plugin.test.ts b/integration-tests/fixtures/generate-bundle-stats/sveltekit/sveltekit-plugin.test.ts index 9fdc20bb..186027e0 100644 --- a/integration-tests/fixtures/generate-bundle-stats/sveltekit/sveltekit-plugin.test.ts +++ b/integration-tests/fixtures/generate-bundle-stats/sveltekit/sveltekit-plugin.test.ts @@ -139,5 +139,50 @@ describe("Generating sveltekit stats", () => { { timeout: 25_000 }, ); }); + + describe("invalid bundle name is passed", () => { + beforeEach(async () => { + const config = new GenerateConfig({ + plugin: "sveltekit", + configFileName: "vite", + format: "esm", + detectFormat: "esm", + version: `v2`, + detectVersion: "v2", + file_format: "ts", + enableSourceMaps: false, + overrideOutputPath: `${sveltekitApp}/vite.config.ts`, + }); + + await config.createConfig(); + config.removeBundleName(`test-sveltekit-v${version}`); + await config.writeConfig(); + }); + + afterEach(async () => { + await $`rm -rf ${sveltekitApp}/vite.config.ts`; + await $`rm -rf ${sveltekitApp}/.svelte-kit`; + await $`rm -rf ./fixtures/generate-bundle-stats/sveltekit/.svelte-kit`; + }); + + it( + "warns users and exits process with a code 1", + async () => { + const id = `sveltekit-v${version}-${Date.now()}`; + const API_URL = `http://localhost:8000/test-url/${id}/200/false`; + + // prepare and build the app + const { exitCode, stdout } = + await $`cd test-apps/sveltekit && API_URL=${API_URL} pnpm run build`.nothrow(); + + expect(exitCode).toBe(1); + // for some reason this isn't being outputted in the test env + expect(stdout.toString()).toContain( + "[codecov] bundleName: `` does not match format: `/^[wd_:/@.{}[]$-]+$/`.", + ); + }, + { timeout: 25_000 }, + ); + }); }); }); diff --git a/integration-tests/fixtures/generate-bundle-stats/vite/vite-plugin.test.ts b/integration-tests/fixtures/generate-bundle-stats/vite/vite-plugin.test.ts index 4a778ce9..bfba7ac1 100644 --- a/integration-tests/fixtures/generate-bundle-stats/vite/vite-plugin.test.ts +++ b/integration-tests/fixtures/generate-bundle-stats/vite/vite-plugin.test.ts @@ -6,7 +6,7 @@ import { GenerateConfig } from "../../../scripts/gen-config"; const vitePath = (version: number) => `node_modules/viteV${version}/bin/vite.js`; const viteConfig = (version: number, format: string) => - `fixtures/generate-bundle-stats/vite/vite-v${version}-${format}.config.ts`; + `fixtures/generate-bundle-stats/vite/vite-v${version}-${format}.config.*`; const viteApp = "test-apps/vite"; const VERSIONS = [4, 5]; @@ -124,5 +124,45 @@ describe("Generating vite stats", () => { }); }); }); + + describe("invalid bundle name is passed", () => { + beforeEach(async () => { + const config = new GenerateConfig({ + plugin: "vite", + configFileName: "vite", + format: "esm", + detectFormat: "esm", + version: `v${version}`, + detectVersion: "v5", + file_format: "ts", + enableSourceMaps: true, + }); + + await config.createConfig(); + config.removeBundleName(`test-vite-v${version}`); + await config.writeConfig(); + }); + + afterEach(async () => { + await $`rm -rf ${viteConfig(version, "esm")}`; + await $`rm -rf ${viteApp}/distV${version}`; + }); + + it("warns users and exits process with a code 1", async () => { + const id = `vite-v${version}-sourcemaps-${Date.now()}`; + const vite = vitePath(version); + const configFile = viteConfig(version, "esm"); + const API_URL = `http://localhost:8000/test-url/${id}/200/false`; + + // build the app + const { exitCode, stdout } = + await $`API_URL=${API_URL} node ${vite} build -c ${configFile}`.nothrow(); + + expect(exitCode).toBe(1); + expect(stdout.toString()).toContain( + "[codecov] bundleName: `` does not match format: `/^[wd_:/@.{}[]$-]+$/`.", + ); + }); + }); }); }); diff --git a/integration-tests/fixtures/generate-bundle-stats/webpack/webpack-plugin.test.ts b/integration-tests/fixtures/generate-bundle-stats/webpack/webpack-plugin.test.ts index 679381a2..ddcac89d 100644 --- a/integration-tests/fixtures/generate-bundle-stats/webpack/webpack-plugin.test.ts +++ b/integration-tests/fixtures/generate-bundle-stats/webpack/webpack-plugin.test.ts @@ -6,7 +6,7 @@ import { GenerateConfig } from "../../../scripts/gen-config"; const webpackPath = (version: number) => `node_modules/webpackV${version}/bin/webpack.js`; const webpackConfig = (version: number, format: string) => - `fixtures/generate-bundle-stats/webpack/webpack-v${version}-${format}.config.cjs`; + `fixtures/generate-bundle-stats/webpack/webpack-v${version}-${format}.config.*`; const webpackApp = "test-apps/webpack"; const VERSIONS = [5]; @@ -116,5 +116,45 @@ describe("Generating webpack stats", () => { }); }); }); + + describe("invalid bundle name is passed", () => { + beforeEach(async () => { + const config = new GenerateConfig({ + plugin: "webpack", + configFileName: "webpack", + format: "module", + detectFormat: "commonjs", + version: `v${version}`, + detectVersion: "v5", + file_format: "cjs", + enableSourceMaps: false, + }); + + await config.createConfig(); + config.removeBundleName(`test-webpack-v${version}`); + await config.writeConfig(); + }); + + afterEach(async () => { + await $`rm -rf ${webpackConfig(version, "module")}`; + await $`rm -rf ${webpackApp}/distV${version}`; + }); + + it("warns users and exits process with a code 1", async () => { + const id = `webpack-v${version}-sourcemaps-${Date.now()}`; + const webpack = webpackPath(version); + const configFile = webpackConfig(version, "module"); + const API_URL = `http://localhost:8000/test-url/${id}/200/false`; + + // build the app + const { exitCode, stdout } = + await $`API_URL=${API_URL} node ${webpack} --config ${configFile}`.nothrow(); + + expect(exitCode).toBe(1); + expect(stdout.toString()).toContain( + "[codecov] bundleName: `` does not match format: `/^[wd_:/@.{}[]$-]+$/`.", + ); + }); + }); }); }); diff --git a/integration-tests/scripts/gen-config.ts b/integration-tests/scripts/gen-config.ts index 6a1d03b8..43ba33d0 100644 --- a/integration-tests/scripts/gen-config.ts +++ b/integration-tests/scripts/gen-config.ts @@ -114,6 +114,15 @@ export class GenerateConfig { return this.newConfigContents; } + removeBundleName(bundleName: string) { + if (typeof this.newConfigContents === "string") { + this.newConfigContents = this.newConfigContents.replaceAll( + bundleName, + "", + ); + } + } + async writeConfig() { if (typeof this.newConfigContents === "string") { await Bun.write(this.outFilePath, this.newConfigContents); diff --git a/packages/bundler-plugin-core/src/index.ts b/packages/bundler-plugin-core/src/index.ts index bdcac304..b7ccea78 100644 --- a/packages/bundler-plugin-core/src/index.ts +++ b/packages/bundler-plugin-core/src/index.ts @@ -1,26 +1,33 @@ import { type Asset, + type BundleAnalysisUploadPlugin, type Chunk, type Module, type Options, type ProviderUtilInputs, type UploadOverrides, - type BundleAnalysisUploadPlugin, } from "./types.ts"; import { checkNodeVersion } from "./utils/checkNodeVersion.ts"; import { red } from "./utils/logging.ts"; -import { normalizeOptions } from "./utils/normalizeOptions.ts"; +import { handleErrors, normalizeOptions } from "./utils/normalizeOptions.ts"; import { normalizePath } from "./utils/normalizePath.ts"; import { Output } from "./utils/Output.ts"; export type { Asset, + BundleAnalysisUploadPlugin, Chunk, Module, Options, ProviderUtilInputs, UploadOverrides, - BundleAnalysisUploadPlugin, }; -export { checkNodeVersion, normalizeOptions, normalizePath, red, Output }; +export { + checkNodeVersion, + handleErrors, + normalizeOptions, + normalizePath, + Output, + red, +}; diff --git a/packages/bundler-plugin-core/src/utils/__tests__/normalizeOptions.test.ts b/packages/bundler-plugin-core/src/utils/__tests__/normalizeOptions.test.ts index 97a1eb8e..33ff1fec 100644 --- a/packages/bundler-plugin-core/src/utils/__tests__/normalizeOptions.test.ts +++ b/packages/bundler-plugin-core/src/utils/__tests__/normalizeOptions.test.ts @@ -1,8 +1,17 @@ -import { describe, it, expect } from "vitest"; +import { + describe, + expect, + it, + vi, + beforeEach, + afterEach, + type MockInstance, +} from "vitest"; import { type Options } from "../../types.ts"; import { normalizeOptions, type NormalizedOptionsResult, + handleErrors, } from "../normalizeOptions"; interface Test { @@ -195,3 +204,65 @@ describe("normalizeOptions", () => { expect(expectation).toEqual(expected); }); }); + +describe("handleErrors", () => { + let consoleSpy: MockInstance; + + beforeEach(() => { + consoleSpy = vi.spyOn(console, "log"); + }); + + afterEach(() => { + consoleSpy.mockReset(); + }); + + describe("there is a bundleName error", () => { + it("logs out the error message", () => { + handleErrors({ + success: false, + errors: [ + "`bundleName` is required for uploading bundle analysis information.", + ], + }); + + expect(consoleSpy).toHaveBeenCalled(); + expect(consoleSpy).toHaveBeenCalledWith( + "[codecov] `bundleName` is required for uploading bundle analysis information.", + ); + }); + + it("returns shouldExit as true", () => { + const { shouldExit } = handleErrors({ + success: false, + errors: [ + "`bundleName` is required for uploading bundle analysis information.", + ], + }); + expect(shouldExit).toBeTruthy(); + }); + }); + + describe("there is no bundleName error", () => { + it("logs out the error message", () => { + handleErrors({ + success: false, + errors: [ + "`bundleName` is required for uploading bundle analysis information.", + ], + }); + + expect(consoleSpy).toHaveBeenCalled(); + expect(consoleSpy).toHaveBeenCalledWith( + "[codecov] `bundleName` is required for uploading bundle analysis information.", + ); + }); + + it("returns shouldExit as false", () => { + const { shouldExit } = handleErrors({ + success: false, + errors: ["random error"], + }); + expect(shouldExit).toBeFalsy(); + }); + }); +}); diff --git a/packages/bundler-plugin-core/src/utils/normalizeOptions.ts b/packages/bundler-plugin-core/src/utils/normalizeOptions.ts index 826a6210..64892250 100644 --- a/packages/bundler-plugin-core/src/utils/normalizeOptions.ts +++ b/packages/bundler-plugin-core/src/utils/normalizeOptions.ts @@ -1,5 +1,6 @@ -import { type Options } from "../types.ts"; import { z } from "zod"; +import { type Options } from "../types.ts"; +import { red } from "./logging.ts"; export type NormalizedOptions = z.infer< ReturnType @@ -97,10 +98,23 @@ interface NormalizedOptionsSuccess { options: NormalizedOptions; } +/** + * This type represents a union of possible results from the the function. + * + * @see {@link normalizeOptions} + */ export type NormalizedOptionsResult = | NormalizedOptionsFailure | NormalizedOptionsSuccess; +/** + * This function is used to normalize the options provided by the user. Validating the options + * passed by the user, and providing default values for a given set of options if none were + * provided. + * + * @param {Options} userOptions + * @returns {NormalizedOptionsResult} + */ export const normalizeOptions = ( userOptions: Options, ): NormalizedOptionsResult => { @@ -126,3 +140,23 @@ export const normalizeOptions = ( success: true, }; }; + +/** + * This function logs the errors to the console, and will return `shouldExit` if there are errors + * that we should exit the build process for. + * + * @param {NormalizedOptionsFailure} options - The normalized options that failed validation. + */ +export const handleErrors = (options: NormalizedOptionsFailure) => { + let shouldExit = false; + // we probably don't want to exit early so we can provide all the errors to the user + for (const error of options.errors) { + // if the error is related to the bundleName, we should set a flag + if (error.includes("bundleName")) { + shouldExit = true; + } + red(error); + } + + return { shouldExit }; +}; diff --git a/packages/nuxt-plugin/src/index.ts b/packages/nuxt-plugin/src/index.ts index 3b59b502..6b0e87e2 100644 --- a/packages/nuxt-plugin/src/index.ts +++ b/packages/nuxt-plugin/src/index.ts @@ -3,9 +3,9 @@ import { type UnpluginOptions, createVitePlugin } from "unplugin"; import { type Options, normalizeOptions, - red, checkNodeVersion, Output, + handleErrors, } from "@codecov/bundler-plugin-core"; import { _internal_viteBundleAnalysisPlugin } from "@codecov/vite-plugin"; import { addVitePlugin, defineNuxtModule } from "@nuxt/kit"; @@ -24,8 +24,10 @@ const codecovNuxtPluginFactory = createVitePlugin( const normalizedOptions = normalizeOptions(userOptions); if (!normalizedOptions.success) { - for (const error of normalizedOptions.errors) { - red(error); + const { shouldExit } = handleErrors(normalizedOptions); + + if (shouldExit) { + process.exit(1); } return []; } diff --git a/packages/rollup-plugin/src/index.ts b/packages/rollup-plugin/src/index.ts index 59f5fa84..415d78e7 100644 --- a/packages/rollup-plugin/src/index.ts +++ b/packages/rollup-plugin/src/index.ts @@ -6,10 +6,10 @@ import { } from "unplugin"; import { normalizeOptions, - red, type Options, checkNodeVersion, Output, + handleErrors, } from "@codecov/bundler-plugin-core"; import { rollupBundleAnalysisPlugin } from "./rollup-bundle-analysis/rollupBundleAnalysisPlugin"; @@ -22,8 +22,10 @@ const codecovRollupPluginFactory = createRollupPlugin( const normalizedOptions = normalizeOptions(userOptions); if (!normalizedOptions.success) { - for (const error of normalizedOptions.errors) { - red(error); + const { shouldExit } = handleErrors(normalizedOptions); + + if (shouldExit) { + process.exit(1); } return []; } diff --git a/packages/sveltekit-plugin/src/index.ts b/packages/sveltekit-plugin/src/index.ts index 69744977..cfb1dbe1 100644 --- a/packages/sveltekit-plugin/src/index.ts +++ b/packages/sveltekit-plugin/src/index.ts @@ -7,9 +7,9 @@ import { import { type Options, normalizeOptions, - red, checkNodeVersion, Output, + handleErrors, } from "@codecov/bundler-plugin-core"; import { _internal_viteBundleAnalysisPlugin } from "@codecov/vite-plugin"; @@ -23,8 +23,10 @@ const codecovSvelteKitPluginFactory = createVitePlugin( const normalizedOptions = normalizeOptions(userOptions); if (!normalizedOptions.success) { - for (const error of normalizedOptions.errors) { - red(error); + const { shouldExit } = handleErrors(normalizedOptions); + + if (shouldExit) { + process.exit(1); } return []; } diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index c7f7b637..5ccbc908 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -7,9 +7,9 @@ import { import { type Options, normalizeOptions, - red, checkNodeVersion, Output, + handleErrors, } from "@codecov/bundler-plugin-core"; import { viteBundleAnalysisPlugin } from "./vite-bundle-analysis/viteBundleAnalysisPlugin"; @@ -22,16 +22,18 @@ const codecovVitePluginFactory = createVitePlugin( const normalizedOptions = normalizeOptions(userOptions); if (!normalizedOptions.success) { - for (const error of normalizedOptions.errors) { - red(error); + const { shouldExit } = handleErrors(normalizedOptions); + + if (shouldExit) { + process.exit(1); } return []; } const plugins: UnpluginOptions[] = []; - const output = new Output(normalizedOptions.options); const options = normalizedOptions.options; if (options.enableBundleAnalysis) { + const output = new Output(normalizedOptions.options); plugins.push(viteBundleAnalysisPlugin({ output })); } diff --git a/packages/webpack-plugin/src/index.ts b/packages/webpack-plugin/src/index.ts index acde6614..14dfc440 100644 --- a/packages/webpack-plugin/src/index.ts +++ b/packages/webpack-plugin/src/index.ts @@ -6,10 +6,10 @@ import { } from "unplugin"; import { normalizeOptions, - red, type Options, checkNodeVersion, Output, + handleErrors, } from "@codecov/bundler-plugin-core"; import { webpackBundleAnalysisPlugin } from "./webpack-bundle-analysis/webpackBundleAnalysisPlugin"; @@ -22,8 +22,10 @@ const codecovWebpackPluginFactory = createWebpackPlugin( const normalizedOptions = normalizeOptions(userOptions); if (!normalizedOptions.success) { - for (const error of normalizedOptions.errors) { - red(error); + const { shouldExit } = handleErrors(normalizedOptions); + + if (shouldExit) { + process.exit(1); } return []; }