From 303e24012925d355ec184d7a246fb8238775c3e4 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 16 Jul 2025 23:10:13 +0200 Subject: [PATCH 1/3] fix: Use `renderChunk` for release injection for Rollup/Rolldown/Vite --- packages/bundler-plugin-core/src/index.ts | 113 +++++++++------------- 1 file changed, 44 insertions(+), 69 deletions(-) diff --git a/packages/bundler-plugin-core/src/index.ts b/packages/bundler-plugin-core/src/index.ts index 25ed91e2..5d1021ce 100644 --- a/packages/bundler-plugin-core/src/index.ts +++ b/packages/bundler-plugin-core/src/index.ts @@ -190,63 +190,59 @@ export function sentryCliBinaryExists(): boolean { return fs.existsSync(SentryCli.getPath()); } +// We need to be careful not to inject the snippet before any `"use strict";`s. +// As an additional complication `"use strict";`s may come after any number of comments. +const COMMENT_USE_STRICT_REGEX = + // Note: CodeQL complains that this regex potentially has n^2 runtime. This likely won't affect realistic files. + /^(?:\s*|\/\*(?:.|\r|\n)*?\*\/|\/\/.*[\n\r])*(?:"[^"]*";|'[^']*';)?/; + +/** + * Simplified `renderChunk` hook type from Rollup. + * We can't reference the type directly because the Vite plugin complains + * about type mismatches + */ +type RenderChunkHook = ( + code: string, + chunk: { + fileName: string; + } +) => { + code: string; + map: SourceMap; +} | null; + export function createRollupReleaseInjectionHooks(injectionCode: string): { - resolveId: UnpluginOptions["resolveId"]; - load: UnpluginOptions["load"]; - transform: UnpluginOptions["transform"]; + renderChunk: RenderChunkHook; } { - const virtualReleaseInjectionFileId = "\0sentry-release-injection-file"; return { - resolveId(id: string) { - if (id === virtualReleaseInjectionFileId) { - return { - id: virtualReleaseInjectionFileId, - external: false, - moduleSideEffects: true, - }; - } else { - return null; - } - }, - - load(id: string) { - if (id === virtualReleaseInjectionFileId) { - return injectionCode; - } else { - return null; - } - }, - - transform(code: string, id: string) { - if (id === virtualReleaseInjectionFileId) { - return null; - } - - // id may contain query and hash which will trip up our file extension logic below - const idWithoutQueryAndHash = stripQueryAndHashFromPath(id); - - if (idWithoutQueryAndHash.match(/\\node_modules\\|\/node_modules\//)) { - return null; - } - + renderChunk(code: string, chunk: { fileName: string }) { if ( - ![".js", ".ts", ".jsx", ".tsx", ".mjs"].some((ending) => - idWithoutQueryAndHash.endsWith(ending) + // chunks could be any file (html, md, ...) + [".js", ".mjs", ".cjs"].some((ending) => + stripQueryAndHashFromPath(chunk.fileName).endsWith(ending) ) ) { - return null; - } + const ms = new MagicString(code, { filename: chunk.fileName }); - const ms = new MagicString(code); + const match = code.match(COMMENT_USE_STRICT_REGEX)?.[0]; - // Appending instead of prepending has less probability of mucking with user's source maps. - // Luckily import statements get hoisted to the top anyways. - ms.append(`\n\n;import "${virtualReleaseInjectionFileId}";`); + if (match) { + // Add injected code after any comments or "use strict" at the beginning of the bundle. + ms.appendLeft(match.length, injectionCode); + } else { + // ms.replace() doesn't work when there is an empty string match (which happens if + // there is neither, a comment, nor a "use strict" at the top of the chunk) so we + // need this special case here. + ms.prepend(injectionCode); + } - return { - code: ms.toString(), - map: ms.generateMap({ hires: "boundary" }), - }; + return { + code: ms.toString(), + map: ms.generateMap({ file: chunk.fileName, hires: "boundary" }), + }; + } else { + return null; // returning null means not modifying the chunk at all + } }, }; } @@ -261,27 +257,6 @@ export function createRollupBundleSizeOptimizationHooks(replacementValues: Sentr }; } -// We need to be careful not to inject the snippet before any `"use strict";`s. -// As an additional complication `"use strict";`s may come after any number of comments. -const COMMENT_USE_STRICT_REGEX = - // Note: CodeQL complains that this regex potentially has n^2 runtime. This likely won't affect realistic files. - /^(?:\s*|\/\*(?:.|\r|\n)*?\*\/|\/\/.*[\n\r])*(?:"[^"]*";|'[^']*';)?/; - -/** - * Simplified `renderChunk` hook type from Rollup. - * We can't reference the type directly because the Vite plugin complains - * about type mismatches - */ -type RenderChunkHook = ( - code: string, - chunk: { - fileName: string; - } -) => { - code: string; - map: SourceMap; -} | null; - export function createRollupDebugIdInjectionHooks(): { renderChunk: RenderChunkHook; } { From 41bc007eb187459f107791d59a4d3ebfc964fd49 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 23 Jul 2025 23:56:22 +0200 Subject: [PATCH 2/3] Minify injected code --- packages/bundler-plugin-core/src/utils.ts | 44 ++-------- .../test/__snapshots__/utils.test.ts.snap | 9 ++ .../bundler-plugin-core/test/utils.test.ts | 83 +------------------ 3 files changed, 19 insertions(+), 117 deletions(-) create mode 100644 packages/bundler-plugin-core/test/__snapshots__/utils.test.ts.snap diff --git a/packages/bundler-plugin-core/src/utils.ts b/packages/bundler-plugin-core/src/utils.ts index 16a8d2e9..4597183f 100644 --- a/packages/bundler-plugin-core/src/utils.ts +++ b/packages/bundler-plugin-core/src/utils.ts @@ -314,28 +314,17 @@ export function generateGlobalInjectorCode({ }): string { // The code below is mostly ternary operators because it saves bundle size. // The checks are to support as many environments as possible. (Node.js, Browser, webworkers, etc.) - let code = `(function(){ - var _global = - typeof window !== 'undefined' ? - window : - typeof global !== 'undefined' ? - global : - typeof globalThis !== 'undefined' ? - globalThis : - typeof self !== 'undefined' ? - self : - {}; - - _global.SENTRY_RELEASE={id:${JSON.stringify(release)}};`; + let code = `!function(){var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};`; + + code += `e.SENTRY_RELEASE={id:${JSON.stringify(release)}};`; if (injectBuildInformation) { const buildInfo = getBuildInformation(); - code += ` - _global.SENTRY_BUILD_INFO=${JSON.stringify(buildInfo)};`; + code += `e.SENTRY_BUILD_INFO=${JSON.stringify(buildInfo)};`; } - code += "})();"; + code += "}();"; return code; } @@ -345,28 +334,7 @@ export function generateModuleMetadataInjectorCode(metadata: any): string { // The code below is mostly ternary operators because it saves bundle size. // The checks are to support as many environments as possible. (Node.js, Browser, webworkers, etc.) // We are merging the metadata objects in case modules are bundled twice with the plugin - return `(function(){ - var _sentryModuleMetadataGlobal = - typeof window !== "undefined" - ? window - : typeof global !== "undefined" - ? global - : typeof globalThis !== "undefined" - ? globalThis - : typeof self !== "undefined" - ? self - : {}; - - _sentryModuleMetadataGlobal._sentryModuleMetadata = - _sentryModuleMetadataGlobal._sentryModuleMetadata || {}; - - _sentryModuleMetadataGlobal._sentryModuleMetadata[new _sentryModuleMetadataGlobal.Error().stack] = - Object.assign( - {}, - _sentryModuleMetadataGlobal._sentryModuleMetadata[new _sentryModuleMetadataGlobal.Error().stack], - ${JSON.stringify(metadata)} - ); -})();`; + return `!function(){var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=Object.assign({},e._sentryModuleMetadata[(new e.Error).stack],${JSON.stringify(metadata)})}();`; } export function getBuildInformation(): { diff --git a/packages/bundler-plugin-core/test/__snapshots__/utils.test.ts.snap b/packages/bundler-plugin-core/test/__snapshots__/utils.test.ts.snap new file mode 100644 index 00000000..b94f01b6 --- /dev/null +++ b/packages/bundler-plugin-core/test/__snapshots__/utils.test.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`generateGlobalInjectorCode generates code with release 1`] = `"!function(){var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};e.SENTRY_RELEASE={id:\\"1.2.3\\"};}();"`; + +exports[`generateGlobalInjectorCode generates code with release and build information 1`] = `"!function(){var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};e.SENTRY_RELEASE={id:\\"1.2.3\\"};e.SENTRY_BUILD_INFO={\\"deps\\":[\\"myDep\\",\\"rollup\\"],\\"depsVersions\\":{\\"rollup\\":3},\\"nodeVersion\\":18};}();"`; + +exports[`generateModuleMetadataInjectorCode generates code with empty metadata object 1`] = `"!function(){var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=Object.assign({},e._sentryModuleMetadata[(new e.Error).stack],{})}();"`; + +exports[`generateModuleMetadataInjectorCode generates code with metadata object 1`] = `"!function(){var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=Object.assign({},e._sentryModuleMetadata[(new e.Error).stack],{\\"file1.js\\":{\\"foo\\":\\"bar\\"},\\"file2.js\\":{\\"bar\\":\\"baz\\"}})}();"`; diff --git a/packages/bundler-plugin-core/test/utils.test.ts b/packages/bundler-plugin-core/test/utils.test.ts index e7787a7f..90fe9666 100644 --- a/packages/bundler-plugin-core/test/utils.test.ts +++ b/packages/bundler-plugin-core/test/utils.test.ts @@ -227,21 +227,7 @@ describe("generateGlobalInjectorCode", () => { injectBuildInformation: false, }); - expect(generatedCode).toMatchInlineSnapshot(` - "(function(){ - var _global = - typeof window !== 'undefined' ? - window : - typeof global !== 'undefined' ? - global : - typeof globalThis !== 'undefined' ? - globalThis : - typeof self !== 'undefined' ? - self : - {}; - - _global.SENTRY_RELEASE={id:\\"1.2.3\\"};})();" - `); + expect(generatedCode).toMatchSnapshot(); }); it("generates code with release and build information", () => { @@ -262,52 +248,14 @@ describe("generateGlobalInjectorCode", () => { injectBuildInformation: true, }); - expect(generatedCode).toMatchInlineSnapshot(` - "(function(){ - var _global = - typeof window !== 'undefined' ? - window : - typeof global !== 'undefined' ? - global : - typeof globalThis !== 'undefined' ? - globalThis : - typeof self !== 'undefined' ? - self : - {}; - - _global.SENTRY_RELEASE={id:\\"1.2.3\\"}; - _global.SENTRY_BUILD_INFO={\\"deps\\":[\\"myDep\\",\\"rollup\\"],\\"depsVersions\\":{\\"rollup\\":3},\\"nodeVersion\\":18};})();" - `); + expect(generatedCode).toMatchSnapshot(); }); }); describe("generateModuleMetadataInjectorCode", () => { it("generates code with empty metadata object", () => { const generatedCode = generateModuleMetadataInjectorCode({}); - expect(generatedCode).toMatchInlineSnapshot(` - "(function(){ - var _sentryModuleMetadataGlobal = - typeof window !== \\"undefined\\" - ? window - : typeof global !== \\"undefined\\" - ? global - : typeof globalThis !== \\"undefined\\" - ? globalThis - : typeof self !== \\"undefined\\" - ? self - : {}; - - _sentryModuleMetadataGlobal._sentryModuleMetadata = - _sentryModuleMetadataGlobal._sentryModuleMetadata || {}; - - _sentryModuleMetadataGlobal._sentryModuleMetadata[new _sentryModuleMetadataGlobal.Error().stack] = - Object.assign( - {}, - _sentryModuleMetadataGlobal._sentryModuleMetadata[new _sentryModuleMetadataGlobal.Error().stack], - {} - ); - })();" - `); + expect(generatedCode).toMatchSnapshot(); }); it("generates code with metadata object", () => { @@ -319,29 +267,6 @@ describe("generateModuleMetadataInjectorCode", () => { bar: "baz", }, }); - expect(generatedCode).toMatchInlineSnapshot(` - "(function(){ - var _sentryModuleMetadataGlobal = - typeof window !== \\"undefined\\" - ? window - : typeof global !== \\"undefined\\" - ? global - : typeof globalThis !== \\"undefined\\" - ? globalThis - : typeof self !== \\"undefined\\" - ? self - : {}; - - _sentryModuleMetadataGlobal._sentryModuleMetadata = - _sentryModuleMetadataGlobal._sentryModuleMetadata || {}; - - _sentryModuleMetadataGlobal._sentryModuleMetadata[new _sentryModuleMetadataGlobal.Error().stack] = - Object.assign( - {}, - _sentryModuleMetadataGlobal._sentryModuleMetadata[new _sentryModuleMetadataGlobal.Error().stack], - {\\"file1.js\\":{\\"foo\\":\\"bar\\"},\\"file2.js\\":{\\"bar\\":\\"baz\\"}} - ); - })();" - `); + expect(generatedCode).toMatchSnapshot(); }); }); From 5b90a75513d43622e3855f36df1584fdd454560b Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 23 Jul 2025 23:57:32 +0200 Subject: [PATCH 3/3] Lint --- packages/bundler-plugin-core/src/utils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/bundler-plugin-core/src/utils.ts b/packages/bundler-plugin-core/src/utils.ts index 4597183f..62dc2a5b 100644 --- a/packages/bundler-plugin-core/src/utils.ts +++ b/packages/bundler-plugin-core/src/utils.ts @@ -334,7 +334,9 @@ export function generateModuleMetadataInjectorCode(metadata: any): string { // The code below is mostly ternary operators because it saves bundle size. // The checks are to support as many environments as possible. (Node.js, Browser, webworkers, etc.) // We are merging the metadata objects in case modules are bundled twice with the plugin - return `!function(){var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=Object.assign({},e._sentryModuleMetadata[(new e.Error).stack],${JSON.stringify(metadata)})}();`; + return `!function(){var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=Object.assign({},e._sentryModuleMetadata[(new e.Error).stack],${JSON.stringify( + metadata + )})}();`; } export function getBuildInformation(): {