From 2a473399af9e0bbca10a60872fe5d9187183dba0 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 29 Jan 2026 18:30:52 +0000 Subject: [PATCH 1/5] PoC: Regular Rollup Plugin --- packages/bundler-plugin-core/src/index.ts | 44 +-- packages/bundler-plugin-core/src/utils.ts | 2 +- .../test/__snapshots__/utils.test.ts.snap | 8 +- .../bundler-plugin-core/test/utils.test.ts | 8 +- packages/rollup-plugin/src/index.ts | 250 ++++++++++++++---- .../rollup-plugin/test/public-api.test.ts | 21 +- 6 files changed, 241 insertions(+), 92 deletions(-) diff --git a/packages/bundler-plugin-core/src/index.ts b/packages/bundler-plugin-core/src/index.ts index a31e4215..e282f625 100644 --- a/packages/bundler-plugin-core/src/index.ts +++ b/packages/bundler-plugin-core/src/index.ts @@ -16,7 +16,7 @@ import { Options, SentrySDKBuildFlags } from "./types"; import { CodeInjection, containsOnlyImports, - generateGlobalInjectorCode, + generateReleaseInjectorCode, generateModuleMetadataInjectorCode, replaceBooleanFlagsInCode, stringToUUID, @@ -118,7 +118,7 @@ export function sentryUnpluginFactory({ "No release name provided. Will not inject release. Please set the `release.name` option to identify your release." ); } else { - const code = generateGlobalInjectorCode({ + const code = generateReleaseInjectorCode({ release: options.release.name, injectBuildInformation: options._experiments.injectBuildInformation || false, }); @@ -227,7 +227,7 @@ export function sentryCliBinaryExists(): boolean { // 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 = +export 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])*(?:"[^"]*";|'[^']*';)?/; @@ -253,7 +253,7 @@ type RenderChunkHook = ( * Checks if a file is a JavaScript file based on its extension. * Handles query strings and hashes in the filename. */ -function isJsFile(fileName: string): boolean { +export function isJsFile(fileName: string): boolean { const cleanFileName = stripQueryAndHashFromPath(fileName); return [".js", ".mjs", ".cjs"].some((ext) => cleanFileName.endsWith(ext)); } @@ -272,7 +272,10 @@ function isJsFile(fileName: string): boolean { * @param facadeModuleId - The facade module ID (if any) - HTML files create facade chunks * @returns true if the chunk should be skipped */ -function shouldSkipCodeInjection(code: string, facadeModuleId: string | null | undefined): boolean { +export function shouldSkipCodeInjection( + code: string, + facadeModuleId: string | null | undefined +): boolean { // Skip empty chunks - these are placeholder chunks that should be optimized away if (code.trim().length === 0) { return true; @@ -369,6 +372,19 @@ export function createRollupInjectionHooks( }; } +export function globFiles(outputDir: string): Promise { + return glob( + ["/**/*.js", "/**/*.mjs", "/**/*.cjs", "/**/*.js.map", "/**/*.mjs.map", "/**/*.cjs.map"].map( + (q) => `${q}?(\\?*)?(#*)` + ), // We want to allow query and hashes strings at the end of files + { + root: outputDir, + absolute: true, + nodir: true, + } + ); +} + export function createRollupDebugIdUploadHooks( upload: (buildArtifacts: string[]) => Promise, _logger: Logger, @@ -388,21 +404,7 @@ export function createRollupDebugIdUploadHooks( try { if (outputOptions.dir) { const outputDir = outputOptions.dir; - const buildArtifacts = await glob( - [ - "/**/*.js", - "/**/*.mjs", - "/**/*.cjs", - "/**/*.js.map", - "/**/*.mjs.map", - "/**/*.cjs.map", - ].map((q) => `${q}?(\\?*)?(#*)`), // We want to allow query and hashes strings at the end of files - { - root: outputDir, - absolute: true, - nodir: true, - } - ); + const buildArtifacts = await globFiles(outputDir); await upload(buildArtifacts); } else if (outputOptions.file) { await upload([outputOptions.file]); @@ -492,3 +494,5 @@ export type { Logger } from "./logger"; export type { Options, SentrySDKBuildFlags } from "./types"; export { CodeInjection, replaceBooleanFlagsInCode, stringToUUID } from "./utils"; export { createSentryBuildPluginManager } from "./build-plugin-manager"; +export { generateReleaseInjectorCode, generateModuleMetadataInjectorCode } from "./utils"; +export { createDebugIdUploadFunction } from "./debug-id-upload"; diff --git a/packages/bundler-plugin-core/src/utils.ts b/packages/bundler-plugin-core/src/utils.ts index 9726895e..45d98ab4 100644 --- a/packages/bundler-plugin-core/src/utils.ts +++ b/packages/bundler-plugin-core/src/utils.ts @@ -305,7 +305,7 @@ export function determineReleaseName(): string | undefined { * Generates code for the global injector which is responsible for setting the global * `SENTRY_RELEASE` & `SENTRY_BUILD_INFO` variables. */ -export function generateGlobalInjectorCode({ +export function generateReleaseInjectorCode({ release, injectBuildInformation, }: { diff --git a/packages/bundler-plugin-core/test/__snapshots__/utils.test.ts.snap b/packages/bundler-plugin-core/test/__snapshots__/utils.test.ts.snap index 1bacbe62..0597ebce 100644 --- a/packages/bundler-plugin-core/test/__snapshots__/utils.test.ts.snap +++ b/packages/bundler-plugin-core/test/__snapshots__/utils.test.ts.snap @@ -1,9 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`generateGlobalInjectorCode generates code with release 1`] = `"!function(){try{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\\"};}catch(e){}}();"`; - -exports[`generateGlobalInjectorCode generates code with release and build information 1`] = `"!function(){try{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};}catch(e){}}();"`; - exports[`generateModuleMetadataInjectorCode generates code with empty metadata object 1`] = `"!function(){try{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]=function(e){for(var n=1;n { +describe("generateReleaseInjectorCode", () => { it("generates code with release", () => { - const generatedCode = generateGlobalInjectorCode({ + const generatedCode = generateReleaseInjectorCode({ release: "1.2.3", injectBuildInformation: false, }); @@ -244,7 +244,7 @@ describe("generateGlobalInjectorCode", () => { }) ); - const generatedCode = generateGlobalInjectorCode({ + const generatedCode = generateReleaseInjectorCode({ release: "1.2.3", injectBuildInformation: true, }); diff --git a/packages/rollup-plugin/src/index.ts b/packages/rollup-plugin/src/index.ts index f29ac5f0..21993770 100644 --- a/packages/rollup-plugin/src/index.ts +++ b/packages/rollup-plugin/src/index.ts @@ -1,62 +1,218 @@ import { - CodeInjection, - sentryUnpluginFactory, + createSentryBuildPluginManager, + generateReleaseInjectorCode, + generateModuleMetadataInjectorCode, + isJsFile, + shouldSkipCodeInjection, Options, - createRollupInjectionHooks, - createRollupDebugIdUploadHooks, - SentrySDKBuildFlags, - createRollupBundleSizeOptimizationHooks, + getDebugIdSnippet, + stringToUUID, + COMMENT_USE_STRICT_REGEX, + createDebugIdUploadFunction, + globFiles, createComponentNameAnnotateHooks, - Logger, + replaceBooleanFlagsInCode, + CodeInjection, } from "@sentry/bundler-plugin-core"; -import type { UnpluginOptions } from "unplugin"; +import MagicString, { SourceMap } from "magic-string"; +import type { TransformHook } from "rollup"; +import * as path from "path"; -function rollupComponentNameAnnotatePlugin( - ignoredComponents: string[], - injectIntoHtml: boolean -): UnpluginOptions { - return { - name: "sentry-rollup-component-name-annotate-plugin", - rollup: createComponentNameAnnotateHooks(ignoredComponents, injectIntoHtml), - }; -} +function hasExistingDebugID(code: string): boolean { + // Check if a debug ID has already been injected to avoid duplicate injection (e.g. by another plugin or Sentry CLI) + const chunkStartSnippet = code.slice(0, 6000); + const chunkEndSnippet = code.slice(-500); -function rollupInjectionPlugin(injectionCode: CodeInjection, debugIds: boolean): UnpluginOptions { - return { - name: "sentry-rollup-injection-plugin", - rollup: createRollupInjectionHooks(injectionCode, debugIds), - }; -} + if ( + chunkStartSnippet.includes("_sentryDebugIdIdentifier") || + chunkEndSnippet.includes("//# debugId=") + ) { + return true; // Debug ID already present, skip injection + } -function rollupDebugIdUploadPlugin( - upload: (buildArtifacts: string[]) => Promise, - logger: Logger, - createDependencyOnBuildArtifacts: () => () => void -): UnpluginOptions { - return { - name: "sentry-rollup-debug-id-upload-plugin", - rollup: createRollupDebugIdUploadHooks(upload, logger, createDependencyOnBuildArtifacts), - }; + return false; } -function rollupBundleSizeOptimizationsPlugin( - replacementValues: SentrySDKBuildFlags -): UnpluginOptions { +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function sentryRollupPlugin(userOptions: Options = {}) { + const sentryBuildPluginManager = createSentryBuildPluginManager(userOptions, { + loggerPrefix: userOptions._metaOptions?.loggerPrefixOverride ?? "[sentry-rollup-plugin]", + buildTool: "rollup", + }); + + const { + logger, + normalizedOptions: options, + bundleSizeOptimizationReplacementValues: replacementValues, + bundleMetadata, + createDependencyOnBuildArtifacts, + } = sentryBuildPluginManager; + + if (options.disable) { + return { + name: "sentry-noop-plugin", + }; + } + + if (process.cwd().match(/\\node_modules\\|\/node_modules\//)) { + logger.warn( + "Running Sentry plugin from within a `node_modules` folder. Some features may not work." + ); + } + + const freeGlobalDependencyOnBuildArtifacts = createDependencyOnBuildArtifacts(); + const upload = createDebugIdUploadFunction({ sentryBuildPluginManager }); + const sourcemapsEnabled = options.sourcemaps?.disable !== true; + const staticInjectionCode = new CodeInjection(); + + if (!options.release.inject) { + logger.debug( + "Release injection disabled via `release.inject` option. Will not inject release." + ); + } else if (!options.release.name) { + logger.debug( + "No release name provided. Will not inject release. Please set the `release.name` option to identify your release." + ); + } else { + staticInjectionCode.append( + generateReleaseInjectorCode({ + release: options.release.name, + injectBuildInformation: options._experiments.injectBuildInformation || false, + }) + ); + } + + if (Object.keys(bundleMetadata).length > 0) { + staticInjectionCode.append(generateModuleMetadataInjectorCode(bundleMetadata)); + } + + const transformAnnotations = options.reactComponentAnnotation?.enabled + ? createComponentNameAnnotateHooks( + options.reactComponentAnnotation?.ignoredComponents || [], + !!options.reactComponentAnnotation?._experimentalInjectIntoHtml + ) + : undefined; + + const transformReplace = Object.keys(replacementValues).length > 0; + const shouldTransform = transformAnnotations || transformReplace; + + function buildStart(): void { + void sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal().catch(() => { + // Telemetry failures are acceptable + }); + } + + function transform(code: string, id: string): ReturnType { + // Component annotations are only in user code and boolean flag replacements are + // only in Sentry code. If we successfully add annotations, we can return early. + + if (transformAnnotations?.transform) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TS complains about 'this' + const result = transformAnnotations.transform(code, id); + if (result) { + return result; + } + } + + if (transformReplace) { + return replaceBooleanFlagsInCode(code, replacementValues); + } + + return null; + } + + function renderChunk( + code: string, + chunk: { fileName: string; facadeModuleId?: string | null } + ): { + code: string; + map: SourceMap; + } | null { + if (!isJsFile(chunk.fileName)) { + return null; // returning null means not modifying the chunk at all + } + + // Skip empty chunks and HTML facade chunks (Vite MPA) + if (shouldSkipCodeInjection(code, chunk.facadeModuleId)) { + return null; + } + + const injectCode = staticInjectionCode.clone(); + + if (sourcemapsEnabled && !hasExistingDebugID(code)) { + const debugId = stringToUUID(code); // generate a deterministic debug ID + injectCode.append(getDebugIdSnippet(debugId)); + } + + if (injectCode.isEmpty()) { + return null; + } + + const ms = new MagicString(code, { filename: chunk.fileName }); + + const match = code.match(COMMENT_USE_STRICT_REGEX)?.[0]; + + if (match) { + // Add injected code after any comments or "use strict" at the beginning of the bundle. + ms.appendLeft(match.length, injectCode.code()); + } 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(injectCode.code()); + } + + return { + code: ms.toString(), + map: ms.generateMap({ file: chunk.fileName, hires: "boundary" as unknown as undefined }), + }; + } + + async function writeBundle( + outputOptions: { dir?: string; file?: string }, + bundle: { [fileName: string]: unknown } + ): Promise { + if (!sourcemapsEnabled) { + return; + } + + try { + await sentryBuildPluginManager.createRelease(); + + if (outputOptions.dir) { + const outputDir = outputOptions.dir; + const buildArtifacts = await globFiles(outputDir); + await upload(buildArtifacts); + } else if (outputOptions.file) { + await upload([outputOptions.file]); + } else { + const buildArtifacts = Object.keys(bundle).map((asset) => path.join(path.resolve(), asset)); + await upload(buildArtifacts); + } + } finally { + freeGlobalDependencyOnBuildArtifacts(); + await sentryBuildPluginManager.deleteArtifacts(); + } + } + + if (shouldTransform) { + return { + name: "sentry-rollup-plugin", + buildStart, + transform, + renderChunk, + writeBundle, + }; + } + return { - name: "sentry-rollup-bundle-size-optimizations-plugin", - rollup: createRollupBundleSizeOptimizationHooks(replacementValues), + name: "sentry-rollup-plugin", + buildStart, + renderChunk, + writeBundle, }; } -const sentryUnplugin = sentryUnpluginFactory({ - injectionPlugin: rollupInjectionPlugin, - componentNameAnnotatePlugin: rollupComponentNameAnnotatePlugin, - debugIdUploadPlugin: rollupDebugIdUploadPlugin, - bundleSizeOptimizationsPlugin: rollupBundleSizeOptimizationsPlugin, -}); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const sentryRollupPlugin: (options?: Options) => any = sentryUnplugin.rollup; - export type { Options as SentryRollupPluginOptions } from "@sentry/bundler-plugin-core"; export { sentryCliBinaryExists } from "@sentry/bundler-plugin-core"; diff --git a/packages/rollup-plugin/test/public-api.test.ts b/packages/rollup-plugin/test/public-api.test.ts index ced22fbc..b4e72418 100644 --- a/packages/rollup-plugin/test/public-api.test.ts +++ b/packages/rollup-plugin/test/public-api.test.ts @@ -1,4 +1,3 @@ -import { Plugin } from "rollup"; import { sentryRollupPlugin } from "../src"; test("Rollup plugin should exist", () => { @@ -11,25 +10,15 @@ describe("sentryRollupPlugin", () => { jest.clearAllMocks(); }); - it("returns an array of rollup plugins", () => { - const plugins = sentryRollupPlugin({ + it("returns a single rollup plugin", () => { + const plugin = sentryRollupPlugin({ authToken: "test-token", org: "test-org", project: "test-project", - }) as Plugin[]; + }); - expect(Array.isArray(plugins)).toBe(true); + expect(Array.isArray(plugin)).not.toBe(true); - const pluginNames = plugins.map((plugin) => plugin.name); - - expect(pluginNames).toEqual( - expect.arrayContaining([ - "sentry-telemetry-plugin", - "sentry-release-management-plugin", - "sentry-rollup-injection-plugin", - "sentry-rollup-debug-id-upload-plugin", - "sentry-file-deletion-plugin", - ]) - ); + expect(plugin.name).toBe("sentry-rollup-plugin"); }); }); From 2c77a7d2eed4cd6ebd4dc9a1e47b79b61fccb80b Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 9 Feb 2026 13:21:37 +0100 Subject: [PATCH 2/5] Sort out the tests and Vite --- packages/bundler-plugin-core/src/index.ts | 141 ------------ .../bundler-plugin-core/test/index.test.ts | 198 +---------------- packages/e2e-tests/package.json | 2 +- .../disabled-sourcemaps-upload.test.ts | 26 --- .../input/bundle.js | 2 - packages/rollup-plugin/package.json | 2 +- packages/rollup-plugin/src/index.ts | 38 +++- .../rollup-plugin/test/public-api.test.ts | 200 +++++++++++++++++- packages/vite-plugin/package.json | 2 +- packages/vite-plugin/src/index.ts | 76 +------ packages/vite-plugin/test/public-api.test.ts | 10 +- yarn.lock | 7 - 12 files changed, 244 insertions(+), 460 deletions(-) delete mode 100644 packages/integration-tests/fixtures/disabled-sourcemaps-upload/disabled-sourcemaps-upload.test.ts delete mode 100644 packages/integration-tests/fixtures/disabled-sourcemaps-upload/input/bundle.js diff --git a/packages/bundler-plugin-core/src/index.ts b/packages/bundler-plugin-core/src/index.ts index e282f625..345f4b9a 100644 --- a/packages/bundler-plugin-core/src/index.ts +++ b/packages/bundler-plugin-core/src/index.ts @@ -6,8 +6,6 @@ import SentryCli from "@sentry/cli"; import { logger } from "@sentry/utils"; import * as fs from "fs"; import { glob } from "glob"; -import MagicString, { SourceMap } from "magic-string"; -import * as path from "path"; import { createUnplugin, TransformResult, UnpluginInstance, UnpluginOptions } from "unplugin"; import { createSentryBuildPluginManager } from "./build-plugin-manager"; import { createDebugIdUploadFunction } from "./debug-id-upload"; @@ -18,8 +16,6 @@ import { containsOnlyImports, generateReleaseInjectorCode, generateModuleMetadataInjectorCode, - replaceBooleanFlagsInCode, - stringToUUID, stripQueryAndHashFromPath, } from "./utils"; @@ -231,24 +227,6 @@ export 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; - facadeModuleId?: string | null; - }, - outputOptions?: unknown, - meta?: { magicString?: MagicString } -) => { - code: string; - readonly map?: SourceMap; -} | null; - /** * Checks if a file is a JavaScript file based on its extension. * Handles query strings and hashes in the filename. @@ -289,89 +267,6 @@ export function shouldSkipCodeInjection( return false; } -export function createRollupBundleSizeOptimizationHooks(replacementValues: SentrySDKBuildFlags): { - transform: UnpluginOptions["transform"]; -} { - return { - transform(code: string) { - return replaceBooleanFlagsInCode(code, replacementValues); - }, - }; -} - -export function createRollupInjectionHooks( - injectionCode: CodeInjection, - debugIds: boolean -): { - renderChunk: RenderChunkHook; -} { - return { - renderChunk( - code: string, - chunk: { fileName: string; facadeModuleId?: string | null }, - _?: unknown, - meta?: { magicString?: MagicString } - ) { - if (!isJsFile(chunk.fileName)) { - return null; // returning null means not modifying the chunk at all - } - - // Skip empty chunks and HTML facade chunks (Vite MPA) - if (shouldSkipCodeInjection(code, chunk.facadeModuleId)) { - return null; - } - - const codeToInject = injectionCode.clone(); - - if (debugIds) { - // Check if a debug ID has already been injected to avoid duplicate injection (e.g. by another plugin or Sentry CLI) - const chunkStartSnippet = code.slice(0, 6000); - const chunkEndSnippet = code.slice(-500); - - if ( - !( - chunkStartSnippet.includes("_sentryDebugIdIdentifier") || - chunkEndSnippet.includes("//# debugId=") - ) - ) { - const debugId = stringToUUID(code); // generate a deterministic debug ID - codeToInject.append(getDebugIdSnippet(debugId)); - } - } - - const ms = meta?.magicString || new MagicString(code, { filename: chunk.fileName }); - const match = code.match(COMMENT_USE_STRICT_REGEX)?.[0]; - - if (match) { - // Add injected code after any comments or "use strict" at the beginning of the bundle. - ms.appendLeft(match.length, codeToInject.code()); - } 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(codeToInject.code()); - } - - // Rolldown can pass a native MagicString instance in meta.magicString - // https://rolldown.rs/in-depth/native-magic-string#usage-examples - if (ms?.constructor?.name === "BindingMagicString") { - // Rolldown docs say to return the magic string instance directly in this case - return { code: ms as unknown as string }; - } - - return { - code: ms.toString(), - get map() { - return ms.generateMap({ - file: chunk.fileName, - hires: "boundary", - }); - }, - }; - }, - }; -} - export function globFiles(outputDir: string): Promise { return glob( ["/**/*.js", "/**/*.mjs", "/**/*.cjs", "/**/*.js.map", "/**/*.mjs.map", "/**/*.cjs.map"].map( @@ -385,42 +280,6 @@ export function globFiles(outputDir: string): Promise { ); } -export function createRollupDebugIdUploadHooks( - upload: (buildArtifacts: string[]) => Promise, - _logger: Logger, - createDependencyOnBuildArtifacts: () => () => void -): { - writeBundle: ( - outputOptions: { dir?: string; file?: string }, - bundle: { [fileName: string]: unknown } - ) => Promise; -} { - const freeGlobalDependencyOnDebugIdSourcemapArtifacts = createDependencyOnBuildArtifacts(); - return { - async writeBundle( - outputOptions: { dir?: string; file?: string }, - bundle: { [fileName: string]: unknown } - ) { - try { - if (outputOptions.dir) { - const outputDir = outputOptions.dir; - const buildArtifacts = await globFiles(outputDir); - await upload(buildArtifacts); - } else if (outputOptions.file) { - await upload([outputOptions.file]); - } else { - const buildArtifacts = Object.keys(bundle).map((asset) => - path.join(path.resolve(), asset) - ); - await upload(buildArtifacts); - } - } finally { - freeGlobalDependencyOnDebugIdSourcemapArtifacts(); - } - }, - }; -} - export function createComponentNameAnnotateHooks( ignoredComponents: string[], injectIntoHtml: boolean diff --git a/packages/bundler-plugin-core/test/index.test.ts b/packages/bundler-plugin-core/test/index.test.ts index 863d3666..2b56d984 100644 --- a/packages/bundler-plugin-core/test/index.test.ts +++ b/packages/bundler-plugin-core/test/index.test.ts @@ -1,6 +1,6 @@ import { Compiler } from "webpack"; -import { getDebugIdSnippet, sentryUnpluginFactory, createRollupInjectionHooks } from "../src"; -import { CodeInjection, containsOnlyImports } from "../src/utils"; +import { getDebugIdSnippet, sentryUnpluginFactory } from "../src"; +import { containsOnlyImports } from "../src/utils"; describe("getDebugIdSnippet", () => { it("returns the debugId injection snippet for a passed debugId", () => { @@ -142,200 +142,6 @@ app.mount('#app'); }); }); -describe("createRollupInjectionHooks", () => { - const inject = new CodeInjection(); - const hooks = createRollupInjectionHooks(inject, true); - - beforeEach(() => { - inject.clear(); - }); - - describe("renderChunk", () => { - it("should inject debug ID into clean JavaScript files", () => { - const code = 'console.log("Hello world");'; - const result = hooks.renderChunk(code, { fileName: "bundle.js" }); - - expect(result).not.toBeNull(); - expect(result?.code).toMatchInlineSnapshot( - `"!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"d4309f93-5358-4ae1-bcf0-3813aa590eb5\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-d4309f93-5358-4ae1-bcf0-3813aa590eb5\\");}catch(e){}}();console.log(\\"Hello world\\");"` - ); - }); - - it("should inject debug ID after 'use strict'", () => { - const code = '"use strict";\nconsole.log("Hello world");'; - const result = hooks.renderChunk(code, { fileName: "bundle.js" }); - - expect(result).not.toBeNull(); - expect(result?.code).toMatchInlineSnapshot(` - "\\"use strict\\";!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"79a86c07-8ecc-4367-82b0-88cf822f2d41\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-79a86c07-8ecc-4367-82b0-88cf822f2d41\\");}catch(e){}}(); - console.log(\\"Hello world\\");" - `); - }); - - it.each([ - ["bundle.js"], - ["bundle.mjs"], - ["bundle.cjs"], - ["bundle.js?foo=bar"], - ["bundle.js#hash"], - ])("should process file '%s': %s", (fileName) => { - const code = 'console.log("test");'; - const result = hooks.renderChunk(code, { fileName }); - - expect(result).not.toBeNull(); - expect(result?.code).toMatchInlineSnapshot( - `"!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"b80112c0-6818-486d-96f0-185c023439b4\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-b80112c0-6818-486d-96f0-185c023439b4\\");}catch(e){}}();console.log(\\"test\\");"` - ); - }); - - it.each([["index.html"], ["styles.css"]])("should NOT process file '%s': %s", (fileName) => { - const code = 'console.log("test");'; - const result = hooks.renderChunk(code, { fileName }); - - expect(result).toBeNull(); - }); - - it.each([ - [ - "inline format at start", - ';{try{(function(){var e="undefined"!=typeof window?window:e._sentryDebugIdIdentifier="sentry-dbid-existing-id");})();}catch(e){}};console.log("test");', - ], - [ - "comment format at end", - 'console.log("test");\n//# debugId=f6ccd6f4-7ea0-4854-8384-1c9f8340af81\n//# sourceMappingURL=bundle.js.map', - ], - [ - "inline format with large file", - '"use strict";\n' + - "// comment\n".repeat(10) + - ';{try{(function(){var e="undefined"!=typeof window?window:e._sentryDebugIdIdentifier="sentry-dbid-existing-id");})();}catch(e){}};' + - '\nconsole.log("line");\n'.repeat(100), - ], - ])("should NOT inject when debug ID already exists (%s)", (_description, code) => { - const result = hooks.renderChunk(code, { fileName: "bundle.js" }); - expect(result?.code).not.toContain("_sentryDebugIds"); - }); - - it("should only check boundaries for performance (not entire file)", () => { - // Inline format beyond first 6KB boundary - const codeWithInlineBeyond6KB = - "a".repeat(6100) + - ';{try{(function(){var e="undefined"!=typeof window?window:e._sentryDebugIdIdentifier="sentry-dbid-existing-id");})();}catch(e){}};'; - - expect(hooks.renderChunk(codeWithInlineBeyond6KB, { fileName: "bundle.js" })).not.toBeNull(); - - // Comment format beyond last 500 bytes boundary - const codeWithCommentBeyond500B = - "//# debugId=f6ccd6f4-7ea0-4854-8384-1c9f8340af81\n" + "a".repeat(600); - - expect( - hooks.renderChunk(codeWithCommentBeyond500B, { fileName: "bundle.js" }) - ).not.toBeNull(); - }); - - describe("HTML facade chunks (MPA vs SPA)", () => { - // Issue #829: MPA facades should be skipped - // Regression fix: SPA main bundles with HTML facades should NOT be skipped - - it.each([ - ["empty", ""], - ["only side-effect imports", `import './shared-module.js';`], - ["only named imports", `import { foo, bar } from './shared-module.js';`], - ["only re-exports", `export * from './shared-module.js';`], - [ - "multiple imports and comments", - `// This is a facade module -import './moduleA.js'; -import { x } from './moduleB.js'; -/* block comment */ -export * from './moduleC.js';`, - ], - ["'use strict' and imports only", `"use strict";\nimport './shared-module.js';`], - ["query string in facadeModuleId", `import './shared.js';`, "?query=param"], - ["hash in facadeModuleId", `import './shared.js';`, "#hash"], - ])("should SKIP HTML facade chunks: %s", (_, code, suffix = "") => { - const result = hooks.renderChunk(code, { - fileName: "page1.js", - facadeModuleId: `/path/to/page1.html${suffix}`, - }); - expect(result).toBeNull(); - }); - - it("should inject into HTML facade with function declarations", () => { - const result = hooks.renderChunk(`function main() { console.log("hello"); }`, { - fileName: "index.js", - facadeModuleId: "/path/to/index.html", - }); - expect(result).not.toBeNull(); - expect(result?.code).toMatchInlineSnapshot( - `"!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"c4c89e04-3658-4874-b25b-07e638185091\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-c4c89e04-3658-4874-b25b-07e638185091\\");}catch(e){}}();function main() { console.log(\\"hello\\"); }"` - ); - }); - - it("should inject into HTML facade with variable declarations", () => { - const result = hooks.renderChunk(`const x = 42;`, { - fileName: "index.js", - facadeModuleId: "/path/to/index.html", - }); - expect(result).not.toBeNull(); - expect(result?.code).toMatchInlineSnapshot( - `"!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"43e69766-1963-49f2-a291-ff8de60cc652\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-43e69766-1963-49f2-a291-ff8de60cc652\\");}catch(e){}}();const x = 42;"` - ); - }); - - it("should inject into HTML facade with substantial code (SPA main bundle)", () => { - const code = `import { initApp } from './app.js'; - -const config = { debug: true }; - -function bootstrap() { - initApp(config); -} - -bootstrap();`; - const result = hooks.renderChunk(code, { - fileName: "index.js", - facadeModuleId: "/path/to/index.html", - }); - expect(result).not.toBeNull(); - expect(result?.code).toMatchInlineSnapshot(` - "!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"d0c4524b-496e-45a4-9852-7558d043ba3c\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-d0c4524b-496e-45a4-9852-7558d043ba3c\\");}catch(e){}}();import { initApp } from './app.js'; - - const config = { debug: true }; - - function bootstrap() { - initApp(config); - } - - bootstrap();" - `); - }); - - it("should inject into HTML facade with mixed imports and code", () => { - const result = hooks.renderChunk( - `import './polyfills.js';\nimport { init } from './app.js';\n\ninit();`, - { fileName: "index.js", facadeModuleId: "/path/to/index.html" } - ); - expect(result).not.toBeNull(); - expect(result?.code).toMatchInlineSnapshot(` - "!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"28f0bbaa-9aeb-40c4-98c9-4e44f1d4e175\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-28f0bbaa-9aeb-40c4-98c9-4e44f1d4e175\\");}catch(e){}}();import './polyfills.js'; - import { init } from './app.js'; - - init();" - `); - }); - - it("should inject into regular JS chunks (no HTML facade)", () => { - const result = hooks.renderChunk(`console.log("Hello");`, { fileName: "bundle.js" }); - expect(result).not.toBeNull(); - expect(result?.code).toMatchInlineSnapshot( - `"!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"79f18a7f-ca16-4168-9797-906c82058367\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-79f18a7f-ca16-4168-9797-906c82058367\\");}catch(e){}}();console.log(\\"Hello\\");"` - ); - }); - }); - }); -}); - describe("sentryUnpluginFactory sourcemaps.disable behavior", () => { const mockComponentNameAnnotatePlugin = jest.fn(() => ({ name: "mock-component-name-annotate-plugin", diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index 5684e3c7..51f713d7 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -34,7 +34,7 @@ "glob": "8.0.3", "jest": "^28.1.3", "premove": "^4.0.0", - "rollup": "2.77.0", + "rollup": "3.2.0", "ts-node": "^10.9.1", "vite": "3.0.0", "webpack4": "npm:webpack@^4", diff --git a/packages/integration-tests/fixtures/disabled-sourcemaps-upload/disabled-sourcemaps-upload.test.ts b/packages/integration-tests/fixtures/disabled-sourcemaps-upload/disabled-sourcemaps-upload.test.ts deleted file mode 100644 index dd22ef31..00000000 --- a/packages/integration-tests/fixtures/disabled-sourcemaps-upload/disabled-sourcemaps-upload.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { sentryRollupPlugin } from "@sentry/rollup-plugin"; - -const debugIdUploadPluginName = "sentry-rollup-debug-id-upload-plugin"; - -test("should not call upload plugin when sourcemaps are disabled", () => { - const plugins = sentryRollupPlugin({ - telemetry: false, - sourcemaps: { - disable: true, - }, - }) as Array<{ name: string }>; - - const debugIdUploadPlugin = plugins.find((plugin) => plugin.name === debugIdUploadPluginName); - - expect(debugIdUploadPlugin).toBeUndefined(); -}); - -test("should call upload plugin when sourcemaps are enabled", () => { - const plugins = sentryRollupPlugin({ - telemetry: false, - }) as Array<{ name: string }>; - - const debugIdUploadPlugin = plugins.find((plugin) => plugin.name === debugIdUploadPluginName); - - expect(debugIdUploadPlugin).toBeDefined(); -}); diff --git a/packages/integration-tests/fixtures/disabled-sourcemaps-upload/input/bundle.js b/packages/integration-tests/fixtures/disabled-sourcemaps-upload/input/bundle.js deleted file mode 100644 index 85b0fd2b..00000000 --- a/packages/integration-tests/fixtures/disabled-sourcemaps-upload/input/bundle.js +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-console -console.log("Beep!"); diff --git a/packages/rollup-plugin/package.json b/packages/rollup-plugin/package.json index bffcd031..28911e43 100644 --- a/packages/rollup-plugin/package.json +++ b/packages/rollup-plugin/package.json @@ -50,7 +50,7 @@ }, "dependencies": { "@sentry/bundler-plugin-core": "4.9.0", - "unplugin": "1.0.1" + "magic-string": "0.30.8" }, "peerDependencies": { "rollup": ">=3.2.0" diff --git a/packages/rollup-plugin/src/index.ts b/packages/rollup-plugin/src/index.ts index 21993770..2ab2e736 100644 --- a/packages/rollup-plugin/src/index.ts +++ b/packages/rollup-plugin/src/index.ts @@ -33,11 +33,14 @@ function hasExistingDebugID(code: string): boolean { return false; } +/** + * @ignore - this is the internal plugin factory function + */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function sentryRollupPlugin(userOptions: Options = {}) { +export function _rollupPluginInternal(userOptions: Options = {}, buildTool: string) { const sentryBuildPluginManager = createSentryBuildPluginManager(userOptions, { - loggerPrefix: userOptions._metaOptions?.loggerPrefixOverride ?? "[sentry-rollup-plugin]", - buildTool: "rollup", + loggerPrefix: userOptions._metaOptions?.loggerPrefixOverride ?? `[sentry-${buildTool}-plugin]`, + buildTool, }); const { @@ -124,10 +127,12 @@ export function sentryRollupPlugin(userOptions: Options = {}) { function renderChunk( code: string, - chunk: { fileName: string; facadeModuleId?: string | null } + chunk: { fileName: string; facadeModuleId?: string | null }, + _?: unknown, + meta?: { magicString?: MagicString } ): { code: string; - map: SourceMap; + map?: SourceMap; } | null { if (!isJsFile(chunk.fileName)) { return null; // returning null means not modifying the chunk at all @@ -149,8 +154,7 @@ export function sentryRollupPlugin(userOptions: Options = {}) { return null; } - const ms = new MagicString(code, { filename: chunk.fileName }); - + const ms = meta?.magicString || new MagicString(code, { filename: chunk.fileName }); const match = code.match(COMMENT_USE_STRICT_REGEX)?.[0]; if (match) { @@ -163,6 +167,13 @@ export function sentryRollupPlugin(userOptions: Options = {}) { ms.prepend(injectCode.code()); } + // Rolldown can pass a native MagicString instance in meta.magicString + // https://rolldown.rs/in-depth/native-magic-string#usage-examples + if (ms?.constructor?.name === "BindingMagicString") { + // Rolldown docs say to return the magic string instance directly in this case + return { code: ms as unknown as string }; + } + return { code: ms.toString(), map: ms.generateMap({ file: chunk.fileName, hires: "boundary" as unknown as undefined }), @@ -196,9 +207,11 @@ export function sentryRollupPlugin(userOptions: Options = {}) { } } + const name = `sentry-${buildTool}-plugin`; + if (shouldTransform) { return { - name: "sentry-rollup-plugin", + name, buildStart, transform, renderChunk, @@ -207,12 +220,19 @@ export function sentryRollupPlugin(userOptions: Options = {}) { } return { - name: "sentry-rollup-plugin", + name, buildStart, renderChunk, writeBundle, }; } +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/no-explicit-any +export function sentryRollupPlugin(userOptions: Options = {}): any { + // We return an array here so we don't break backwards compatibility with what + // unplugin used to return + return [_rollupPluginInternal(userOptions, "rollup")]; +} + export type { Options as SentryRollupPluginOptions } from "@sentry/bundler-plugin-core"; export { sentryCliBinaryExists } from "@sentry/bundler-plugin-core"; diff --git a/packages/rollup-plugin/test/public-api.test.ts b/packages/rollup-plugin/test/public-api.test.ts index b4e72418..22190364 100644 --- a/packages/rollup-plugin/test/public-api.test.ts +++ b/packages/rollup-plugin/test/public-api.test.ts @@ -1,4 +1,5 @@ import { sentryRollupPlugin } from "../src"; +import { Plugin, SourceMap } from "rollup"; test("Rollup plugin should exist", () => { expect(sentryRollupPlugin).toBeDefined(); @@ -11,7 +12,7 @@ describe("sentryRollupPlugin", () => { }); it("returns a single rollup plugin", () => { - const plugin = sentryRollupPlugin({ + const [plugin] = sentryRollupPlugin({ authToken: "test-token", org: "test-org", project: "test-project", @@ -19,6 +20,201 @@ describe("sentryRollupPlugin", () => { expect(Array.isArray(plugin)).not.toBe(true); - expect(plugin.name).toBe("sentry-rollup-plugin"); + expect(plugin?.name).toBe("sentry-rollup-plugin"); + }); +}); + +describe("Hooks", () => { + const [plugin] = sentryRollupPlugin({ release: { inject: false } }) as [Plugin]; + + const renderChunk = plugin.renderChunk as ( + code: string, + chunkInfo: { fileName: string; facadeModuleId?: string } + ) => { + code: string; + map: SourceMap; + } | null; + + describe("renderChunk", () => { + it("should inject debug ID into clean JavaScript files", () => { + const code = 'console.log("Hello world");'; + const result = renderChunk(code, { fileName: "bundle.js" }); + + expect(result).not.toBeNull(); + expect(result?.code).toMatchInlineSnapshot( + `"!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"d4309f93-5358-4ae1-bcf0-3813aa590eb5\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-d4309f93-5358-4ae1-bcf0-3813aa590eb5\\");}catch(e){}}();console.log(\\"Hello world\\");"` + ); + }); + + it("should inject debug ID after 'use strict'", () => { + const code = '"use strict";\nconsole.log("Hello world");'; + const result = renderChunk(code, { fileName: "bundle.js" }); + + expect(result).not.toBeNull(); + expect(result?.code).toMatchInlineSnapshot(` + "\\"use strict\\";!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"79a86c07-8ecc-4367-82b0-88cf822f2d41\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-79a86c07-8ecc-4367-82b0-88cf822f2d41\\");}catch(e){}}(); + console.log(\\"Hello world\\");" + `); + }); + + it.each([ + ["bundle.js"], + ["bundle.mjs"], + ["bundle.cjs"], + ["bundle.js?foo=bar"], + ["bundle.js#hash"], + ])("should process file '%s': %s", (fileName) => { + const code = 'console.log("test");'; + const result = renderChunk(code, { fileName }); + + expect(result).not.toBeNull(); + expect(result?.code).toMatchInlineSnapshot( + `"!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"b80112c0-6818-486d-96f0-185c023439b4\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-b80112c0-6818-486d-96f0-185c023439b4\\");}catch(e){}}();console.log(\\"test\\");"` + ); + }); + + it.each([["index.html"], ["styles.css"]])("should NOT process file '%s': %s", (fileName) => { + const code = 'console.log("test");'; + const result = renderChunk(code, { fileName }); + + expect(result).toBeNull(); + }); + + it.each([ + [ + "inline format at start", + ';{try{(function(){var e="undefined"!=typeof window?window:e._sentryDebugIdIdentifier="sentry-dbid-existing-id");})();}catch(e){}};console.log("test");', + ], + [ + "comment format at end", + 'console.log("test");\n//# debugId=f6ccd6f4-7ea0-4854-8384-1c9f8340af81\n//# sourceMappingURL=bundle.js.map', + ], + [ + "inline format with large file", + '"use strict";\n' + + "// comment\n".repeat(10) + + ';{try{(function(){var e="undefined"!=typeof window?window:e._sentryDebugIdIdentifier="sentry-dbid-existing-id");})();}catch(e){}};' + + '\nconsole.log("line");\n'.repeat(100), + ], + ])("should NOT inject when debug ID already exists (%s)", (_description, code) => { + const result = renderChunk(code, { fileName: "bundle.js" }); + expect(result).toBeNull(); + }); + + it("should only check boundaries for performance (not entire file)", () => { + // Inline format beyond first 6KB boundary + const codeWithInlineBeyond6KB = + "a".repeat(6100) + + ';{try{(function(){var e="undefined"!=typeof window?window:e._sentryDebugIdIdentifier="sentry-dbid-existing-id");})();}catch(e){}};'; + + expect(renderChunk(codeWithInlineBeyond6KB, { fileName: "bundle.js" })).not.toBeNull(); + + // Comment format beyond last 500 bytes boundary + const codeWithCommentBeyond500B = + "//# debugId=f6ccd6f4-7ea0-4854-8384-1c9f8340af81\n" + "a".repeat(600); + + expect(renderChunk(codeWithCommentBeyond500B, { fileName: "bundle.js" })).not.toBeNull(); + }); + + describe("HTML facade chunks (MPA vs SPA)", () => { + // Issue #829: MPA facades should be skipped + // Regression fix: SPA main bundles with HTML facades should NOT be skipped + + it.each([ + ["empty", ""], + ["only side-effect imports", `import './shared-module.js';`], + ["only named imports", `import { foo, bar } from './shared-module.js';`], + ["only re-exports", `export * from './shared-module.js';`], + [ + "multiple imports and comments", + `// This is a facade module +import './moduleA.js'; +import { x } from './moduleB.js'; +/* block comment */ +export * from './moduleC.js';`, + ], + ["'use strict' and imports only", `"use strict";\nimport './shared-module.js';`], + ["query string in facadeModuleId", `import './shared.js';`, "?query=param"], + ["hash in facadeModuleId", `import './shared.js';`, "#hash"], + ])("should SKIP HTML facade chunks: %s", (_, code, suffix = "") => { + const result = renderChunk(code, { + fileName: "page1.js", + facadeModuleId: `/path/to/page1.html${suffix}`, + }); + expect(result).toBeNull(); + }); + + it("should inject into HTML facade with function declarations", () => { + const result = renderChunk(`function main() { console.log("hello"); }`, { + fileName: "index.js", + facadeModuleId: "/path/to/index.html", + }); + expect(result).not.toBeNull(); + expect(result?.code).toMatchInlineSnapshot( + `"!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"c4c89e04-3658-4874-b25b-07e638185091\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-c4c89e04-3658-4874-b25b-07e638185091\\");}catch(e){}}();function main() { console.log(\\"hello\\"); }"` + ); + }); + + it("should inject into HTML facade with variable declarations", () => { + const result = renderChunk(`const x = 42;`, { + fileName: "index.js", + facadeModuleId: "/path/to/index.html", + }); + expect(result).not.toBeNull(); + expect(result?.code).toMatchInlineSnapshot( + `"!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"43e69766-1963-49f2-a291-ff8de60cc652\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-43e69766-1963-49f2-a291-ff8de60cc652\\");}catch(e){}}();const x = 42;"` + ); + }); + + it("should inject into HTML facade with substantial code (SPA main bundle)", () => { + const code = `import { initApp } from './app.js'; + +const config = { debug: true }; + +function bootstrap() { + initApp(config); +} + +bootstrap();`; + const result = renderChunk(code, { + fileName: "index.js", + facadeModuleId: "/path/to/index.html", + }); + expect(result).not.toBeNull(); + expect(result?.code).toMatchInlineSnapshot(` + "!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"d0c4524b-496e-45a4-9852-7558d043ba3c\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-d0c4524b-496e-45a4-9852-7558d043ba3c\\");}catch(e){}}();import { initApp } from './app.js'; + + const config = { debug: true }; + + function bootstrap() { + initApp(config); + } + + bootstrap();" + `); + }); + + it("should inject into HTML facade with mixed imports and code", () => { + const result = renderChunk( + `import './polyfills.js';\nimport { init } from './app.js';\n\ninit();`, + { fileName: "index.js", facadeModuleId: "/path/to/index.html" } + ); + expect(result).not.toBeNull(); + expect(result?.code).toMatchInlineSnapshot(` + "!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"28f0bbaa-9aeb-40c4-98c9-4e44f1d4e175\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-28f0bbaa-9aeb-40c4-98c9-4e44f1d4e175\\");}catch(e){}}();import './polyfills.js'; + import { init } from './app.js'; + + init();" + `); + }); + + it("should inject into regular JS chunks (no HTML facade)", () => { + const result = renderChunk(`console.log("Hello");`, { fileName: "bundle.js" }); + expect(result).not.toBeNull(); + expect(result?.code).toMatchInlineSnapshot( + `"!function(){try{var e=\\"undefined\\"!=typeof window?window:\\"undefined\\"!=typeof global?global:\\"undefined\\"!=typeof globalThis?globalThis:\\"undefined\\"!=typeof self?self:{};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]=\\"79f18a7f-ca16-4168-9797-906c82058367\\",e._sentryDebugIdIdentifier=\\"sentry-dbid-79f18a7f-ca16-4168-9797-906c82058367\\");}catch(e){}}();console.log(\\"Hello\\");"` + ); + }); + }); }); }); diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index e4e6dc2e..69c70f5a 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -49,7 +49,7 @@ }, "dependencies": { "@sentry/bundler-plugin-core": "4.9.0", - "unplugin": "1.0.1" + "@sentry/rollup-plugin": "4.9.0" }, "devDependencies": { "@babel/core": "7.18.5", diff --git a/packages/vite-plugin/src/index.ts b/packages/vite-plugin/src/index.ts index 51a79602..e3326386 100644 --- a/packages/vite-plugin/src/index.ts +++ b/packages/vite-plugin/src/index.ts @@ -1,68 +1,14 @@ -import { - CodeInjection, - sentryUnpluginFactory, - Options, - createRollupInjectionHooks, - createRollupDebugIdUploadHooks, - SentrySDKBuildFlags, - createRollupBundleSizeOptimizationHooks, - createComponentNameAnnotateHooks, - Logger, -} from "@sentry/bundler-plugin-core"; -import { UnpluginOptions, VitePlugin } from "unplugin"; - -function viteInjectionPlugin(injectionCode: CodeInjection, debugIds: boolean): UnpluginOptions { - return { - name: "sentry-vite-injection-plugin", - // run `post` to avoid tripping up @rollup/plugin-commonjs when cjs is used - // as we inject an `import` statement - enforce: "post" as const, // need this so that vite runs the resolveId hook - vite: createRollupInjectionHooks(injectionCode, debugIds), - }; -} - -function viteComponentNameAnnotatePlugin( - ignoredComponents: string[], - injectIntoHtml: boolean -): UnpluginOptions { - return { - name: "sentry-vite-component-name-annotate-plugin", - enforce: "pre" as const, - vite: createComponentNameAnnotateHooks(ignoredComponents, injectIntoHtml), - }; -} - -function viteDebugIdUploadPlugin( - upload: (buildArtifacts: string[]) => Promise, - logger: Logger, - createDependencyOnBuildArtifacts: () => () => void -): UnpluginOptions { - return { - name: "sentry-vite-debug-id-upload-plugin", - vite: createRollupDebugIdUploadHooks(upload, logger, createDependencyOnBuildArtifacts), - }; -} - -function viteBundleSizeOptimizationsPlugin( - replacementValues: SentrySDKBuildFlags -): UnpluginOptions { - return { - name: "sentry-vite-bundle-size-optimizations-plugin", - vite: createRollupBundleSizeOptimizationHooks(replacementValues), - }; -} - -const sentryUnplugin = sentryUnpluginFactory({ - injectionPlugin: viteInjectionPlugin, - componentNameAnnotatePlugin: viteComponentNameAnnotatePlugin, - debugIdUploadPlugin: viteDebugIdUploadPlugin, - bundleSizeOptimizationsPlugin: viteBundleSizeOptimizationsPlugin, -}); - -export const sentryVitePlugin = (options?: Options): VitePlugin[] => { - const result = sentryUnplugin.vite(options); - // unplugin returns a single plugin instead of an array when only one plugin is created, so we normalize this here. - return Array.isArray(result) ? result : [result]; +import { SentryRollupPluginOptions } from "@sentry/rollup-plugin"; +import { _rollupPluginInternal } from "@sentry/rollup-plugin"; +import { Plugin } from "vite"; + +export const sentryVitePlugin = (options?: SentryRollupPluginOptions): Plugin[] => { + return [ + { + enforce: "pre", + ..._rollupPluginInternal(options, "vite"), + }, + ]; }; export type { Options as SentryVitePluginOptions } from "@sentry/bundler-plugin-core"; diff --git a/packages/vite-plugin/test/public-api.test.ts b/packages/vite-plugin/test/public-api.test.ts index bd0d0827..cc709292 100644 --- a/packages/vite-plugin/test/public-api.test.ts +++ b/packages/vite-plugin/test/public-api.test.ts @@ -21,15 +21,7 @@ describe("sentryVitePlugin", () => { const pluginNames = plugins.map((plugin) => plugin.name); - expect(pluginNames).toEqual( - expect.arrayContaining([ - "sentry-telemetry-plugin", - "sentry-release-management-plugin", - "sentry-vite-injection-plugin", - "sentry-vite-debug-id-upload-plugin", - "sentry-file-deletion-plugin", - ]) - ); + expect(pluginNames).toEqual(expect.arrayContaining(["sentry-vite-plugin"])); }); it("returns an array of Vite pluginswhen unplugin returns a single plugin", () => { diff --git a/yarn.lock b/yarn.lock index 5f7ab0c9..4c0e8a6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11535,13 +11535,6 @@ rollup@2.75.7: optionalDependencies: fsevents "~2.3.2" -rollup@2.77.0: - version "2.77.0" - resolved "https://registry.npmjs.org/rollup/-/rollup-2.77.0.tgz#749eaa5ac09b6baa52acc076bc46613eddfd53f4" - integrity sha512-vL8xjY4yOQEw79DvyXLijhnhh+R/O9zpF/LEgkCebZFtb6ELeN9H3/2T0r8+mp+fFTBHZ5qGpOpW2ela2zRt3g== - optionalDependencies: - fsevents "~2.3.2" - rollup@2.79.2: version "2.79.2" resolved "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz#f150e4a5db4b121a21a747d762f701e5e9f49090" From 0ab8602d59e34e888ded70be31fe332915b1582e Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 9 Feb 2026 21:35:19 +0100 Subject: [PATCH 3/5] More fixes --- packages/rollup-plugin/src/index.ts | 2 +- packages/rollup-plugin/test/public-api.test.ts | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/rollup-plugin/src/index.ts b/packages/rollup-plugin/src/index.ts index 2ab2e736..bded11f1 100644 --- a/packages/rollup-plugin/src/index.ts +++ b/packages/rollup-plugin/src/index.ts @@ -34,7 +34,7 @@ function hasExistingDebugID(code: string): boolean { } /** - * @ignore - this is the internal plugin factory function + * @ignore - this is the internal plugin factory function only used for the Vite plugin! */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function _rollupPluginInternal(userOptions: Options = {}, buildTool: string) { diff --git a/packages/rollup-plugin/test/public-api.test.ts b/packages/rollup-plugin/test/public-api.test.ts index 22190364..e470d965 100644 --- a/packages/rollup-plugin/test/public-api.test.ts +++ b/packages/rollup-plugin/test/public-api.test.ts @@ -11,16 +11,17 @@ describe("sentryRollupPlugin", () => { jest.clearAllMocks(); }); - it("returns a single rollup plugin", () => { - const [plugin] = sentryRollupPlugin({ + it("returns an array of rollup plugins (although only one)", () => { + const plugins = sentryRollupPlugin({ authToken: "test-token", org: "test-org", project: "test-project", - }); + }) as Plugin[]; - expect(Array.isArray(plugin)).not.toBe(true); + expect(Array.isArray(plugins)).toBe(true); + expect(plugins).toHaveLength(1); - expect(plugin?.name).toBe("sentry-rollup-plugin"); + expect(plugins[0]?.name).toBe("sentry-rollup-plugin"); }); }); From c7f027adb9353498f5bfdde73199dd5bd0d73923 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 11 Feb 2026 14:01:27 +0100 Subject: [PATCH 4/5] always create a release even if sourcemap upload is disabled --- packages/rollup-plugin/src/index.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/rollup-plugin/src/index.ts b/packages/rollup-plugin/src/index.ts index 4f66b94d..0f2b36f0 100644 --- a/packages/rollup-plugin/src/index.ts +++ b/packages/rollup-plugin/src/index.ts @@ -204,22 +204,22 @@ export function _rollupPluginInternal( outputOptions: { dir?: string; file?: string }, bundle: { [fileName: string]: unknown } ): Promise { - if (!sourcemapsEnabled) { - return; - } - try { await sentryBuildPluginManager.createRelease(); - if (outputOptions.dir) { - const outputDir = outputOptions.dir; - const buildArtifacts = await globFiles(outputDir); - await upload(buildArtifacts); - } else if (outputOptions.file) { - await upload([outputOptions.file]); - } else { - const buildArtifacts = Object.keys(bundle).map((asset) => path.join(path.resolve(), asset)); - await upload(buildArtifacts); + if (sourcemapsEnabled && options.sourcemaps?.disable !== "disable-upload") { + if (outputOptions.dir) { + const outputDir = outputOptions.dir; + const buildArtifacts = await globFiles(outputDir); + await upload(buildArtifacts); + } else if (outputOptions.file) { + await upload([outputOptions.file]); + } else { + const buildArtifacts = Object.keys(bundle).map((asset) => + path.join(path.resolve(), asset) + ); + await upload(buildArtifacts); + } } } finally { freeGlobalDependencyOnBuildArtifacts(); From abac461eb81d5c8764dfcdc47f3eb4ae4857cfef Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Sat, 14 Feb 2026 09:54:20 +0000 Subject: [PATCH 5/5] PR review --- packages/rollup-plugin/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rollup-plugin/src/index.ts b/packages/rollup-plugin/src/index.ts index 0f2b36f0..902051cd 100644 --- a/packages/rollup-plugin/src/index.ts +++ b/packages/rollup-plugin/src/index.ts @@ -54,7 +54,7 @@ function getRollupMajorVersion(): string | undefined { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function _rollupPluginInternal( userOptions: Options = {}, - buildTool: string, + buildTool: "rollup" | "vite", buildToolMajorVersion?: string ) { const sentryBuildPluginManager = createSentryBuildPluginManager(userOptions, {