diff --git a/packages/cli/src/lib/sourcemap/inject.ts b/packages/cli/src/lib/sourcemap/inject.ts index a8c405275..dfd9d2cf9 100644 --- a/packages/cli/src/lib/sourcemap/inject.ts +++ b/packages/cli/src/lib/sourcemap/inject.ts @@ -835,6 +835,41 @@ export async function resolveDirectorySourcemaps( return results; } +/** Next.js build output: any path with a `.next` directory segment. */ +const NEXT_DIST_DIR_RE = /(?:^|\/)\.next(?:\/|$)/; + +/** Next.js server output, whose sourcemaps have separate switches. */ +const NEXT_SERVER_DIR_RE = /(?:^|\/)\.next\/server(?:\/|$)/; + +/** + * How to make the bundler that built `dir` emit sourcemaps. + * + * Nearly every zero-pairs report comes from `@sentry/nextjs`, which uploads + * `.next/server` and `.next/static/...`. The Vite and webpack settings do not + * apply there, so Next.js output gets its own advice (CLI-1FN). + */ +function sourcemapSettingHint(dir: string): string { + const posixDir = dir.replaceAll("\\", "/"); + if (NEXT_SERVER_DIR_RE.test(posixDir)) { + return ( + "For Next.js server output, check that next.config does not turn " + + "server sourcemaps off, e.g. a custom `webpack()` that sets " + + "`config.devtool`, or `experimental.serverSourceMaps: false`." + ); + } + if (NEXT_DIST_DIR_RE.test(posixDir)) { + return ( + "For Next.js, remove `productionBrowserSourceMaps: false` from " + + "next.config (`withSentryConfig` turns it on), or set " + + "`sourcemaps.disable: true` in `withSentryConfig` to skip the upload." + ); + } + return ( + "For Vite/Astro: `vite.environments.client.build.sourcemap: 'hidden'`. " + + "For webpack: `devtool: 'hidden-source-map'`." + ); +} + /** * Build an actionable error for the zero-pairs case, tailored to * which side of the JS/map pairing is missing. @@ -855,9 +890,8 @@ export function buildEmptyDiscoveryError( if (jsFiles > 0 && mapFiles === 0) { return new ValidationError( `Found ${jsFiles} JS file(s) in '${dir}' but no companion .map ` + - "files. Your bundler is not emitting sourcemaps. For Vite/Astro: " + - "`vite.environments.client.build.sourcemap: 'hidden'`. For webpack: " + - "`devtool: 'hidden-source-map'`. Pass --allow-empty to suppress.", + "files. Your bundler is not emitting sourcemaps. " + + `${sourcemapSettingHint(dir)} Pass --allow-empty to suppress.`, "directory", ); } @@ -871,9 +905,10 @@ export function buildEmptyDiscoveryError( } return new ValidationError( `Found ${jsFiles} JS and ${mapFiles} .map file(s) in '${dir}' but ` + - "no JS file has a matching `.map` companion. Check that your " + - "bundler emits JS and sourcemaps with matching basenames. Pass " + - "--allow-empty to suppress.", + "no JS file has a sourcemap: none has a sibling `.map` or a " + + "`//# sourceMappingURL=` comment pointing at a map in this " + + `directory. ${sourcemapSettingHint(dir)} Pass --allow-empty to ` + + "suppress.", "directory", ); } diff --git a/packages/cli/test/commands/sourcemap/upload.test.ts b/packages/cli/test/commands/sourcemap/upload.test.ts index cfb2a1fc0..4b81582d1 100644 --- a/packages/cli/test/commands/sourcemap/upload.test.ts +++ b/packages/cli/test/commands/sourcemap/upload.test.ts @@ -15,6 +15,7 @@ import { uploadCommand } from "../../../src/commands/sourcemap/upload.js"; import * as sourcemapsApi from "../../../src/lib/api/sourcemaps.js"; import { ValidationError } from "../../../src/lib/errors.js"; import { getDebugIdSnippet } from "../../../src/lib/sourcemap/debug-id.js"; +import { buildEmptyDiscoveryError } from "../../../src/lib/sourcemap/inject.js"; type InjectFuncArgs = { ext?: string; @@ -43,6 +44,26 @@ function makeContext() { }; } +describe("buildEmptyDiscoveryError — bundler hint", () => { + test.each([ + ["/app/dist", /vite.*webpack/i], + ["/app/.next/static/chunks", /productionBrowserSourceMaps: false/], + ["/app/.next/static/immutable/chunks", /productionBrowserSourceMaps/], + ["C:\\app\\.next\\static\\chunks", /productionBrowserSourceMaps/], + ["/app/.next/server", /serverSourceMaps/], + ["/app/not.next/static", /vite.*webpack/i], + ])("%s", (dir, hint) => { + for (const diag of [ + { jsFiles: 3, mapFiles: 0 }, + { jsFiles: 3, mapFiles: 1 }, + ]) { + const msg = buildEmptyDiscoveryError(dir, diag).message; + expect(msg).toMatch(hint); + expect(msg).toContain("--allow-empty"); + } + }); +}); + describe("sourcemap inject command — --allow-empty behavior", () => { let dir: string; let func: CmdFunc; @@ -126,7 +147,7 @@ describe("sourcemap inject command — --allow-empty behavior", () => { const msg = (err as Error).message; expect(msg).toContain("1 JS"); expect(msg).toContain("1 .map"); - expect(msg).toContain("matching basename"); + expect(msg).toContain("sourceMappingURL"); } });