diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/cjs-directives.config.js b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/cjs-directives.config.js new file mode 100644 index 000000000000..30a9bf23f2b1 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/cjs-directives.config.js @@ -0,0 +1,55 @@ +import * as esbuild from "esbuild"; +import { sentryEsbuildPlugin } from "@sentry/bundler-plugins/esbuild"; + +await esbuild.build({ + entryPoints: { + sloppy: "./src/sloppy-mode.cjs", + }, + bundle: true, + outdir: "./out/cjs-directives/without-plugin", + outExtension: { ".js": ".cjs" }, + minify: false, + format: "cjs", + tsconfigRaw: { compilerOptions: { alwaysStrict: false } }, +}); + +await esbuild.build({ + entryPoints: { + strict: "./src/strict-mode.cjs", + sloppy: "./src/sloppy-mode.cjs", + }, + bundle: true, + outdir: "./out/cjs-directives/static-injection", + outExtension: { ".js": ".cjs" }, + minify: false, + format: "cjs", + tsconfigRaw: { compilerOptions: { alwaysStrict: false } }, + plugins: [ + sentryEsbuildPlugin({ + telemetry: false, + release: { name: "strict-mode-release", create: false }, + sourcemaps: { disable: true }, + }), + ], +}); + +await esbuild.build({ + entryPoints: { + strict: "./src/strict-mode.cjs", + sloppy: "./src/sloppy-mode.cjs", + }, + bundle: true, + outdir: "./out/cjs-directives/debug-id-injection", + outExtension: { ".js": ".cjs" }, + minify: false, + format: "cjs", + sourcemap: true, + tsconfigRaw: { compilerOptions: { alwaysStrict: false } }, + plugins: [ + sentryEsbuildPlugin({ + telemetry: false, + release: { inject: false }, + sourcemaps: { disable: "disable-upload" }, + }), + ], +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/cjs-directives.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/cjs-directives.test.ts new file mode 100644 index 000000000000..cfb43ef05ae8 --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/cjs-directives.test.ts @@ -0,0 +1,32 @@ +import { expect } from "vitest"; +import { test } from "./utils"; + +test(import.meta.url, ({ runBundler, runFileInNode }) => { + runBundler(); + + expect(JSON.parse(runFileInNode("without-plugin/sloppy.cjs"))).toEqual({ + sloppyModePreserved: true, + releaseInjected: false, + debugIdInjected: false, + }); + expect(JSON.parse(runFileInNode("static-injection/strict.cjs"))).toEqual({ + strictModePreserved: true, + releaseInjected: true, + debugIdInjected: false, + }); + expect(JSON.parse(runFileInNode("static-injection/sloppy.cjs"))).toEqual({ + sloppyModePreserved: true, + releaseInjected: true, + debugIdInjected: false, + }); + expect(JSON.parse(runFileInNode("debug-id-injection/strict.cjs"))).toEqual({ + strictModePreserved: true, + releaseInjected: false, + debugIdInjected: true, + }); + expect(JSON.parse(runFileInNode("debug-id-injection/sloppy.cjs"))).toEqual({ + sloppyModePreserved: true, + releaseInjected: false, + debugIdInjected: true, + }); +}); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/src/sloppy-mode.cjs b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/src/sloppy-mode.cjs new file mode 100644 index 000000000000..ce708c7a27de --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/src/sloppy-mode.cjs @@ -0,0 +1,12 @@ +globalThis.sloppyModePreserved = + (function () { + return this; + })() === globalThis; + +console.log( + JSON.stringify({ + sloppyModePreserved: globalThis.sloppyModePreserved, + releaseInjected: globalThis.SENTRY_RELEASE?.id === "strict-mode-release", + debugIdInjected: Object.keys(globalThis._sentryDebugIds || {}).length === 1, + }) +); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/src/strict-mode.cjs b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/src/strict-mode.cjs new file mode 100644 index 000000000000..fa2e93dba59b --- /dev/null +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/esbuild/src/strict-mode.cjs @@ -0,0 +1,14 @@ +"use strict"; + +globalThis.strictModePreserved = + (function () { + return this; + })() === undefined; + +console.log( + JSON.stringify({ + strictModePreserved: globalThis.strictModePreserved, + releaseInjected: globalThis.SENTRY_RELEASE?.id === "strict-mode-release", + debugIdInjected: Object.keys(globalThis._sentryDebugIds || {}).length === 1, + }) +); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/after-upload-deletion.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/after-upload-deletion.test.ts index a4bcd8a767cb..b82433103407 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/after-upload-deletion.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/after-upload-deletion.test.ts @@ -5,8 +5,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { runBundler(); expect(readOutputFiles()).toMatchInlineSnapshot(` { - "basic.js": "!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}(); - /******/ (() => { // webpackBootstrap + "basic.js": "/******/ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();(() => { // webpackBootstrap /******/ "use strict"; // eslint-disable-next-line no-console console.log("hello world"); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/application-key.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/application-key.test.ts index 76b6d9adb0e8..1b7b4b354b7c 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/application-key.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/application-key.test.ts @@ -5,8 +5,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { runBundler(); expect(readOutputFiles()).toMatchInlineSnapshot(` { - "basic.js": "!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=function(e){for(var n=1;n { // webpackBootstrap + "basic.js": "/******/ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=function(e){for(var n=1;n { // webpackBootstrap /******/ "use strict"; // eslint-disable-next-line no-console console.log("hello world"); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-cjs.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-cjs.test.ts index 8095d1ba0754..c0a5365c61fd 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-cjs.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-cjs.test.ts @@ -5,8 +5,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { runBundler(); expect(readOutputFiles()).toMatchInlineSnapshot(` { - "basic.js": "!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}(); - /******/ (() => { // webpackBootstrap + "basic.js": "/******/ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();(() => { // webpackBootstrap /******/ "use strict"; // eslint-disable-next-line no-console console.log("hello world"); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-release-disabled.test.ts index c7fc5905957a..0f0ac8d0feff 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-release-disabled.test.ts @@ -5,8 +5,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { runBundler(); expect(readOutputFiles()).toMatchInlineSnapshot(` { - "basic.js": "!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]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}(); - /******/ (() => { // webpackBootstrap + "basic.js": "/******/ !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]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();(() => { // webpackBootstrap /******/ "use strict"; // eslint-disable-next-line no-console console.log("hello world"); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-sourcemaps.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-sourcemaps.test.ts index fbcababfd029..575d00dc1ee9 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-sourcemaps.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic-sourcemaps.test.ts @@ -5,8 +5,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { runBundler(); expect(readOutputFiles()).toMatchInlineSnapshot(` { - "basic.js": "!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}(); - /******/ (() => { // webpackBootstrap + "basic.js": "/******/ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();(() => { // webpackBootstrap /******/ "use strict"; // eslint-disable-next-line no-console console.log("hello world"); @@ -14,7 +13,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { /******/ })() ; //# sourceMappingURL=basic.js.map", - "basic.js.map": "{"version":3,"file":"basic.js","mappings":";;;AAAA;AACA","sources":["webpack://webpack5-integration-tests/./src/basic.js"],"sourcesContent":["// eslint-disable-next-line no-console\\nconsole.log(\\"hello world\\");\\n"],"names":[],"sourceRoot":""}", + "basic.js.map": "{"version":3,"file":"basic.js","mappings":";;AAAA;AACA","sources":["webpack://webpack5-integration-tests/./src/basic.js"],"sourcesContent":["// eslint-disable-next-line no-console\\nconsole.log(\\"hello world\\");\\n"],"names":[],"sourceRoot":""}", "sentry-cli-mock.json": "["release","create","CURRENT_SHA","--project","fake-project"], ["release","set-commits","CURRENT_SHA","--auto"], ["release","finalize","CURRENT_SHA"], diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic.test.ts index 8095d1ba0754..c0a5365c61fd 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/basic.test.ts @@ -5,8 +5,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { runBundler(); expect(readOutputFiles()).toMatchInlineSnapshot(` { - "basic.js": "!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}(); - /******/ (() => { // webpackBootstrap + "basic.js": "/******/ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();(() => { // webpackBootstrap /******/ "use strict"; // eslint-disable-next-line no-console console.log("hello world"); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/build-info.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/build-info.test.ts index 4742786c9b76..1f5107e559ef 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/build-info.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/build-info.test.ts @@ -5,8 +5,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { runBundler(); expect(readOutputFiles()).toMatchInlineSnapshot(` { - "basic.js": "!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"build-information-injection-test"};e.SENTRY_BUILD_INFO={"deps":["@babel/preset-react","@sentry/bundler-plugins","babel-loader","webpack","webpack-cli"],"depsVersions":{"webpack":5},"nodeVersion":"NODE_VERSION"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}(); - /******/ (() => { // webpackBootstrap + "basic.js": "/******/ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"build-information-injection-test"};e.SENTRY_BUILD_INFO={"deps":["@babel/preset-react","@sentry/bundler-plugins","babel-loader","webpack","webpack-cli"],"depsVersions":{"webpack":5},"nodeVersion":"NODE_VERSION"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();(() => { // webpackBootstrap /******/ "use strict"; // eslint-disable-next-line no-console console.log("hello world"); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/bundle-size-optimizations.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/bundle-size-optimizations.test.ts index 7313767b10ad..dacf1bc67701 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/bundle-size-optimizations.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/bundle-size-optimizations.test.ts @@ -5,8 +5,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { runBundler(); expect(readOutputFiles()).toMatchInlineSnapshot(` { - "bundle.js": "!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}(); - /******/ (() => { // webpackBootstrap + "bundle.js": "/******/ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();(() => { // webpackBootstrap /******/ "use strict"; console.log( JSON.stringify({ diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/component-annotation-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/component-annotation-disabled.test.ts index 39a5b67c4108..d3a532ff0ba3 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/component-annotation-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/component-annotation-disabled.test.ts @@ -5,8 +5,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { runBundler(); expect(readOutputFiles()).toMatchInlineSnapshot(` { - "app.js": "!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}(); - /******/ (() => { // webpackBootstrap + "app.js": "/******/ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();(() => { // webpackBootstrap /******/ "use strict"; // UNUSED EXPORTS: default diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/component-annotation-next.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/component-annotation-next.test.ts index 28d9443ae50d..ae93c38a8bc3 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/component-annotation-next.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/component-annotation-next.test.ts @@ -10,8 +10,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, ctx }) => { runBundler(); expect(readOutputFiles()).toMatchInlineSnapshot(` { - "app.js": "!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}(); - /******/ (() => { // webpackBootstrap + "app.js": "/******/ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();(() => { // webpackBootstrap /******/ "use strict"; // UNUSED EXPORTS: default diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/component-annotation.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/component-annotation.test.ts index 1c5f9f8cc6ac..750c7fbdaebe 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/component-annotation.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/component-annotation.test.ts @@ -10,8 +10,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, ctx }) => { runBundler(); expect(readOutputFiles()).toMatchInlineSnapshot(` { - "app.js": "!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}(); - /******/ (() => { // webpackBootstrap + "app.js": "/******/ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();(() => { // webpackBootstrap /******/ "use strict"; // UNUSED EXPORTS: default diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/debugids-already-injected.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/debugids-already-injected.test.ts index 3509e3431c86..cf466e381a46 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/debugids-already-injected.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/debugids-already-injected.test.ts @@ -9,8 +9,7 @@ test(import.meta.url, ({ runBundler, createTempDir }) => { const files = readAllFiles(tempDir); expect(files).toMatchInlineSnapshot(` { - "33730b8e-5b8d-4795-94b2-666cea28fce6-0.js": "!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}(); - /******/ (() => { // webpackBootstrap + "33730b8e-5b8d-4795-94b2-666cea28fce6-0.js": "/******/ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();(() => { // webpackBootstrap /******/ "use strict"; // eslint-disable-next-line no-console console.log("hello world"); @@ -19,7 +18,7 @@ test(import.meta.url, ({ runBundler, createTempDir }) => { ; //# sourceMappingURL=basic.js.map //# debugId=00000000-0000-0000-0000-000000000000", - "33730b8e-5b8d-4795-94b2-666cea28fce6-0.js.map": "{"version":3,"file":"basic.js","mappings":";;;AAAA;AACA","sources":["webpack5-integration-tests/./src/basic.js"],"sourcesContent":["// eslint-disable-next-line no-console\\nconsole.log(\\"hello world\\");\\n"],"names":[],"sourceRoot":"","debug_id":"33730b8e-5b8d-4795-94b2-666cea28fce6","debugId":"33730b8e-5b8d-4795-94b2-666cea28fce6"}", + "33730b8e-5b8d-4795-94b2-666cea28fce6-0.js.map": "{"version":3,"file":"basic.js","mappings":";;AAAA;AACA","sources":["webpack5-integration-tests/./src/basic.js"],"sourcesContent":["// eslint-disable-next-line no-console\\nconsole.log(\\"hello world\\");\\n"],"names":[],"sourceRoot":"","debug_id":"33730b8e-5b8d-4795-94b2-666cea28fce6","debugId":"33730b8e-5b8d-4795-94b2-666cea28fce6"}", } `); }); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/module-metadata.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/module-metadata.test.ts index 2672e9edf824..a6830a1f55b4 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/module-metadata.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/module-metadata.test.ts @@ -5,8 +5,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { runBundler(); expect(readOutputFiles()).toMatchInlineSnapshot(` { - "basic.js": "!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=function(e){for(var n=1;n { // webpackBootstrap + "basic.js": "/******/ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=function(e){for(var n=1;n { // webpackBootstrap /******/ "use strict"; // eslint-disable-next-line no-console console.log("hello world"); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/multiple-entry-points.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/multiple-entry-points.test.ts index 5f054a9ba2da..2683bd1001e3 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/multiple-entry-points.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/multiple-entry-points.test.ts @@ -5,8 +5,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { runBundler(); expect(readOutputFiles()).toMatchInlineSnapshot(` { - "entry1.js": "!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]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}(); - /******/ (() => { // webpackBootstrap + "entry1.js": "/******/ !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]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();(() => { // webpackBootstrap /******/ "use strict"; ;// ./src/common.js @@ -21,8 +20,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { /******/ })() ;", - "entry2.js": "!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]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}(); - /******/ (() => { // webpackBootstrap + "entry2.js": "/******/ !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]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();(() => { // webpackBootstrap /******/ "use strict"; ;// ./src/common.js diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/release-disabled.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/release-disabled.test.ts index 5288fc48bcba..9982a5b85bf9 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/release-disabled.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/release-disabled.test.ts @@ -5,8 +5,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles }) => { runBundler(); expect(readOutputFiles()).toMatchInlineSnapshot(` { - "basic.js": "!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}(); - /******/ (() => { // webpackBootstrap + "basic.js": "/******/ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();(() => { // webpackBootstrap /******/ "use strict"; // eslint-disable-next-line no-console console.log("hello world"); diff --git a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/telemetry.test.ts b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/telemetry.test.ts index 0fa7cf672d64..e8b0d915de0e 100644 --- a/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/telemetry.test.ts +++ b/dev-packages/bundler-plugin-integration-tests/fixtures/webpack5/telemetry.test.ts @@ -5,8 +5,7 @@ test(import.meta.url, ({ runBundler, readOutputFiles, runFileInNode }) => { runBundler(); expect(readOutputFiles()).toMatchInlineSnapshot(` { - "basic.js": "!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}(); - /******/ (() => { // webpackBootstrap + "basic.js": "/******/ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{};e.SENTRY_RELEASE={id:"CURRENT_SHA"};var n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="00000000-0000-0000-0000-000000000000",e._sentryDebugIdIdentifier="sentry-dbid-00000000-0000-0000-0000-000000000000");}catch(e){}}();(() => { // webpackBootstrap /******/ "use strict"; // eslint-disable-next-line no-console console.log("hello world"); diff --git a/packages/bundler-plugins/src/core/get-code-injection-position.ts b/packages/bundler-plugins/src/core/get-code-injection-position.ts new file mode 100644 index 000000000000..a7299685a4db --- /dev/null +++ b/packages/bundler-plugins/src/core/get-code-injection-position.ts @@ -0,0 +1,104 @@ +function isLineTerminator(character: string | undefined): boolean { + return character === '\n' || character === '\r' || character === '\u2028' || character === '\u2029'; +} + +function skipTrivia(code: string, start: number): { end: number; hasLineBreak: boolean } { + let position = start; + let hasLineBreak = false; + + while (position < code.length) { + const character = code[position]; + + if (/\s/.test(character || '')) { + hasLineBreak ||= isLineTerminator(character); + position++; + } else if (code.startsWith('//', position) || (position === 0 && code.startsWith('#!', position))) { + let lineEnd = position + 2; + while (lineEnd < code.length && !isLineTerminator(code[lineEnd])) { + lineEnd++; + } + if (lineEnd === code.length) { + return { end: code.length, hasLineBreak }; + } + position = lineEnd + 1; + hasLineBreak = true; + } else if (code.startsWith('/*', position)) { + const commentEnd = code.indexOf('*/', position + 2); + if (commentEnd === -1) { + return { end: code.length, hasLineBreak }; + } + const comment = code.slice(position, commentEnd + 2); + hasLineBreak ||= /[\n\r\u2028\u2029]/.test(comment); + position = commentEnd + 2; + } else { + break; + } + } + + return { end: position, hasLineBreak }; +} + +function findStringLiteralEnd(code: string, start: number): number | undefined { + const quote = code[start]; + if (quote !== '"' && quote !== "'") { + return undefined; + } + + for (let position = start + 1; position < code.length; position++) { + const character = code[position]; + if (character === '\\') { + position += code[position + 1] === '\r' && code[position + 2] === '\n' ? 2 : 1; + } else if (character === quote) { + return position + 1; + } else if (isLineTerminator(character)) { + return undefined; + } + } + + return undefined; +} + +function startsWithBinaryOperatorKeyword(remainder: string, keyword: string): boolean { + return remainder.startsWith(keyword) && !/^[$_\\\u200C\u200D\p{ID_Continue}]/u.test(remainder.slice(keyword.length)); +} + +function canContinueStringExpression(code: string, position: number): boolean { + const remainder = code.slice(position, position + 'instanceof'.length + 1); + if (/^(?:\+\+|--|!(?!=))/.test(remainder)) { + return false; + } + + return ( + /^!={1,2}/.test(remainder) || + /^[([.`+\-*/%<>=&|^?,:]/.test(remainder) || + ['in', 'instanceof'].some(keyword => startsWithBinaryOperatorKeyword(remainder, keyword)) + ); +} + +export function getCodeInjectionPosition(code: string): number { + let position = skipTrivia(code, 0).end; + let prologueEnd = position; + + while (position < code.length) { + const stringEnd = findStringLiteralEnd(code, position); + if (stringEnd === undefined) { + break; + } + + const trailingTrivia = skipTrivia(code, stringEnd); + if (code[trailingTrivia.end] === ';') { + position = skipTrivia(code, trailingTrivia.end + 1).end; + } else if ( + trailingTrivia.end === code.length || + (trailingTrivia.hasLineBreak && !canContinueStringExpression(code, trailingTrivia.end)) + ) { + position = trailingTrivia.end; + } else { + break; + } + + prologueEnd = position; + } + + return prologueEnd; +} diff --git a/packages/bundler-plugins/src/core/index.ts b/packages/bundler-plugins/src/core/index.ts index 0386bf3e86ac..9c5008bf8587 100644 --- a/packages/bundler-plugins/src/core/index.ts +++ b/packages/bundler-plugins/src/core/index.ts @@ -30,12 +30,6 @@ function loadBabelAnnotationRuntime(): Promise { return babelAnnotationRuntimePromise; } -// 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. -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])*(?:"[^"]*";|'[^']*';)?/; - /** * Checks if a file is a JavaScript file based on its extension. * Handles query strings and hashes in the filename. @@ -74,6 +68,7 @@ export function shouldSkipCodeInjection(code: string, facadeModuleId: string | n } export { globFiles } from './glob'; +export { getCodeInjectionPosition } from './get-code-injection-position'; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function createComponentNameAnnotateHooks(ignoredComponents: string[], injectIntoHtml: boolean) { diff --git a/packages/bundler-plugins/src/esbuild/index.ts b/packages/bundler-plugins/src/esbuild/index.ts index f3bc0fc8c01e..fc17904624be 100644 --- a/packages/bundler-plugins/src/esbuild/index.ts +++ b/packages/bundler-plugins/src/esbuild/index.ts @@ -161,6 +161,8 @@ export function sentryEsbuildPlugin(userOptions: Options = {}): any { if (!staticInjectionCode.isEmpty()) { const virtualInjectionFilePath = path.resolve('_sentry-injection-stub'); initialOptions.inject = initialOptions.inject || []; + // esbuild emits injected files after an entry's directive prologue. A banner would precede + // "use strict" and turn it into an ordinary string expression. initialOptions.inject.push(virtualInjectionFilePath); onResolve({ filter: /_sentry-injection-stub/ }, args => { diff --git a/packages/bundler-plugins/src/rollup/index.ts b/packages/bundler-plugins/src/rollup/index.ts index 1bb1def0b80d..9b41176701d9 100644 --- a/packages/bundler-plugins/src/rollup/index.ts +++ b/packages/bundler-plugins/src/rollup/index.ts @@ -7,13 +7,13 @@ import { shouldSkipCodeInjection, getDebugIdSnippet, stringToUUID, - COMMENT_USE_STRICT_REGEX, createDebugIdUploadFunction, globFiles, createComponentNameAnnotateHooks, replaceBooleanFlagsInCode, CodeInjection, stampDebugId, + getCodeInjectionPosition, } from '../core'; import type { ComponentAnnotationTransformMeta, @@ -267,17 +267,9 @@ export function _rollupPluginInternal( } 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, 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()); - } + const injectionPosition = getCodeInjectionPosition(code); + const codeToInject = injectionPosition === code.length ? `\n${injectCode.code()}` : injectCode.code(); + ms.appendLeft(injectionPosition, codeToInject); // Rolldown can pass a native MagicString instance in meta.magicString // https://rolldown.rs/in-depth/native-magic-string#usage-examples @@ -288,7 +280,7 @@ export function _rollupPluginInternal( return { code: ms.toString(), - map: ms.generateMap({ file: chunk.fileName, hires: 'boundary' as unknown as undefined }), + map: ms.generateMap({ file: chunk.fileName, hires: 'boundary' }), }; } diff --git a/packages/bundler-plugins/src/webpack/index.ts b/packages/bundler-plugins/src/webpack/index.ts index 0597b5794c08..2adcd4ddc0b6 100644 --- a/packages/bundler-plugins/src/webpack/index.ts +++ b/packages/bundler-plugins/src/webpack/index.ts @@ -1,3 +1,5 @@ +/* oxlint-disable max-lines */ + import type { Options } from '../core/index'; import { createSentryBuildPluginManager, @@ -8,6 +10,7 @@ import { CodeInjection, getDebugIdSnippet, createDebugIdUploadFunction, + getCodeInjectionPosition, isJsFile, stampDebugId, } from '../core/index'; @@ -35,15 +38,6 @@ try { COMPONENT_ANNOTATION_LOADER = path.resolve(dirname, 'component-annotation-transform.js'); } -interface BannerPluginCallbackArg { - chunk?: { - hash?: string; - contentHash?: { - javascript?: string; - }; - }; -} - type PluginClass = { // eslint-disable-next-line @typescript-eslint/no-explicit-any new (options: any): unknown; @@ -57,6 +51,12 @@ type WebpackRawSource = { new (source: string): WebpackSource; }; +type WebpackReplaceSource = WebpackSource & { + insert: (position: number, value: string) => void; +}; + +const WEBPACK_JAVASCRIPT_ASSET_REGEX = /\.(?:js|ts|jsx|tsx|mjs|cjs|mts|cts)(?:\?[^?]*)?(?:#[^#]*)?$/; + type WebpackAsset = { name: string; source: WebpackSource; @@ -76,6 +76,9 @@ type WebpackCompiler = { }; }; hooks: { + compilation: { + tap: (name: string, callback: (compilation: WebpackCompilation) => void) => void; + }; thisCompilation: { tap: (name: string, callback: (compilation: WebpackCompilation) => void) => void; }; @@ -87,18 +90,24 @@ type WebpackCompiler = { }; }; webpack?: { - BannerPlugin?: PluginClass; DefinePlugin?: PluginClass; Compilation?: { + PROCESS_ASSETS_STAGE_ADDITIONS?: number; PROCESS_ASSETS_STAGE_DEV_TOOLING?: number; }; sources?: { RawSource?: WebpackRawSource; + ReplaceSource?: new (source: WebpackSource) => WebpackReplaceSource; }; }; }; type WebpackCompilation = { + chunks: Iterable<{ + files: Iterable; + hash?: string; + contentHash?: { javascript?: string }; + }>; outputOptions: { path?: string; }; @@ -108,7 +117,10 @@ type WebpackCompilation = { updateAsset: (name: string, source: WebpackSource) => void; hooks: { processAssets: { - tap: (options: { name: string; stage: number }, callback: () => void) => void; + tap: ( + options: { name: string; stage: number }, + callback: (assets: Record) => void, + ) => void; }; }; }; @@ -216,39 +228,72 @@ function createSentryWebpackPlugin(userOptions: SentryWebpackPluginOptions = {}) const transformReplace = Object.keys(replacementValues).length > 0; + function addCodeInjection(compiler: WebpackCompiler): void { + if (staticInjectionCode.isEmpty() && !sourcemapsEnabled) { + return; + } + + const ReplaceSource = compiler.webpack?.sources?.ReplaceSource; + const processAssetsStage = compiler.webpack?.Compilation?.PROCESS_ASSETS_STAGE_ADDITIONS; + + if (!ReplaceSource || processAssetsStage === undefined) { + logger.warn( + 'Webpack sources are not available. Skipping code injection. This usually means webpack is not properly configured.', + ); + return; + } + + compiler.hooks.compilation.tap('sentry-webpack-plugin-injection', compilation => { + compilation.hooks.processAssets.tap( + { + name: 'sentry-webpack-plugin-injection', + stage: processAssetsStage, + }, + assets => { + const injectedAssets = new Set(); + + for (const chunk of compilation.chunks) { + for (const assetName of chunk.files) { + if (injectedAssets.has(assetName) || !WEBPACK_JAVASCRIPT_ASSET_REGEX.test(assetName)) { + continue; + } + + const source = assets[assetName]; + if (!source) { + continue; + } + injectedAssets.add(assetName); + + const sourceContents = source.source(); + const code = typeof sourceContents === 'string' ? sourceContents : Buffer.from(sourceContents).toString(); + const codeToInject = staticInjectionCode.clone(); + if (sourcemapsEnabled) { + const hash = chunk.contentHash?.javascript ?? chunk.hash; + codeToInject.append(getDebugIdSnippet(hash ? stringToUUID(hash) : randomUUID())); + } + + const injectionPosition = getCodeInjectionPosition(code); + const injection = injectionPosition === code.length ? `\n${codeToInject.code()}` : codeToInject.code(); + const updatedSource = new ReplaceSource(source); + updatedSource.insert(injectionPosition, injection); + compilation.updateAsset(assetName, updatedSource); + } + } + }, + ); + }); + } + return { apply(compiler: WebpackCompiler) { void sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal().catch(() => { // Telemetry failures are acceptable }); - const { BannerPlugin, DefinePlugin } = compiler.webpack ?? {}; + const { DefinePlugin } = compiler.webpack ?? {}; - // Add BannerPlugin for code injection (release, metadata, debug IDs) - if (!staticInjectionCode.isEmpty() || sourcemapsEnabled) { - if (!BannerPlugin) { - logger.warn( - 'BannerPlugin is not available. Skipping code injection. This usually means webpack is not properly configured.', - ); - } else { - compiler.options.plugins = compiler.options.plugins || []; - compiler.options.plugins.push( - new BannerPlugin({ - raw: true, - include: /\.(js|ts|jsx|tsx|mjs|cjs)(\?[^?]*)?(#[^#]*)?$/, - banner: (arg?: BannerPluginCallbackArg) => { - const codeToInject = staticInjectionCode.clone(); - if (sourcemapsEnabled) { - const hash = arg?.chunk?.contentHash?.javascript ?? arg?.chunk?.hash; - const debugId = hash ? stringToUUID(hash) : randomUUID(); - codeToInject.append(getDebugIdSnippet(debugId)); - } - return codeToInject.code(); - }, - }), - ); - } - } + // BannerPlugin prepends executable code before directive prologues. + addCodeInjection(compiler); // The upload routine (which stamps debug IDs into temp copies of the artifacts) is skipped // with `disable-upload`, so the emitted artifacts get stamped in the asset pipeline instead. diff --git a/packages/bundler-plugins/test/core/get-code-injection-position.test.ts b/packages/bundler-plugins/test/core/get-code-injection-position.test.ts new file mode 100644 index 000000000000..91ea92211d93 --- /dev/null +++ b/packages/bundler-plugins/test/core/get-code-injection-position.test.ts @@ -0,0 +1,36 @@ +import { getCodeInjectionPosition } from '../../src/core/get-code-injection-position'; +import { describe, expect, it } from 'vitest'; + +describe('getCodeInjectionPosition', () => { + it.each([ + [ + 'multiple directives and a block comment', + `/* license */\n"use client";\n'use strict'\nglobalThis.appStarted = true;`, + `/* license */\n"use client";\n'use strict'\n`, + ], + ['a semicolonless directive before a unary IIFE', '"use strict"\n!function () {}();', '"use strict"\n'], + ['a CRLF line comment', '// license\r\n"use strict";\r\nstartApp();', '// license\r\n"use strict";\r\n'], + ['a CR-only line comment', '// license\r"use strict"\rstartApp();', '// license\r"use strict"\r'], + ['a Unicode line separator', '"use strict"\u2028startApp();', '"use strict"\u2028'], + ['a Unicode paragraph separator', '"use strict"\u2029startApp();', '"use strict"\u2029'], + ['a hashbang', '#!/usr/bin/env node\n"use strict";\nstartApp();', '#!/usr/bin/env node\n"use strict";\n'], + ['an escaped string directive', '"use\\x20strict";\nstartApp();', '"use\\x20strict";\n'], + [ + 'an escaped CRLF in a directive string', + '"not strict\\\r\n";\n"use strict";\nstartApp();', + '"not strict\\\r\n";\n"use strict";\n', + ], + ['an unterminated string', '"use strict', ''], + ['an unterminated block comment', '/* license', '/* license'], + ['leading trivia without directives', '/* license */\nstartApp();', '/* license */\n'], + ['a prefix increment statement', '"use strict"\n++value;', '"use strict"\n'], + ['a prefix decrement statement', '"use strict"\n--value;', '"use strict"\n'], + ['an inequality continuation', '"not a directive"\n!= expectedValue;', ''], + ['an addition continuation', '"not a directive"\n+ otherValue;', ''], + ['an identifier prefixed with in', '"use strict"\nin$foo: ;', '"use strict"\n'], + ['a Unicode identifier prefixed with instanceof', '"use strict"\ninstanceofπ: ;', '"use strict"\n'], + ['an escaped identifier prefixed with in', '"use strict"\nin\\u0066oo: ;', '"use strict"\n'], + ])('returns the injection position for %s', (_description, code, expectedPrefix) => { + expect(code.slice(0, getCodeInjectionPosition(code))).toBe(expectedPrefix); + }); +}); diff --git a/packages/bundler-plugins/test/rollup/public-api.test.ts b/packages/bundler-plugins/test/rollup/public-api.test.ts index 06b8220dee2c..2fec55901df9 100644 --- a/packages/bundler-plugins/test/rollup/public-api.test.ts +++ b/packages/bundler-plugins/test/rollup/public-api.test.ts @@ -1,6 +1,7 @@ import { _rollupPluginInternal, sentryRollupPlugin } from '../../src/rollup'; import { createComponentNameAnnotateHooks } from '../../src/core'; import type { Plugin, SourceMap } from 'rollup'; +import { runInNewContext } from 'node:vm'; import { describe, it, expect, test, beforeEach, vi } from 'vitest'; const { @@ -184,11 +185,61 @@ describe('Hooks', () => { 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");" + ""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('preserves source mappings when injecting after a directive prologue', () => { + const code = '"use strict";\nglobalThis.applicationStarted = true;'; + const result = renderChunk(code, { fileName: 'bundle.js' }); + + expect(result).not.toBeNull(); + expect(JSON.parse(result?.map.toString() ?? '')).toEqual({ + version: 3, + file: 'bundle.js', + sources: ['bundle.js'], + names: [], + mappings: 'AAAA,CAAC,GAAG,CAAC,MAAM,CAAC;qYACZ,UAAU,CAAC,kBAAkB,CAAC,CAAC,CAAC,IAAI', + }); + }); + + it.each([ + ['when the directive has no semicolon', '"use strict"\n'], + ['when another directive precedes it', '"use client";\n"use strict";\n'], + ['after an escaped CRLF in an earlier directive', '"not strict\\\r\n";\n"use strict";\n'], + ['before an identifier prefixed with an operator keyword', '"use strict"\nin$foo: ;\n'], + ])('preserves strict mode %s', (_description, codePrefix) => { + const code = `${codePrefix}globalThis.strictModePreserved = (function () { return this; })() === undefined;`; + const result = renderChunk(code, { fileName: 'bundle.js' }); + const context: { strictModePreserved?: boolean; _sentryDebugIds?: Record } = {}; + + expect(result).not.toBeNull(); + runInNewContext(result?.code ?? '', context); + + expect(context.strictModePreserved).toBe(true); + expect(Object.keys(context._sentryDebugIds ?? {})).toHaveLength(1); + }); + + it.each([ + ['a semicolonless directive', '"use strict"'], + ['trailing whitespace', '"use strict" '], + ['a trailing block comment', '"use strict"/* trailing */'], + ['a trailing line comment', '"use strict" // trailing'], + ])('preserves a directive at EOF with %s', (_description, code) => { + const result = renderChunk(code, { fileName: 'bundle.js' }); + const context: { strictModePreserved?: boolean; _sentryDebugIds?: Record } = {}; + + expect(result).not.toBeNull(); + runInNewContext( + `${result?.code ?? ''}\nglobalThis.strictModePreserved = (function () { return this; })() === undefined;`, + context, + ); + + expect(context.strictModePreserved).toBe(true); + expect(Object.keys(context._sentryDebugIds ?? {})).toHaveLength(1); + }); + it.each([['bundle.js'], ['bundle.mjs'], ['bundle.cjs'], ['bundle.js?foo=bar'], ['bundle.js#hash']])( "should process file '%s'", fileName => { diff --git a/packages/bundler-plugins/test/vite/public-api.test.ts b/packages/bundler-plugins/test/vite/public-api.test.ts index 6f8dd9f84260..cade4da1c67a 100644 --- a/packages/bundler-plugins/test/vite/public-api.test.ts +++ b/packages/bundler-plugins/test/vite/public-api.test.ts @@ -1,4 +1,6 @@ import { sentryVitePlugin } from '../../src/vite'; +import type { Plugin, SourceMap } from 'rollup'; +import { runInNewContext } from 'node:vm'; import { describe, it, expect, test, beforeEach, vi } from 'vitest'; test('Vite plugin should exist', () => { @@ -37,4 +39,27 @@ describe('sentryVitePlugin', () => { expect(plugins.length).toBeGreaterThanOrEqual(1); expect(plugins[0]).toHaveProperty('name'); }); + + it.each([ + ['when the directive has no semicolon', '"use strict"\n'], + ['when another directive precedes it', '"use client";\n"use strict";\n'], + ['after an escaped CRLF in an earlier directive', '"not strict\\\r\n";\n"use strict";\n'], + ['before an identifier prefixed with an operator keyword', '"use strict"\nin$foo: ;\n'], + ])('preserves strict mode %s', (_description, codePrefix) => { + const [plugin] = sentryVitePlugin({ release: { inject: false }, telemetry: false }) as Array; + const renderChunk = plugin?.renderChunk as ( + code: string, + chunkInfo: { fileName: string }, + ) => { code: string; map: SourceMap } | null; + const code = `${codePrefix}globalThis.strictModePreserved = (function () { return this; })() === undefined;`; + + const result = renderChunk(code, { fileName: 'bundle.js' }); + const context: { strictModePreserved?: boolean; _sentryDebugIds?: Record } = {}; + + expect(result).not.toBeNull(); + runInNewContext(result?.code ?? '', context); + + expect(context.strictModePreserved).toBe(true); + expect(Object.keys(context._sentryDebugIds ?? {})).toHaveLength(1); + }); }); diff --git a/packages/bundler-plugins/test/webpack/child-compiler.test.ts b/packages/bundler-plugins/test/webpack/child-compiler.test.ts new file mode 100644 index 000000000000..04edf0b82b9f --- /dev/null +++ b/packages/bundler-plugins/test/webpack/child-compiler.test.ts @@ -0,0 +1,78 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { EntryPlugin, webpack } from 'webpack'; +import type { Compiler, Configuration, Stats } from 'webpack'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { sentryWebpackPlugin } from '../../src/webpack/index'; + +function build(config: Configuration): Promise { + return new Promise((resolve, reject) => { + webpack(config, (err, stats) => { + if (err) { + return reject(err); + } + if (!stats || stats.hasErrors()) { + return reject(new Error(stats?.toString() ?? 'no stats')); + } + resolve(stats); + }); + }); +} + +function createChildCompilerPlugin(context: string): { apply(compiler: Compiler): void } { + return { + apply(compiler) { + compiler.hooks.make.tapAsync('test-child-compiler', (compilation, callback) => { + const childCompiler = compilation.createChildCompiler('test-child-compiler', { filename: 'worker.js' }, [ + new EntryPlugin(context, './worker.js', { name: 'worker' }), + ]); + + childCompiler.runAsChild(error => { + if (error) { + callback(error); + } else { + callback(); + } + }); + }); + }, + }; +} + +describe('child compiler injection', () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-webpack-child-compiler-')); + fs.writeFileSync(path.join(tmpDir, 'entry.js'), 'globalThis.applicationLoaded = true;\n'); + fs.writeFileSync(path.join(tmpDir, 'worker.js'), 'globalThis.workerLoaded = true;\n'); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('injects release information into child-compiler bundles', async () => { + const outDir = path.join(tmpDir, 'dist'); + + await build({ + mode: 'production', + context: tmpDir, + entry: './entry.js', + output: { path: outDir, filename: 'bundle.js' }, + plugins: [ + sentryWebpackPlugin({ + release: { name: 'child-compiler-release' }, + sourcemaps: { disable: true }, + telemetry: false, + }), + createChildCompilerPlugin(tmpDir), + ], + }); + + const workerBundle = fs.readFileSync(path.join(outDir, 'worker.js'), 'utf8'); + + expect(workerBundle).toContain('child-compiler-release'); + }); +}); diff --git a/packages/bundler-plugins/test/webpack/injection.test.ts b/packages/bundler-plugins/test/webpack/injection.test.ts new file mode 100644 index 000000000000..baf74dc6b7db --- /dev/null +++ b/packages/bundler-plugins/test/webpack/injection.test.ts @@ -0,0 +1,149 @@ +import webpack from 'webpack'; +import MagicString from 'magic-string'; +import { runInNewContext } from 'node:vm'; +import { describe, expect, it } from 'vitest'; +import { sentryWebpackPlugin } from '../../src/webpack'; + +interface TestChunk { + files: string[]; + hash?: string; +} + +function runWebpackSourceInjection( + assetName: string, + source: webpack.sources.Source, + chunks: TestChunk[] = [{ files: [assetName] }], +): webpack.sources.Source { + const webpackPlugin = sentryWebpackPlugin({ + release: { inject: false }, + telemetry: false, + }); + let compilationCallback!: (compilation: unknown) => void; + let processAssets!: (assets: Record) => void; + const assets = { [assetName]: source }; + const compiler = { + options: { plugins: [] as unknown[] }, + webpack: { + Compilation: { PROCESS_ASSETS_STAGE_ADDITIONS: -100 }, + sources: { ReplaceSource: webpack.sources.ReplaceSource }, + }, + hooks: { + compilation: { + tap: (_name: string, callback: (compilation: unknown) => void) => { + compilationCallback = callback; + }, + }, + afterEmit: { tapAsync: () => undefined }, + done: { tap: () => undefined }, + }, + }; + const compilation = { + chunks, + compiler: {}, + hooks: { + processAssets: { + tap: (_options: unknown, callback: (assets: Record) => void) => { + processAssets = callback; + }, + }, + }, + updateAsset: (name: string, updatedSource: webpack.sources.Source) => { + assets[name] = updatedSource; + }, + }; + + webpackPlugin.apply(compiler as never); + compilationCallback(compilation); + processAssets(assets); + + return assets[assetName]; +} + +function runWebpackInjection(assetName: string, code: string, chunks?: TestChunk[]): string { + return runWebpackSourceInjection(assetName, new webpack.sources.RawSource(code), chunks).source().toString(); +} + +describe('sentryWebpackPlugin', () => { + it('preserves a top-level strict mode directive', () => { + const code = '"use strict";\nglobalThis.strictModePreserved = (function () { return this; })() === undefined;'; + const output = runWebpackInjection('120.js', code); + const context: { strictModePreserved?: boolean; _sentryDebugIds?: Record } = {}; + + runInNewContext(output, context); + + expect(context.strictModePreserved).toBe(true); + expect(Object.keys(context._sentryDebugIds ?? {})).toHaveLength(1); + }); + + it('preserves source mappings when injecting after a directive prologue', () => { + const code = '"use strict";\nglobalThis.applicationStarted = true;'; + const inputMap = new MagicString(code).generateMap({ + source: 'application.js', + hires: 'boundary', + includeContent: true, + }); + const source = new webpack.sources.SourceMapSource(code, 'bundle.js', inputMap.toString()); + + const output = runWebpackSourceInjection('bundle.js', source); + const outputMap = output.map(); + + expect(outputMap?.sources).toEqual(['application.js']); + expect(outputMap?.sourcesContent).toEqual([code]); + expect(outputMap?.mappings).toBe('AAAA,CAAC,GAAG,CAAC,MAAM,CAAC;AACZ,+YAAU,CAAC,kBAAkB,CAAC,CAAC,CAAC,IAAI'); + }); + + it('derives the debug ID from the Webpack chunk hash', () => { + const output = runWebpackSourceInjection( + 'bundle.js', + new webpack.sources.RawSource('globalThis.bundleLoaded = true;'), + [{ files: ['bundle.js'], hash: 'stable-webpack-chunk-hash' }], + ) + .source() + .toString(); + + expect(output).toContain('sentry-dbid-1924c426-ebb3-47c2-8293-ea326e499bcc'); + }); + + it.each([ + ['a semicolonless directive', '"use strict"'], + ['trailing whitespace', '"use strict" '], + ['a trailing block comment', '"use strict"/* trailing */'], + ['a trailing line comment', '"use strict" // trailing'], + ])('preserves a directive at EOF with %s', (_description, code) => { + const output = runWebpackInjection('120.js', code); + const context: { strictModePreserved?: boolean; _sentryDebugIds?: Record } = {}; + + runInNewContext( + `${output}\nglobalThis.strictModePreserved = (function () { return this; })() === undefined;`, + context, + ); + + expect(context.strictModePreserved).toBe(true); + expect(Object.keys(context._sentryDebugIds ?? {})).toHaveLength(1); + }); + + it.each(['.ts', '.tsx', '.jsx', '.mts', '.cts'])('injects into a %s asset', extension => { + const output = runWebpackInjection(`bundle${extension}`, 'globalThis.bundleLoaded = true;'); + const context: { _sentryDebugIds?: Record } = {}; + + runInNewContext(output, context); + + expect(Object.keys(context._sentryDebugIds ?? {})).toHaveLength(1); + }); + + it('does not inject into JavaScript assets outside chunks', () => { + const code = 'globalThis.bundleLoaded = true;'; + const output = runWebpackInjection('copied.js', code, []); + + expect(output).toBe(code); + }); + + it('injects into an asset shared by multiple chunks once', () => { + const output = runWebpackInjection('shared.js', 'globalThis.bundleLoaded = true;', [ + { files: ['shared.js'], hash: 'first-chunk-hash' }, + { files: ['shared.js'], hash: 'second-chunk-hash' }, + ]); + + expect(output.match(/_sentryDebugIdIdentifier/g)).toHaveLength(1); + }); +}); diff --git a/packages/bundler-plugins/test/webpack/public-api.test.ts b/packages/bundler-plugins/test/webpack/public-api.test.ts index 97854c6352bf..a6c87bff7270 100644 --- a/packages/bundler-plugins/test/webpack/public-api.test.ts +++ b/packages/bundler-plugins/test/webpack/public-api.test.ts @@ -12,21 +12,26 @@ type PluginClass = new (options: unknown) => unknown; type Compiler = { options: { plugins: unknown[] }; hooks: Record>>; - webpack?: { BannerPlugin: PluginClass; DefinePlugin: PluginClass }; + webpack?: { + DefinePlugin: PluginClass; + Compilation: { PROCESS_ASSETS_STAGE_ADDITIONS: number }; + sources: { ReplaceSource: PluginClass }; + }; }; -class BannerPlugin { +class DefinePlugin { public constructor(public options: unknown) {} } -class DefinePlugin { - public constructor(public options: unknown) {} +class ReplaceSource { + public constructor(public source: unknown) {} } function createCompiler(webpack?: Compiler['webpack']): Compiler { return { options: { plugins: [] }, hooks: { + compilation: { tap: vi.fn() }, thisCompilation: { tap: vi.fn() }, afterEmit: { tapAsync: vi.fn() }, done: { tap: vi.fn() }, @@ -47,12 +52,19 @@ describe('sentryWebpackPlugin', () => { expect(plugin).toEqual({ apply: expect.any(Function) }); }); - it('registers the plugin classes provided by `compiler.webpack`', () => { - const compiler = createCompiler({ BannerPlugin, DefinePlugin }); + it('registers code injection through the compilation hook', () => { + const compiler = createCompiler({ + DefinePlugin, + Compilation: { PROCESS_ASSETS_STAGE_ADDITIONS: -100 }, + sources: { ReplaceSource }, + }); - sentryWebpackPlugin({ telemetry: false, release: { name: 'my-release' } }).apply(compiler); + sentryWebpackPlugin({ telemetry: false, release: { name: 'my-release' } }).apply(compiler as never); - expect(compiler.options.plugins).toEqual([expect.any(BannerPlugin)]); + expect(compiler.hooks.compilation?.tap).toHaveBeenCalledWith( + 'sentry-webpack-plugin-injection', + expect.any(Function), + ); }); it('warns instead of throwing when `compiler.webpack` is unavailable', () => { @@ -60,11 +72,11 @@ describe('sentryWebpackPlugin', () => { const compiler = createCompiler(undefined); expect(() => - sentryWebpackPlugin({ telemetry: false, release: { name: 'my-release' } }).apply(compiler), + sentryWebpackPlugin({ telemetry: false, release: { name: 'my-release' } }).apply(compiler as never), ).not.toThrow(); expect(compiler.options.plugins).toEqual([]); - expect(warn).toHaveBeenCalledWith(expect.stringContaining('BannerPlugin is not available')); + expect(warn).toHaveBeenCalledWith(expect.stringContaining('Webpack sources are not available')); warn.mockRestore(); }); }); diff --git a/packages/nextjs/src/config/loaders/moduleMetadataInjectionLoader.ts b/packages/nextjs/src/config/loaders/moduleMetadataInjectionLoader.ts index 7e8315b51831..0c44bac71db5 100644 --- a/packages/nextjs/src/config/loaders/moduleMetadataInjectionLoader.ts +++ b/packages/nextjs/src/config/loaders/moduleMetadataInjectionLoader.ts @@ -1,5 +1,5 @@ +import { getCodeInjectionPosition } from '@sentry/bundler-plugins/core'; import type { LoaderThis } from './types'; -import { findInjectionIndexAfterDirectives } from './valueInjectionLoader'; export type ModuleMetadataInjectionLoaderOptions = { applicationKey: string; @@ -39,6 +39,7 @@ export default function moduleMetadataInjectionLoader( `e._sentryModuleMetadata[(new e.Error).stack]=Object.assign({},e._sentryModuleMetadata[(new e.Error).stack],${metadata});` + '}catch(e){}}();'; - const injectionIndex = findInjectionIndexAfterDirectives(userCode); - return `${userCode.slice(0, injectionIndex)}${injectedCode}${userCode.slice(injectionIndex)}`; + const injectionIndex = getCodeInjectionPosition(userCode); + const codeToInject = injectionIndex === userCode.length ? `\n${injectedCode}` : injectedCode; + return `${userCode.slice(0, injectionIndex)}${codeToInject}${userCode.slice(injectionIndex)}`; } diff --git a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts index 0040e9f4543d..8937b4e435e1 100644 --- a/packages/nextjs/src/config/loaders/valueInjectionLoader.ts +++ b/packages/nextjs/src/config/loaders/valueInjectionLoader.ts @@ -1,150 +1,10 @@ +import { getCodeInjectionPosition } from '@sentry/bundler-plugins/core'; import type { LoaderThis } from './types'; export type ValueInjectionLoaderOptions = { values: Record; }; -/** - * Finds the index in user code at which to inject statements. - * - * The injection must come AFTER all prologue directives ("use strict", "use client", etc.) - * and any surrounding whitespace/comments, but before any actual statements. - * - * Handles multiple directives, comments between directives, directives without semicolons, - * escape sequences in strings, and strings followed by operators (which are not directives). - */ -export function findInjectionIndexAfterDirectives(userCode: string): number { - let index = 0; - let afterLastDirective: number | undefined; - - while (index < userCode.length) { - const char = userCode[index]; - - if (char && /\s/.test(char)) { - index++; - continue; - } - - if (userCode.startsWith('//', index)) { - const newlineIndex = userCode.indexOf('\n', index + 2); - index = newlineIndex === -1 ? userCode.length : newlineIndex + 1; - continue; - } - - if (userCode.startsWith('/*', index)) { - const commentEndIndex = userCode.indexOf('*/', index + 2); - if (commentEndIndex === -1) { - return afterLastDirective ?? 0; - } - - index = commentEndIndex + 2; - continue; - } - - if (char === '"' || char === "'") { - const stringEnd = findStringLiteralEnd(userCode, index); - if (stringEnd === null) { - return afterLastDirective ?? index; - } - - const terminatorEnd = findDirectiveTerminator(userCode, stringEnd); - if (terminatorEnd === null) { - return afterLastDirective ?? index; - } - - afterLastDirective = terminatorEnd; - index = terminatorEnd; - continue; - } - - return afterLastDirective ?? index; - } - - return afterLastDirective ?? index; -} - -/** - * Scans a string literal starting at `start` (which must be a quote character), - * correctly handling escape sequences and rejecting unterminated/multiline strings. - * Returns the index after the closing quote, or null if the string is unterminated. - */ -function findStringLiteralEnd(userCode: string, startIndex: number): number | null { - const quote = userCode[startIndex]; - let index = startIndex + 1; - - while (index < userCode.length) { - const char = userCode[index]; - - if (char === '\\') { - // skip escaped character - index += 2; - continue; - } - - if (char === quote) { - return index + 1; // found closing quote - } - - if (char === '\n' || char === '\r') { - return null; // unterminated - } - - index++; - } - - return null; // unterminated -} - -/** - * Starting at `i`, skips horizontal whitespace and single-line block comments, - * then checks for a valid directive terminator: `;`, newline, `//`, or EOF. - * Returns the index after the terminator, or null if no valid terminator is found - * (meaning the preceding string literal is not a directive). - */ -function findDirectiveTerminator(userCode: string, startIndex: number): number | null { - let index = startIndex; - - while (index < userCode.length) { - const char = userCode[index]; - - if (char === ';') { - return index + 1; - } - - if (char === '\n' || char === '\r' || char === '}') { - return index; - } - - if (char && /\s/.test(char)) { - index++; - continue; - } - - if (userCode.startsWith('//', index)) { - return index; - } - - if (userCode.startsWith('/*', index)) { - const commentEndIndex = userCode.indexOf('*/', index + 2); - if (commentEndIndex === -1) { - return null; - } - - const comment = userCode.slice(index + 2, commentEndIndex); - if (comment.includes('\n') || comment.includes('\r')) { - return index; - } - - index = commentEndIndex + 2; - continue; - } - - return null; // operator or any other token → not a directive - } - - return index; // EOF is a valid terminator -} - /** * Set values on the global/window object at the start of a module. * @@ -167,6 +27,7 @@ export default function valueInjectionLoader(this: LoaderThis `globalThis["${key}"] = ${JSON.stringify(value)};`) .join(''); - const injectionIndex = findInjectionIndexAfterDirectives(userCode); - return `${userCode.slice(0, injectionIndex)}${injectedCode}${userCode.slice(injectionIndex)}`; + const injectionIndex = getCodeInjectionPosition(userCode); + const codeToInject = injectionIndex === userCode.length ? `\n${injectedCode}` : injectedCode; + return `${userCode.slice(0, injectionIndex)}${codeToInject}${userCode.slice(injectionIndex)}`; } diff --git a/packages/nextjs/test/config/__snapshots__/valueInjectionLoader.test.ts.snap b/packages/nextjs/test/config/__snapshots__/valueInjectionLoader.test.ts.snap index 8853a6c160b0..3c75e31c2e44 100644 --- a/packages/nextjs/test/config/__snapshots__/valueInjectionLoader.test.ts.snap +++ b/packages/nextjs/test/config/__snapshots__/valueInjectionLoader.test.ts.snap @@ -40,16 +40,16 @@ exports[`valueInjectionLoader > should correctly insert values with a misplaced exports[`valueInjectionLoader > should correctly insert values with directive 1`] = ` " - "use client";globalThis["foo"] = "bar"; - import * as Sentry from '@sentry/nextjs'; + "use client" + ;globalThis["foo"] = "bar";import * as Sentry from '@sentry/nextjs'; Sentry.init(); " `; exports[`valueInjectionLoader > should correctly insert values with directive 2`] = ` " - "use client";globalThis["foo"] = "bar"; - import * as Sentry from '@sentry/nextjs'; + "use client" + ;globalThis["foo"] = "bar";import * as Sentry from '@sentry/nextjs'; Sentry.init(); " `; @@ -57,8 +57,8 @@ exports[`valueInjectionLoader > should correctly insert values with directive 2` exports[`valueInjectionLoader > should correctly insert values with directive and block comments 1`] = ` " /* test */ - "use client";;globalThis["foo"] = "bar"; - import * as Sentry from '@sentry/nextjs'; + "use client"; + ;globalThis["foo"] = "bar";import * as Sentry from '@sentry/nextjs'; Sentry.init(); " `; @@ -66,8 +66,8 @@ exports[`valueInjectionLoader > should correctly insert values with directive an exports[`valueInjectionLoader > should correctly insert values with directive and block comments 2`] = ` " /* test */ - "use client";;globalThis["foo"] = "bar"; - import * as Sentry from '@sentry/nextjs'; + "use client"; + ;globalThis["foo"] = "bar";import * as Sentry from '@sentry/nextjs'; Sentry.init(); " `; @@ -75,8 +75,8 @@ exports[`valueInjectionLoader > should correctly insert values with directive an exports[`valueInjectionLoader > should correctly insert values with directive and inline comments 1`] = ` " // test - "use client";;globalThis["foo"] = "bar"; - import * as Sentry from '@sentry/nextjs'; + "use client"; + ;globalThis["foo"] = "bar";import * as Sentry from '@sentry/nextjs'; Sentry.init(); " `; @@ -84,8 +84,8 @@ exports[`valueInjectionLoader > should correctly insert values with directive an exports[`valueInjectionLoader > should correctly insert values with directive and inline comments 2`] = ` " // test - "use client";;globalThis["foo"] = "bar"; - import * as Sentry from '@sentry/nextjs'; + "use client"; + ;globalThis["foo"] = "bar";import * as Sentry from '@sentry/nextjs'; Sentry.init(); " `; @@ -95,8 +95,8 @@ exports[`valueInjectionLoader > should correctly insert values with directive an /* test */ - "use client";;globalThis["foo"] = "bar"; - import * as Sentry from '@sentry/nextjs'; + "use client"; + ;globalThis["foo"] = "bar";import * as Sentry from '@sentry/nextjs'; Sentry.init(); " `; @@ -106,8 +106,8 @@ exports[`valueInjectionLoader > should correctly insert values with directive an /* test */ - "use client";;globalThis["foo"] = "bar"; - import * as Sentry from '@sentry/nextjs'; + "use client"; + ;globalThis["foo"] = "bar";import * as Sentry from '@sentry/nextjs'; Sentry.init(); " `; @@ -121,11 +121,11 @@ exports[`valueInjectionLoader > should correctly insert values with directive an - "use client";;globalThis["foo"] = "bar"; + "use client"; - import * as Sentry from '@sentry/nextjs'; + ;globalThis["foo"] = "bar";import * as Sentry from '@sentry/nextjs'; Sentry.init(); " `; @@ -139,27 +139,27 @@ exports[`valueInjectionLoader > should correctly insert values with directive an - "use client";;globalThis["foo"] = "bar"; + "use client"; - import * as Sentry from '@sentry/nextjs'; + ;globalThis["foo"] = "bar";import * as Sentry from '@sentry/nextjs'; Sentry.init(); " `; exports[`valueInjectionLoader > should correctly insert values with directive and semicolon 1`] = ` " - "use client";;globalThis["foo"] = "bar"; - import * as Sentry from '@sentry/nextjs'; + "use client"; + ;globalThis["foo"] = "bar";import * as Sentry from '@sentry/nextjs'; Sentry.init(); " `; exports[`valueInjectionLoader > should correctly insert values with directive and semicolon 2`] = ` " - "use client";;globalThis["foo"] = "bar"; - import * as Sentry from '@sentry/nextjs'; + "use client"; + ;globalThis["foo"] = "bar";import * as Sentry from '@sentry/nextjs'; Sentry.init(); " `; diff --git a/packages/nextjs/test/config/moduleMetadataInjectionLoader.test.ts b/packages/nextjs/test/config/moduleMetadataInjectionLoader.test.ts index f6c1c613bd00..ef61763b1212 100644 --- a/packages/nextjs/test/config/moduleMetadataInjectionLoader.test.ts +++ b/packages/nextjs/test/config/moduleMetadataInjectionLoader.test.ts @@ -157,4 +157,13 @@ describe('moduleMetadataInjectionLoader', () => { expect(metadataIndex).toBeGreaterThan(clientDirectiveIndex); }); + + it('separates an EOF injection from a trailing line comment', () => { + const loaderThis = createLoaderThis('my-app'); + const userCode = '"use client" // trailing'; + + const result = moduleMetadataInjectionLoader.call(loaderThis, userCode); + + expect(result).toContain('// trailing\n;!function(){try{'); + }); }); diff --git a/packages/nextjs/test/config/valueInjectionLoader.test.ts b/packages/nextjs/test/config/valueInjectionLoader.test.ts index 83c0c1d5e0f9..bc6a730d1c69 100644 --- a/packages/nextjs/test/config/valueInjectionLoader.test.ts +++ b/packages/nextjs/test/config/valueInjectionLoader.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import type { LoaderThis } from '../../src/config/loaders/types'; import type { ValueInjectionLoaderOptions } from '../../src/config/loaders/valueInjectionLoader'; -import valueInjectionLoader, { findInjectionIndexAfterDirectives } from '../../src/config/loaders/valueInjectionLoader'; +import valueInjectionLoader from '../../src/config/loaders/valueInjectionLoader'; const defaultLoaderThis = { addDependency: () => undefined, @@ -217,48 +217,20 @@ describe.each([[clientConfigLoaderThis], [instrumentationLoaderThis]])('valueInj expect(injectionIndex).toBeGreaterThan(clientDirectiveIndex); }); -}); - -describe('findInjectionIndexAfterDirectives', () => { - it('returns the position immediately after the last directive', () => { - const userCode = '"use strict";\n"use client";\nimport React from \'react\';'; - - expect(userCode.slice(findInjectionIndexAfterDirectives(userCode))).toBe("\nimport React from 'react';"); - }); - - it('returns the end of the input when the last directive reaches EOF', () => { - const userCode = '"use strict";\n"use client";'; - - expect(findInjectionIndexAfterDirectives(userCode)).toBe(userCode.length); - }); - - it('does not skip a string literal that is not a directive', () => { - const userCode = '"use client" + suffix;'; - expect(findInjectionIndexAfterDirectives(userCode)).toBe(0); - }); - - it('does not treat an escaped quote at EOF as a closed directive', () => { - const userCode = '"use client\\"'; - - expect(findInjectionIndexAfterDirectives(userCode)).toBe(0); - }); + it('inserts values after a directive with a Unicode line separator', () => { + const userCode = '"use client"\u2028startApp();'; - it('returns 0 for an unterminated leading block comment', () => { - const userCode = '/* unterminated'; + const result = valueInjectionLoader.call(loaderThis, userCode); - expect(findInjectionIndexAfterDirectives(userCode)).toBe(0); + expect(result).toBe('"use client"\u2028;globalThis["foo"] = "bar";startApp();'); }); - it('returns the last complete directive when followed by an unterminated block comment', () => { - const userCode = '"use client"; /* unterminated'; - - expect(findInjectionIndexAfterDirectives(userCode)).toBe('"use client";'.length); - }); + it('separates an EOF injection from a trailing line comment', () => { + const userCode = '"use client" // trailing'; - it('treats a block comment without a line break as part of the same statement', () => { - const userCode = '"use client" /* comment */ + suffix;'; + const result = valueInjectionLoader.call(loaderThis, userCode); - expect(findInjectionIndexAfterDirectives(userCode)).toBe(0); + expect(result).toBe('"use client" // trailing\n;globalThis["foo"] = "bar";'); }); });