From b55feb0b13fee9dba191cc15c1b3646b3ebcdc78 Mon Sep 17 00:00:00 2001 From: Kate Corcoran Date: Wed, 30 Apr 2025 11:26:20 -0700 Subject: [PATCH 1/3] feat(core): Add hook to customize source map file resolution fixes #731 --- .../src/build-plugin-manager.ts | 3 +- .../src/debug-id-upload.ts | 86 ++++----- packages/bundler-plugin-core/src/types.ts | 18 +- .../adjacent-sourcemap/index.js | 2 + .../adjacent-sourcemap/index.js.map | 1 + .../separate-directory/bundles/index.js | 2 + .../sourcemaps/index.js.map | 1 + .../test/sentry/resolve-source-maps.test.ts | 164 ++++++++++++++++++ .../build-webpack-resolveSourceMap.js | 45 +++++ packages/playground/package.json | 3 +- 10 files changed, 278 insertions(+), 47 deletions(-) create mode 100644 packages/bundler-plugin-core/test/fixtures/resolve-source-maps/adjacent-sourcemap/index.js create mode 100644 packages/bundler-plugin-core/test/fixtures/resolve-source-maps/adjacent-sourcemap/index.js.map create mode 100644 packages/bundler-plugin-core/test/fixtures/resolve-source-maps/separate-directory/bundles/index.js create mode 100644 packages/bundler-plugin-core/test/fixtures/resolve-source-maps/separate-directory/sourcemaps/index.js.map create mode 100644 packages/bundler-plugin-core/test/sentry/resolve-source-maps.test.ts create mode 100644 packages/playground/build-webpack-resolveSourceMap.js diff --git a/packages/bundler-plugin-core/src/build-plugin-manager.ts b/packages/bundler-plugin-core/src/build-plugin-manager.ts index 397f04bf..1e7f6f81 100644 --- a/packages/bundler-plugin-core/src/build-plugin-manager.ts +++ b/packages/bundler-plugin-core/src/build-plugin-manager.ts @@ -518,7 +518,8 @@ export function createSentryBuildPluginManager( tmpUploadFolder, chunkIndex, logger, - options.sourcemaps?.rewriteSources ?? defaultRewriteSourcesHook + options.sourcemaps?.rewriteSources ?? defaultRewriteSourcesHook, + options.sourcemaps?.resolveSourceMap ); } ); diff --git a/packages/bundler-plugin-core/src/debug-id-upload.ts b/packages/bundler-plugin-core/src/debug-id-upload.ts index c9f9302e..c796d23f 100644 --- a/packages/bundler-plugin-core/src/debug-id-upload.ts +++ b/packages/bundler-plugin-core/src/debug-id-upload.ts @@ -1,14 +1,11 @@ import fs from "fs"; import path from "path"; +import * as url from "url" import * as util from "util"; import { promisify } from "util"; import { SentryBuildPluginManager } from "./build-plugin-manager"; import { Logger } from "./logger"; - -interface RewriteSourcesHook { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (source: string, map: any): string; -} +import { ResolveSourceMapHook, RewriteSourcesHook } from "./types"; interface DebugIdUploadPluginOptions { sentryBuildPluginManager: SentryBuildPluginManager; @@ -27,7 +24,8 @@ export async function prepareBundleForDebugIdUpload( uploadFolder: string, chunkIndex: number, logger: Logger, - rewriteSourcesHook: RewriteSourcesHook + rewriteSourcesHook: RewriteSourcesHook, + resolveSourceMapHook: ResolveSourceMapHook | undefined ) { let bundleContent; try { @@ -60,7 +58,8 @@ export async function prepareBundleForDebugIdUpload( const writeSourceMapFilePromise = determineSourceMapPathFromBundle( bundleFilePath, bundleContent, - logger + logger, + resolveSourceMapHook ).then(async (sourceMapPath) => { if (sourceMapPath) { await prepareSourceMapForDebugIdUpload( @@ -114,61 +113,62 @@ function addDebugIdToBundleSource(bundleSource: string, debugId: string): string * * @returns the path to the bundle's source map or `undefined` if none could be found. */ -async function determineSourceMapPathFromBundle( +export async function determineSourceMapPathFromBundle( bundlePath: string, bundleSource: string, - logger: Logger + logger: Logger, + resolveSourceMapHook: ResolveSourceMapHook | undefined ): Promise { - // 1. try to find source map at `sourceMappingURL` location const sourceMappingUrlMatch = bundleSource.match(/^\s*\/\/# sourceMappingURL=(.*)$/m); - if (sourceMappingUrlMatch) { - const sourceMappingUrl = path.normalize(sourceMappingUrlMatch[1] as string); + const sourceMappingUrl = sourceMappingUrlMatch ? sourceMappingUrlMatch[1] as string : undefined; - let isUrl; - let isSupportedUrl; + const searchLocations: string[] = []; + + if (resolveSourceMapHook) { + const customPath = await resolveSourceMapHook(bundlePath, sourceMappingUrl); + if (customPath) { + searchLocations.push(customPath); + } + } + + // 1. try to find source map at `sourceMappingURL` location + if (sourceMappingUrl) { + let parsedUrl: URL | undefined; try { - const url = new URL(sourceMappingUrl); - isUrl = true; - isSupportedUrl = url.protocol === "file:"; + parsedUrl = new URL(sourceMappingUrl); } catch { - isUrl = false; - isSupportedUrl = false; + // noop } - let absoluteSourceMapPath; - if (isSupportedUrl) { - absoluteSourceMapPath = sourceMappingUrl; - } else if (isUrl) { - // noop + if (parsedUrl && parsedUrl.protocol === "file:") { + searchLocations.push(url.fileURLToPath(sourceMappingUrl)); + } else if (parsedUrl) { + // noop, non-file urls don't translate to a local sourcemap file } else if (path.isAbsolute(sourceMappingUrl)) { - absoluteSourceMapPath = sourceMappingUrl; + searchLocations.push(path.normalize(sourceMappingUrl)) } else { - absoluteSourceMapPath = path.join(path.dirname(bundlePath), sourceMappingUrl); - } - - if (absoluteSourceMapPath) { - try { - // Check if the file actually exists - await util.promisify(fs.access)(absoluteSourceMapPath); - return absoluteSourceMapPath; - } catch (e) { - // noop - } + searchLocations.push(path.normalize(path.join(path.dirname(bundlePath), sourceMappingUrl))); } } // 2. try to find source map at path adjacent to chunk source, but with `.map` appended - try { - const adjacentSourceMapFilePath = bundlePath + ".map"; - await util.promisify(fs.access)(adjacentSourceMapFilePath); - return adjacentSourceMapFilePath; - } catch (e) { - // noop + searchLocations.push(bundlePath + ".map") + + for (const searchLocation of searchLocations) { + try { + await util.promisify(fs.access)(searchLocation); + logger.debug(`Source map found for bundle \`${bundlePath}\`: \`${searchLocation}\``) + return searchLocation; + } catch (e) { + // noop + } } // This is just a debug message because it can be quite spammy for some frameworks logger.debug( - `Could not determine source map path for bundle: ${bundlePath} - Did you turn on source map generation in your bundler?` + `Could not determine source map path for bundle: \`${bundlePath}\`` + + ` - Did you turn on source map generation in your bundler?` + + ` (Attempted paths: ${searchLocations.map(e => `\`${e}\``).join(", ")})` ); return undefined; } diff --git a/packages/bundler-plugin-core/src/types.ts b/packages/bundler-plugin-core/src/types.ts index 9ea12397..bb9bedb9 100644 --- a/packages/bundler-plugin-core/src/types.ts +++ b/packages/bundler-plugin-core/src/types.ts @@ -124,8 +124,17 @@ export interface Options { * * Defaults to making all sources relative to `process.cwd()` while building. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - rewriteSources?: (source: string, map: any) => string; + rewriteSources?: RewriteSourcesHook; + + /** + * Hook to customize source map file resolution. + * + * The hook is called with the absolute path of the build artifact and the value of the `//# sourceMappingURL=` + * comment, if present. The hook should then return an absolute path indicating where to find the artifact's + * corresponding `.map` file. If no path is returned or the returned path doesn't exist, the standard source map + * resolution process will be used. + */ + resolveSourceMap?: ResolveSourceMapHook; /** * A glob or an array of globs that specifies the build artifacts that should be deleted after the artifact upload to Sentry has been completed. @@ -356,6 +365,11 @@ export interface Options { }; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type RewriteSourcesHook = (source: string, map: any) => string + +export type ResolveSourceMapHook = (artifactPath: string, sourceMappingUrl: string | undefined) => string | undefined | Promise + export interface ModuleMetadata { // eslint-disable-next-line @typescript-eslint/no-explicit-any [key: string]: any; diff --git a/packages/bundler-plugin-core/test/fixtures/resolve-source-maps/adjacent-sourcemap/index.js b/packages/bundler-plugin-core/test/fixtures/resolve-source-maps/adjacent-sourcemap/index.js new file mode 100644 index 00000000..ff1a49c7 --- /dev/null +++ b/packages/bundler-plugin-core/test/fixtures/resolve-source-maps/adjacent-sourcemap/index.js @@ -0,0 +1,2 @@ +"use strict"; +console.log("wow!"); \ No newline at end of file diff --git a/packages/bundler-plugin-core/test/fixtures/resolve-source-maps/adjacent-sourcemap/index.js.map b/packages/bundler-plugin-core/test/fixtures/resolve-source-maps/adjacent-sourcemap/index.js.map new file mode 100644 index 00000000..11ab57d0 --- /dev/null +++ b/packages/bundler-plugin-core/test/fixtures/resolve-source-maps/adjacent-sourcemap/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"input.js","sourceRoot":"","sources":["input.tsx"],"names":[],"mappings":";AAAA,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA"} \ No newline at end of file diff --git a/packages/bundler-plugin-core/test/fixtures/resolve-source-maps/separate-directory/bundles/index.js b/packages/bundler-plugin-core/test/fixtures/resolve-source-maps/separate-directory/bundles/index.js new file mode 100644 index 00000000..ff1a49c7 --- /dev/null +++ b/packages/bundler-plugin-core/test/fixtures/resolve-source-maps/separate-directory/bundles/index.js @@ -0,0 +1,2 @@ +"use strict"; +console.log("wow!"); \ No newline at end of file diff --git a/packages/bundler-plugin-core/test/fixtures/resolve-source-maps/separate-directory/sourcemaps/index.js.map b/packages/bundler-plugin-core/test/fixtures/resolve-source-maps/separate-directory/sourcemaps/index.js.map new file mode 100644 index 00000000..88e40095 --- /dev/null +++ b/packages/bundler-plugin-core/test/fixtures/resolve-source-maps/separate-directory/sourcemaps/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"input.js","sourceRoot":"","sources":["input.tsx"],"names":[],"mappings":";AAAA,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA"} diff --git a/packages/bundler-plugin-core/test/sentry/resolve-source-maps.test.ts b/packages/bundler-plugin-core/test/sentry/resolve-source-maps.test.ts new file mode 100644 index 00000000..a7fd1872 --- /dev/null +++ b/packages/bundler-plugin-core/test/sentry/resolve-source-maps.test.ts @@ -0,0 +1,164 @@ +import * as path from "path"; +import * as fs from "fs"; +import * as url from "url"; +import { determineSourceMapPathFromBundle } from "../../src/debug-id-upload"; +import { createLogger } from "../../src/logger"; + +const logger = createLogger({ prefix: "[resolve-source-maps-test]", silent: false, debug: false }); +const fixtureDir = path.resolve(__dirname, "../fixtures/resolve-source-maps"); + +const adjacentBundlePath = path.join(fixtureDir, "adjacent-sourcemap/index.js"); +const adjacentSourceMapPath = path.join(fixtureDir, "adjacent-sourcemap/index.js.map"); +const adjacentBundleContent = fs.readFileSync(adjacentBundlePath, "utf-8"); + +const separateBundlePath = path.join(fixtureDir, "separate-directory/bundles/index.js"); +const separateSourceMapPath = path.join(fixtureDir, "separate-directory/sourcemaps/index.js.map"); +const separateBundleContent = fs.readFileSync(separateBundlePath, "utf-8"); + +const sourceMapUrl = "https://sourcemaps.example.com/foo/index.js.map" + +function srcMappingUrl(url: string): string { + return `\n//# sourceMappingURL=${url}` +} + +describe("Resolve source maps", () => { + it("should resolve source maps next to bundles", async () => { + expect( + await determineSourceMapPathFromBundle( + adjacentBundlePath, + adjacentBundleContent, + logger, + undefined + ) + ).toEqual(adjacentSourceMapPath); + }); + + it("shouldn't resolve source maps in separate directories", async () => { + expect( + await determineSourceMapPathFromBundle( + separateBundlePath, + separateBundleContent, + logger, + undefined + ) + ).toBeUndefined(); + }); + + describe("sourceMappingURL resolution", () => { + it("should resolve source maps when sourceMappingURL is a file URL", async () => { + expect( + await determineSourceMapPathFromBundle( + separateBundlePath, + separateBundleContent + srcMappingUrl(url.pathToFileURL(separateSourceMapPath).href), + logger, + undefined + ) + ).toEqual(separateSourceMapPath); + }); + + it("shouldn't resolve source maps when sourceMappingURL is a non-file URL", async () => { + expect( + await determineSourceMapPathFromBundle( + separateBundlePath, + separateBundleContent + srcMappingUrl(sourceMapUrl), + logger, + undefined + ) + ).toBeUndefined(); + }); + + it("should resolve source maps when sourceMappingURL is an absolute path", async () => { + expect( + await determineSourceMapPathFromBundle( + separateBundlePath, + separateBundleContent + srcMappingUrl(separateSourceMapPath), + logger, + undefined + ) + ).toEqual(separateSourceMapPath); + }); + + it("should resolve source maps when sourceMappingURL is a relative path", async () => { + expect( + await determineSourceMapPathFromBundle( + separateBundlePath, + separateBundleContent + srcMappingUrl(path.relative(path.dirname(separateBundlePath), separateSourceMapPath)), + logger, + undefined + ) + ).toEqual(separateSourceMapPath); + }); + }); + + describe("resolveSourceMap hook", () => { + it("should resolve source maps when a resolveSourceMap hook is provided", async () => { + expect( + await determineSourceMapPathFromBundle( + separateBundlePath, + separateBundleContent + srcMappingUrl(sourceMapUrl), + logger, + () => separateSourceMapPath + ) + ).toEqual(separateSourceMapPath); + }); + + it("should pass the correct values to the resolveSourceMap hook", async () => { + const hook = jest.fn(() => separateSourceMapPath) + expect( + await determineSourceMapPathFromBundle( + separateBundlePath, + separateBundleContent + srcMappingUrl(sourceMapUrl), + logger, + hook + ) + ).toEqual(separateSourceMapPath); + expect(hook.mock.calls[0]).toEqual([separateBundlePath, sourceMapUrl]) + }); + + it("should pass the correct values to the resolveSourceMap hook when no sourceMappingURL is present", async () => { + const hook = jest.fn(() => separateSourceMapPath) + expect( + await determineSourceMapPathFromBundle( + separateBundlePath, + separateBundleContent, + logger, + hook + ) + ).toEqual(separateSourceMapPath); + expect(hook.mock.calls[0]).toEqual([separateBundlePath, undefined]) + }); + + it("should prefer resolveSourceMap result over heuristic results", async () => { + expect( + await determineSourceMapPathFromBundle( + adjacentBundlePath, + adjacentBundleContent, + logger, + () => separateSourceMapPath + ) + ).toEqual(separateSourceMapPath); + }); + + it("should fall back when the resolveSourceMap hook returns undefined", async () => { + expect( + await determineSourceMapPathFromBundle( + adjacentBundlePath, + adjacentBundleContent, + logger, + () => undefined + ) + ).toEqual(adjacentSourceMapPath); + }); + + it("should fall back when the resolveSourceMap hook returns a non-existent path", async () => { + expect( + await determineSourceMapPathFromBundle( + adjacentBundlePath, + adjacentBundleContent, + logger, + () => path.join(fixtureDir, "non-existent.js.map") + ) + ).toEqual(adjacentSourceMapPath); + }); + }); +}); \ No newline at end of file diff --git a/packages/playground/build-webpack-resolveSourceMap.js b/packages/playground/build-webpack-resolveSourceMap.js new file mode 100644 index 00000000..203ae222 --- /dev/null +++ b/packages/playground/build-webpack-resolveSourceMap.js @@ -0,0 +1,45 @@ +// @ts-check +const path = require("path"); +const webpack5 = require("webpack5"); +const { sentryWebpackPlugin } = require("@sentry/webpack-plugin"); + +const baseDir = path.resolve(__dirname, "out", "webpack-resolveSourceMap") +const OUTPUT_DIR = path.resolve(baseDir, "webpack"); +const SOURCE_MAPS_DIR = path.resolve(baseDir, "sourcemaps") +const SOURCE_MAP_HOST = "https://sourcemaps.example.com/foo" + +webpack5( + { + cache: false, + entry: "./src/entrypoint1.js", + output: { + filename: "index.js", + path: OUTPUT_DIR, + library: { + type: "commonjs", + name: "ExampleBundle", + }, + }, + mode: "production", + devtool: false, + plugins: [ + new webpack5.SourceMapDevToolPlugin({ + filename: `../sourcemaps/[file].map`, + append: `\n//# sourceMappingURL=${SOURCE_MAP_HOST}/[file].map`, + }), + sentryWebpackPlugin({ + debug: true, + sourcemaps: { + resolveSourceMap(artifactPath, sourceMappingUrl) { + return sourceMappingUrl?.replace(SOURCE_MAP_HOST, SOURCE_MAPS_DIR) + } + } + }), + ], + }, + (err) => { + if (err) { + throw err; + } + } +); diff --git a/packages/playground/package.json b/packages/playground/package.json index 6e71e9cc..9d4ace95 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -4,11 +4,12 @@ "license": "MIT", "private": true, "scripts": { - "build:playground": "run-p build:rollup build:vite build:webpack4 build:webpack5 build:esbuild", + "build:playground": "run-p build:rollup build:vite build:webpack4 build:webpack5 build:webpackResolveSourceMap build:esbuild", "build:rollup": "rollup --config rollup.config.mjs", "build:vite": "vite build --config vite.config.js", "build:webpack4": "node build-webpack4.js", "build:webpack5": "node build-webpack5.js", + "build:webpackResolveSourceMap": "node build-webpack-resolveSourceMap.js", "build:esbuild": "node build-esbuild.js", "build:smallNodeApp": "vite build --config vite.config.smallNodeApp.js", "clean": "run-s clean:build", From e70736d06492b58651ee885e65a9f46fdc083c3e Mon Sep 17 00:00:00 2001 From: Kate Corcoran Date: Mon, 12 May 2025 16:06:01 -0700 Subject: [PATCH 2/3] feat(core): Address `resolveSourceMap` PR comments 1. Added better debug logging around the `resolveSourceMap` hook invocation 2. Expanded the `resolveSourceMap` doc comment to include more detail and an example use case 3. Added `resolveSourceMap` documentation to `generate-documentation-table.ts` 4. Removed the feature-specific playground test previously added --- .../src/debug-id-upload.ts | 6 ++- packages/bundler-plugin-core/src/types.ts | 17 +++++-- .../src/generate-documentation-table.ts | 22 +++++++++ .../build-webpack-resolveSourceMap.js | 45 ------------------- packages/playground/package.json | 3 +- 5 files changed, 42 insertions(+), 51 deletions(-) delete mode 100644 packages/playground/build-webpack-resolveSourceMap.js diff --git a/packages/bundler-plugin-core/src/debug-id-upload.ts b/packages/bundler-plugin-core/src/debug-id-upload.ts index c796d23f..ef98814a 100644 --- a/packages/bundler-plugin-core/src/debug-id-upload.ts +++ b/packages/bundler-plugin-core/src/debug-id-upload.ts @@ -125,7 +125,10 @@ export async function determineSourceMapPathFromBundle( const searchLocations: string[] = []; if (resolveSourceMapHook) { + logger.debug(`Calling sourcemaps.resolveSourceMap(${JSON.stringify(bundlePath)}, ${JSON.stringify(sourceMappingUrl)})`); const customPath = await resolveSourceMapHook(bundlePath, sourceMappingUrl); + logger.debug(`resolveSourceMap hook returned: ${JSON.stringify(customPath)}`); + if (customPath) { searchLocations.push(customPath); } @@ -166,7 +169,8 @@ export async function determineSourceMapPathFromBundle( // This is just a debug message because it can be quite spammy for some frameworks logger.debug( - `Could not determine source map path for bundle: \`${bundlePath}\`` + + `Could not determine source map path for bundle \`${bundlePath}\`` + + ` with sourceMappingURL=${sourceMappingUrl === undefined ? "undefined" : `\`${sourceMappingUrl}\``}` + ` - Did you turn on source map generation in your bundler?` + ` (Attempted paths: ${searchLocations.map(e => `\`${e}\``).join(", ")})` ); diff --git a/packages/bundler-plugin-core/src/types.ts b/packages/bundler-plugin-core/src/types.ts index bb9bedb9..3cca4aea 100644 --- a/packages/bundler-plugin-core/src/types.ts +++ b/packages/bundler-plugin-core/src/types.ts @@ -130,9 +130,20 @@ export interface Options { * Hook to customize source map file resolution. * * The hook is called with the absolute path of the build artifact and the value of the `//# sourceMappingURL=` - * comment, if present. The hook should then return an absolute path indicating where to find the artifact's - * corresponding `.map` file. If no path is returned or the returned path doesn't exist, the standard source map - * resolution process will be used. + * comment, if present. The hook should then return an absolute path (or a promise that resolves to one) indicating + * where to find the artifact's corresponding source map file. If no path is returned or the returned path doesn't + * exist, the standard source map resolution process will be used. + * + * The standard process first tries to resolve based on the `//# sourceMappingURL=` value (it supports `file://` + * urls and absolute/relative paths). If that path doesn't exist, it then looks for a file named + * `${artifactName}.map` in the same directory as the artifact. + * + * Note: This is mostly helpful for complex builds with custom source map generation. For example, if you put source + * maps into a separate directory and rewrite the `//# sourceMappingURL=` comment to something other than a relative + * directory, sentry will be unable to locate the source maps for a given build artifact. This hook allows you to + * implement the resolution process yourself. + * + * Use the `debug` option to print information about source map resolution. */ resolveSourceMap?: ResolveSourceMapHook; diff --git a/packages/dev-utils/src/generate-documentation-table.ts b/packages/dev-utils/src/generate-documentation-table.ts index 197673a2..3719d760 100644 --- a/packages/dev-utils/src/generate-documentation-table.ts +++ b/packages/dev-utils/src/generate-documentation-table.ts @@ -96,6 +96,28 @@ errorHandler: (err) => { fullDescription: "Hook to rewrite the `sources` field inside the source map before being uploaded to Sentry. Does not modify the actual source map. Effectively, this modifies how files inside the stacktrace will show up in Sentry.\n\nDefaults to making all sources relative to `process.cwd()` while building.", }, + { + name: "resolveSourceMap", + type: "(artifactPath: string, sourceMappingUrl: string | undefined) => string | undefined | Promise", + fullDescription: `Hook to customize source map file resolution. + +The hook is called with the absolute path of the build artifact and the value of the \`//# sourceMappingURL=\` +comment, if present. The hook should then return an absolute path (or a promise that resolves to one) indicating +where to find the artifact's corresponding source map file. If no path is returned or the returned path doesn't +exist, the standard source map resolution process will be used. + +The standard process first tries to resolve based on the \`//# sourceMappingURL=\` value (it supports \`file://\` +urls and absolute/relative paths). If that path doesn't exist, it then looks for a file named +\`\${artifactName}.map\` in the same directory as the artifact. + +Note: This is mostly helpful for complex builds with custom source map generation. For example, if you put source +maps into a separate directory and rewrite the \`//# sourceMappingURL=\` comment to something other than a relative +directory, sentry will be unable to locate the source maps for a given build artifact. This hook allows you to +implement the resolution process yourself. + +Use the \`debug\` option to print information about source map resolution. +`, + }, { name: "filesToDeleteAfterUpload", type: "string | string[] | Promise", diff --git a/packages/playground/build-webpack-resolveSourceMap.js b/packages/playground/build-webpack-resolveSourceMap.js deleted file mode 100644 index 203ae222..00000000 --- a/packages/playground/build-webpack-resolveSourceMap.js +++ /dev/null @@ -1,45 +0,0 @@ -// @ts-check -const path = require("path"); -const webpack5 = require("webpack5"); -const { sentryWebpackPlugin } = require("@sentry/webpack-plugin"); - -const baseDir = path.resolve(__dirname, "out", "webpack-resolveSourceMap") -const OUTPUT_DIR = path.resolve(baseDir, "webpack"); -const SOURCE_MAPS_DIR = path.resolve(baseDir, "sourcemaps") -const SOURCE_MAP_HOST = "https://sourcemaps.example.com/foo" - -webpack5( - { - cache: false, - entry: "./src/entrypoint1.js", - output: { - filename: "index.js", - path: OUTPUT_DIR, - library: { - type: "commonjs", - name: "ExampleBundle", - }, - }, - mode: "production", - devtool: false, - plugins: [ - new webpack5.SourceMapDevToolPlugin({ - filename: `../sourcemaps/[file].map`, - append: `\n//# sourceMappingURL=${SOURCE_MAP_HOST}/[file].map`, - }), - sentryWebpackPlugin({ - debug: true, - sourcemaps: { - resolveSourceMap(artifactPath, sourceMappingUrl) { - return sourceMappingUrl?.replace(SOURCE_MAP_HOST, SOURCE_MAPS_DIR) - } - } - }), - ], - }, - (err) => { - if (err) { - throw err; - } - } -); diff --git a/packages/playground/package.json b/packages/playground/package.json index 9d4ace95..6e71e9cc 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -4,12 +4,11 @@ "license": "MIT", "private": true, "scripts": { - "build:playground": "run-p build:rollup build:vite build:webpack4 build:webpack5 build:webpackResolveSourceMap build:esbuild", + "build:playground": "run-p build:rollup build:vite build:webpack4 build:webpack5 build:esbuild", "build:rollup": "rollup --config rollup.config.mjs", "build:vite": "vite build --config vite.config.js", "build:webpack4": "node build-webpack4.js", "build:webpack5": "node build-webpack5.js", - "build:webpackResolveSourceMap": "node build-webpack-resolveSourceMap.js", "build:esbuild": "node build-esbuild.js", "build:smallNodeApp": "vite build --config vite.config.smallNodeApp.js", "clean": "run-s clean:build", From 89518c42f6dc024e35f631f10767be4ef3155381 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 13 May 2025 14:57:01 +0200 Subject: [PATCH 3/3] Fix formatting --- .../src/debug-id-upload.ts | 24 ++++++++++++------- packages/bundler-plugin-core/src/types.ts | 7 ++++-- .../test/sentry/resolve-source-maps.test.ts | 17 ++++++------- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/packages/bundler-plugin-core/src/debug-id-upload.ts b/packages/bundler-plugin-core/src/debug-id-upload.ts index ef98814a..3f5e47e5 100644 --- a/packages/bundler-plugin-core/src/debug-id-upload.ts +++ b/packages/bundler-plugin-core/src/debug-id-upload.ts @@ -1,6 +1,6 @@ import fs from "fs"; import path from "path"; -import * as url from "url" +import * as url from "url"; import * as util from "util"; import { promisify } from "util"; import { SentryBuildPluginManager } from "./build-plugin-manager"; @@ -120,12 +120,16 @@ export async function determineSourceMapPathFromBundle( resolveSourceMapHook: ResolveSourceMapHook | undefined ): Promise { const sourceMappingUrlMatch = bundleSource.match(/^\s*\/\/# sourceMappingURL=(.*)$/m); - const sourceMappingUrl = sourceMappingUrlMatch ? sourceMappingUrlMatch[1] as string : undefined; + const sourceMappingUrl = sourceMappingUrlMatch ? (sourceMappingUrlMatch[1] as string) : undefined; const searchLocations: string[] = []; if (resolveSourceMapHook) { - logger.debug(`Calling sourcemaps.resolveSourceMap(${JSON.stringify(bundlePath)}, ${JSON.stringify(sourceMappingUrl)})`); + logger.debug( + `Calling sourcemaps.resolveSourceMap(${JSON.stringify(bundlePath)}, ${JSON.stringify( + sourceMappingUrl + )})` + ); const customPath = await resolveSourceMapHook(bundlePath, sourceMappingUrl); logger.debug(`resolveSourceMap hook returned: ${JSON.stringify(customPath)}`); @@ -148,19 +152,19 @@ export async function determineSourceMapPathFromBundle( } else if (parsedUrl) { // noop, non-file urls don't translate to a local sourcemap file } else if (path.isAbsolute(sourceMappingUrl)) { - searchLocations.push(path.normalize(sourceMappingUrl)) + searchLocations.push(path.normalize(sourceMappingUrl)); } else { searchLocations.push(path.normalize(path.join(path.dirname(bundlePath), sourceMappingUrl))); } } // 2. try to find source map at path adjacent to chunk source, but with `.map` appended - searchLocations.push(bundlePath + ".map") + searchLocations.push(bundlePath + ".map"); for (const searchLocation of searchLocations) { try { await util.promisify(fs.access)(searchLocation); - logger.debug(`Source map found for bundle \`${bundlePath}\`: \`${searchLocation}\``) + logger.debug(`Source map found for bundle \`${bundlePath}\`: \`${searchLocation}\``); return searchLocation; } catch (e) { // noop @@ -170,9 +174,11 @@ export async function determineSourceMapPathFromBundle( // This is just a debug message because it can be quite spammy for some frameworks logger.debug( `Could not determine source map path for bundle \`${bundlePath}\`` + - ` with sourceMappingURL=${sourceMappingUrl === undefined ? "undefined" : `\`${sourceMappingUrl}\``}` + - ` - Did you turn on source map generation in your bundler?` + - ` (Attempted paths: ${searchLocations.map(e => `\`${e}\``).join(", ")})` + ` with sourceMappingURL=${ + sourceMappingUrl === undefined ? "undefined" : `\`${sourceMappingUrl}\`` + }` + + ` - Did you turn on source map generation in your bundler?` + + ` (Attempted paths: ${searchLocations.map((e) => `\`${e}\``).join(", ")})` ); return undefined; } diff --git a/packages/bundler-plugin-core/src/types.ts b/packages/bundler-plugin-core/src/types.ts index 3cca4aea..1c088a4d 100644 --- a/packages/bundler-plugin-core/src/types.ts +++ b/packages/bundler-plugin-core/src/types.ts @@ -377,9 +377,12 @@ export interface Options { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type RewriteSourcesHook = (source: string, map: any) => string +export type RewriteSourcesHook = (source: string, map: any) => string; -export type ResolveSourceMapHook = (artifactPath: string, sourceMappingUrl: string | undefined) => string | undefined | Promise +export type ResolveSourceMapHook = ( + artifactPath: string, + sourceMappingUrl: string | undefined +) => string | undefined | Promise; export interface ModuleMetadata { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/bundler-plugin-core/test/sentry/resolve-source-maps.test.ts b/packages/bundler-plugin-core/test/sentry/resolve-source-maps.test.ts index a7fd1872..b8e5c309 100644 --- a/packages/bundler-plugin-core/test/sentry/resolve-source-maps.test.ts +++ b/packages/bundler-plugin-core/test/sentry/resolve-source-maps.test.ts @@ -15,10 +15,10 @@ const separateBundlePath = path.join(fixtureDir, "separate-directory/bundles/ind const separateSourceMapPath = path.join(fixtureDir, "separate-directory/sourcemaps/index.js.map"); const separateBundleContent = fs.readFileSync(separateBundlePath, "utf-8"); -const sourceMapUrl = "https://sourcemaps.example.com/foo/index.js.map" +const sourceMapUrl = "https://sourcemaps.example.com/foo/index.js.map"; function srcMappingUrl(url: string): string { - return `\n//# sourceMappingURL=${url}` + return `\n//# sourceMappingURL=${url}`; } describe("Resolve source maps", () => { @@ -82,7 +82,8 @@ describe("Resolve source maps", () => { expect( await determineSourceMapPathFromBundle( separateBundlePath, - separateBundleContent + srcMappingUrl(path.relative(path.dirname(separateBundlePath), separateSourceMapPath)), + separateBundleContent + + srcMappingUrl(path.relative(path.dirname(separateBundlePath), separateSourceMapPath)), logger, undefined ) @@ -103,7 +104,7 @@ describe("Resolve source maps", () => { }); it("should pass the correct values to the resolveSourceMap hook", async () => { - const hook = jest.fn(() => separateSourceMapPath) + const hook = jest.fn(() => separateSourceMapPath); expect( await determineSourceMapPathFromBundle( separateBundlePath, @@ -112,11 +113,11 @@ describe("Resolve source maps", () => { hook ) ).toEqual(separateSourceMapPath); - expect(hook.mock.calls[0]).toEqual([separateBundlePath, sourceMapUrl]) + expect(hook.mock.calls[0]).toEqual([separateBundlePath, sourceMapUrl]); }); it("should pass the correct values to the resolveSourceMap hook when no sourceMappingURL is present", async () => { - const hook = jest.fn(() => separateSourceMapPath) + const hook = jest.fn(() => separateSourceMapPath); expect( await determineSourceMapPathFromBundle( separateBundlePath, @@ -125,7 +126,7 @@ describe("Resolve source maps", () => { hook ) ).toEqual(separateSourceMapPath); - expect(hook.mock.calls[0]).toEqual([separateBundlePath, undefined]) + expect(hook.mock.calls[0]).toEqual([separateBundlePath, undefined]); }); it("should prefer resolveSourceMap result over heuristic results", async () => { @@ -161,4 +162,4 @@ describe("Resolve source maps", () => { ).toEqual(adjacentSourceMapPath); }); }); -}); \ No newline at end of file +});