Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/nextjs/src/config/diagnosticsChannelInjection.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { resolveOrchestrionRuntimeRequest } from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from './loadOrchestrionBundler';

/**
* Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own
Expand DownExpand Up@@ -58,6 +58,6 @@ export async function externalizeOrchestrionRuntimePackages({
return undefined;
}

const resolved = resolveOrchestrionRuntimeRequest(request);
const resolved = loadOrchestrionBundler().resolveOrchestrionRuntimeRequest(request);
return resolved ? `commonjs ${resolved}` : undefined;
}
29 changes: 29 additions & 0 deletions packages/nextjs/src/config/loadOrchestrionBundler.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
import { createRequire } from 'module';
import type * as orchestrionBundler from '@sentry/server-utils/orchestrion/webpack';

type OrchestrionBundlerModule = typeof orchestrionBundler;

// Use `createRequire` (never the CJS `require` alias) so bundlers don't emit a "Critical
// dependency" warning. Resolving from this file's own location keeps it working under pnpm
// isolated installations.
function getNodeRequire(): ReturnType<typeof createRequire> {
let nodeRequire: ReturnType<typeof createRequire>;
/*! rollup-include-cjs-only */
nodeRequire = createRequire(__filename);
/*! rollup-include-cjs-only-end */
/*! rollup-include-esm-only */
nodeRequire = createRequire(import.meta.url);
/*! rollup-include-esm-only-end */
return nodeRequire;
}

/**
* Loads `@sentry/server-utils/orchestrion/webpack` at call time instead of module scope. The
* runtime server entry re-exports `withSentryConfig`, so a static import would run the bundler
* plugins' module-scope side effects on every server-side SDK import (issues #23789, #22794).
* Synchronous because Next.js `webpack` config functions cannot be async. Node's require cache
* already returns the same module on repeated calls, so no memoization is needed.
*/
export function loadOrchestrionBundler(): OrchestrionBundlerModule {
return getNodeRequire()('@sentry/server-utils/orchestrion/webpack') as OrchestrionBundlerModule;
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import {
getOrchestrionLoaderPath,
getSentryInstrumentations,
serializeInstrumentations,
} from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from '../loadOrchestrionBundler';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type {
Expand DownExpand Up@@ -138,6 +134,8 @@ function maybeAddOrchestrionRule(
return rules;
}

const { getOrchestrionLoaderPath, getSentryInstrumentations, serializeInstrumentations } = loadOrchestrionBundler();

return safelyAddTurbopackRule(rules, {
matcher: '*.{js,mjs,cjs}',
rule: {
Expand Down
6 changes: 4 additions & 2 deletions packages/nextjs/src/config/webpack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@ import type {
WebpackEntryProperty,
WebpackPluginInstance,
} from './types';
import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from './loadOrchestrionBundler';
import { getNextjsVersion, getPackageModules } from './util';
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';

Expand DownExpand Up@@ -434,7 +434,9 @@ export function constructWebpackConfigFunction({

// Orchestrion code-transform loader — Node server runtime only, never the edge compilation
if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) {
newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as unknown as WebpackPluginInstance);
newConfig.plugins.push(
loadOrchestrionBundler().sentryOrchestrionWebpackPlugin() as unknown as WebpackPluginInstance,
);
prependOrchestrionRuntimeExternals(newConfig);
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import '../mocks';
import * as core from '@sentry/core';
import { describe, expect, it, vi } from 'vitest';
import * as getBuildPluginOptionsModule from '../../../src/config/getBuildPluginOptions';
import type * as loadOrchestrionBundlerModule from '../../../src/config/loadOrchestrionBundler';
import * as util from '../../../src/config/util';
import {
CLIENT_SDK_CONFIG_FILE,
Expand All@@ -16,12 +17,18 @@ import {
} from '../fixtures';
import { materializeFinalNextConfig, materializeFinalWebpackConfig } from '../testUtils';

// Only the plugin factory is stubbed — `resolveOrchestrionRuntimeRequest` must stay real because
// the externals handler under test uses it.
vi.mock('@sentry/server-utils/orchestrion/webpack', async importOriginal => ({
...(await importOriginal<Record<string, unknown>>()),
sentryOrchestrionWebpackPlugin: () => ({ _name: 'sentry-orchestrion-webpack-plugin' }),
}));
// Stub only the plugin factory. The externals handler under test needs the real
// `resolveOrchestrionRuntimeRequest`. The bundler module loads via native `require`, which
// `vi.mock` cannot intercept, so the stub goes on the loader.
vi.mock('../../../src/config/loadOrchestrionBundler', async importOriginal => {
const original = await importOriginal<typeof loadOrchestrionBundlerModule>();
return {
loadOrchestrionBundler: () => ({
...original.loadOrchestrionBundler(),
sentryOrchestrionWebpackPlugin: () => ({ _name: 'sentry-orchestrion-webpack-plugin' }),
}),
};
});

describe('constructWebpackConfigFunction()', () => {
it('includes expected properties', async () => {
Expand Down
32 changes: 32 additions & 0 deletions packages/nextjs/test/serverEntryBundlerGraph.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';

/**
* Importing the SDK server entry must not load the orchestrion bundler plugins. They are
* build-time-only, and their module-scope side effects break runtimes the build never sees,
* like jsdom/happy-dom test runs (issue #23789) and Cloudflare Workers cold starts (issue #22794).
* Runs in a child process for a clean module cache and real Node resolution.
*/
describe('built CJS server entry', () => {
const serverEntry = resolve(__dirname, '../build/cjs/index.server.js');

it('loads under a DOM test environment without pulling in the orchestrion bundler graph', () => {
const script = `
globalThis.document = { baseURI: 'http://localhost:3000/' };
require(${JSON.stringify(serverEntry)});
const toPosix = modulePath => modulePath.split(require('path').sep).join('/');
const bundlerModules = Object.keys(require.cache).map(toPosix).filter(
modulePath => modulePath.includes('code-transformer-bundler-plugins') || modulePath.includes('orchestrion/bundler'),
);
if (bundlerModules.length > 0) {
console.error('Bundler-plugin modules loaded at import time:\\n' + bundlerModules.join('\\n'));
process.exit(1);
}
`;

// On failure, stderr carries either the leaked module list or the import crash itself.
const result = spawnSync(process.execPath, ['-e', script], { encoding: 'utf8' });
expect(result.status, result.stderr).toBe(0);
});
});
15 changes: 14 additions & 1 deletion packages/server-utils/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,6 +53,19 @@ const debugNodeAlias = {
},
};

// This package only runs in Node, but rollup's default CJS replacement for `import.meta.url`
// picks browser behavior whenever a `document` global exists, and jsdom/happy-dom define
// `document` while tests run in Node. Always emit the unconditional Node form instead.
const importMetaUrlNodeShim = {
name: 'import-meta-url-node-shim',
resolveImportMeta(property, { format }) {
if (property === 'url' && format === 'cjs') {
return "require('node:url').pathToFileURL(__filename).href";
}
return null;
},
};

// Bundling files from the repo-root `node_modules` moves rollup's common source ancestor up to the
// repo root, so `preserveModules` names our own files `packages/server-utils/src/...` — strip that
// prefix to keep the `build/cjs/index.js` layout the `exports` map points at. And npm never packs
Expand DownExpand Up@@ -124,7 +137,7 @@ export default [
'src/orchestrion/bundler/esbuild.ts',
],
packageSpecificConfig: {
plugins: [debugNodeAlias, commonJSPlugin, thirdPartyLicensePlugin],
plugins: [debugNodeAlias, commonJSPlugin, importMetaUrlNodeShim, thirdPartyLicensePlugin],
output: {
// set exports to 'named' or 'auto' so that rollup doesn't warn
exports: 'named',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
import { createRequire } from 'node:module';
import { resolve } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';

const nodeRequire = createRequire(import.meta.url);
const BUILD_CJS_DIR = resolve(__dirname, '../../build/cjs');

// The five entries share vendored chunks, and the require cache would keep a chunk's module scope
// from running again after the first test. Drop everything under `build/cjs` first, so each test
// really executes the code it claims to.
function requireFresh(entry: string): unknown {
for (const key of Object.keys(nodeRequire.cache)) {
if (key.startsWith(BUILD_CJS_DIR)) {
Reflect.deleteProperty(nodeRequire.cache, key);
}
}
return nodeRequire(resolve(BUILD_CJS_DIR, 'orchestrion/bundler', `${entry}.js`));
}

/**
* The bundler entries must load in Node even when a `document` global exists, which is the case
* under jsdom/happy-dom: the vendored code must never treat `document` as proof of a browser.
* Runs against `build/cjs` because that guard lives in the emitted code, not the sources.
* Reference Issue: https://github.com/getsentry/sentry-javascript/issues/23789
*/
describe('built CJS bundler entries load under DOM test environments', () => {
afterEach(() => {
delete (globalThis as { document?: unknown }).document;
});

it.each(['webpack', 'webpack-loader', 'esbuild', 'vite', 'rollup'])(
'build/cjs/orchestrion/bundler/%s.js loads while a `document` global is defined',
entry => {
(globalThis as { document?: unknown }).document = { baseURI: 'http://localhost:3000/' };
expect(() => requireFresh(entry)).not.toThrow();
},
);
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/nextjs/src/config/diagnosticsChannelInjection.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { resolveOrchestrionRuntimeRequest } from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from './loadOrchestrionBundler';

/**
* Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own
Expand DownExpand Up@@ -58,6 +58,6 @@ export async function externalizeOrchestrionRuntimePackages({
return undefined;
}

const resolved = resolveOrchestrionRuntimeRequest(request);
const resolved = loadOrchestrionBundler().resolveOrchestrionRuntimeRequest(request);
return resolved ? `commonjs ${resolved}` : undefined;
}
29 changes: 29 additions & 0 deletions packages/nextjs/src/config/loadOrchestrionBundler.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
import { createRequire } from 'module';
import type * as orchestrionBundler from '@sentry/server-utils/orchestrion/webpack';

type OrchestrionBundlerModule = typeof orchestrionBundler;

// Use `createRequire` (never the CJS `require` alias) so bundlers don't emit a "Critical
// dependency" warning. Resolving from this file's own location keeps it working under pnpm
// isolated installations.
function getNodeRequire(): ReturnType<typeof createRequire> {
let nodeRequire: ReturnType<typeof createRequire>;
/*! rollup-include-cjs-only */
nodeRequire = createRequire(__filename);
/*! rollup-include-cjs-only-end */
/*! rollup-include-esm-only */
nodeRequire = createRequire(import.meta.url);
/*! rollup-include-esm-only-end */
return nodeRequire;
}

/**
* Loads `@sentry/server-utils/orchestrion/webpack` at call time instead of module scope. The
* runtime server entry re-exports `withSentryConfig`, so a static import would run the bundler
* plugins' module-scope side effects on every server-side SDK import (issues #23789, #22794).
* Synchronous because Next.js `webpack` config functions cannot be async. Node's require cache
* already returns the same module on repeated calls, so no memoization is needed.
*/
export function loadOrchestrionBundler(): OrchestrionBundlerModule {
return getNodeRequire()('@sentry/server-utils/orchestrion/webpack') as OrchestrionBundlerModule;
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import {
getOrchestrionLoaderPath,
getSentryInstrumentations,
serializeInstrumentations,
} from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from '../loadOrchestrionBundler';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type {
Expand DownExpand Up@@ -138,6 +134,8 @@ function maybeAddOrchestrionRule(
return rules;
}

const { getOrchestrionLoaderPath, getSentryInstrumentations, serializeInstrumentations } = loadOrchestrionBundler();

return safelyAddTurbopackRule(rules, {
matcher: '*.{js,mjs,cjs}',
rule: {
Expand Down
6 changes: 4 additions & 2 deletions packages/nextjs/src/config/webpack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@ import type {
WebpackEntryProperty,
WebpackPluginInstance,
} from './types';
import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from './loadOrchestrionBundler';
import { getNextjsVersion, getPackageModules } from './util';
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';

Expand DownExpand Up@@ -434,7 +434,9 @@ export function constructWebpackConfigFunction({

// Orchestrion code-transform loader — Node server runtime only, never the edge compilation
if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) {
newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as unknown as WebpackPluginInstance);
newConfig.plugins.push(
loadOrchestrionBundler().sentryOrchestrionWebpackPlugin() as unknown as WebpackPluginInstance,
);
prependOrchestrionRuntimeExternals(newConfig);
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import '../mocks';
import * as core from '@sentry/core';
import { describe, expect, it, vi } from 'vitest';
import * as getBuildPluginOptionsModule from '../../../src/config/getBuildPluginOptions';
import type * as loadOrchestrionBundlerModule from '../../../src/config/loadOrchestrionBundler';
import * as util from '../../../src/config/util';
import {
CLIENT_SDK_CONFIG_FILE,
Expand All@@ -16,12 +17,18 @@ import {
} from '../fixtures';
import { materializeFinalNextConfig, materializeFinalWebpackConfig } from '../testUtils';

// Only the plugin factory is stubbed — `resolveOrchestrionRuntimeRequest` must stay real because
// the externals handler under test uses it.
vi.mock('@sentry/server-utils/orchestrion/webpack', async importOriginal => ({
...(await importOriginal<Record<string, unknown>>()),
sentryOrchestrionWebpackPlugin: () => ({ _name: 'sentry-orchestrion-webpack-plugin' }),
}));
// Stub only the plugin factory. The externals handler under test needs the real
// `resolveOrchestrionRuntimeRequest`. The bundler module loads via native `require`, which
// `vi.mock` cannot intercept, so the stub goes on the loader.
vi.mock('../../../src/config/loadOrchestrionBundler', async importOriginal => {
const original = await importOriginal<typeof loadOrchestrionBundlerModule>();
return {
loadOrchestrionBundler: () => ({
...original.loadOrchestrionBundler(),
sentryOrchestrionWebpackPlugin: () => ({ _name: 'sentry-orchestrion-webpack-plugin' }),
}),
};
});

describe('constructWebpackConfigFunction()', () => {
it('includes expected properties', async () => {
Expand Down
32 changes: 32 additions & 0 deletions packages/nextjs/test/serverEntryBundlerGraph.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';

/**
* Importing the SDK server entry must not load the orchestrion bundler plugins. They are
* build-time-only, and their module-scope side effects break runtimes the build never sees,
* like jsdom/happy-dom test runs (issue #23789) and Cloudflare Workers cold starts (issue #22794).
* Runs in a child process for a clean module cache and real Node resolution.
*/
describe('built CJS server entry', () => {
const serverEntry = resolve(__dirname, '../build/cjs/index.server.js');

it('loads under a DOM test environment without pulling in the orchestrion bundler graph', () => {
const script = `
globalThis.document = { baseURI: 'http://localhost:3000/' };
require(${JSON.stringify(serverEntry)});
const toPosix = modulePath => modulePath.split(require('path').sep).join('/');
const bundlerModules = Object.keys(require.cache).map(toPosix).filter(
modulePath => modulePath.includes('code-transformer-bundler-plugins') || modulePath.includes('orchestrion/bundler'),
);
if (bundlerModules.length > 0) {
console.error('Bundler-plugin modules loaded at import time:\\n' + bundlerModules.join('\\n'));
process.exit(1);
}
`;

// On failure, stderr carries either the leaked module list or the import crash itself.
const result = spawnSync(process.execPath, ['-e', script], { encoding: 'utf8' });
expect(result.status, result.stderr).toBe(0);
});
});
15 changes: 14 additions & 1 deletion packages/server-utils/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,6 +53,19 @@ const debugNodeAlias = {
},
};

// This package only runs in Node, but rollup's default CJS replacement for `import.meta.url`
// picks browser behavior whenever a `document` global exists, and jsdom/happy-dom define
// `document` while tests run in Node. Always emit the unconditional Node form instead.
const importMetaUrlNodeShim = {
name: 'import-meta-url-node-shim',
resolveImportMeta(property, { format }) {
if (property === 'url' && format === 'cjs') {
return "require('node:url').pathToFileURL(__filename).href";
}
return null;
},
};

// Bundling files from the repo-root `node_modules` moves rollup's common source ancestor up to the
// repo root, so `preserveModules` names our own files `packages/server-utils/src/...` — strip that
// prefix to keep the `build/cjs/index.js` layout the `exports` map points at. And npm never packs
Expand DownExpand Up@@ -124,7 +137,7 @@ export default [
'src/orchestrion/bundler/esbuild.ts',
],
packageSpecificConfig: {
plugins: [debugNodeAlias, commonJSPlugin, thirdPartyLicensePlugin],
plugins: [debugNodeAlias, commonJSPlugin, importMetaUrlNodeShim, thirdPartyLicensePlugin],
output: {
// set exports to 'named' or 'auto' so that rollup doesn't warn
exports: 'named',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
import { createRequire } from 'node:module';
import { resolve } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';

const nodeRequire = createRequire(import.meta.url);
const BUILD_CJS_DIR = resolve(__dirname, '../../build/cjs');

// The five entries share vendored chunks, and the require cache would keep a chunk's module scope
// from running again after the first test. Drop everything under `build/cjs` first, so each test
// really executes the code it claims to.
function requireFresh(entry: string): unknown {
for (const key of Object.keys(nodeRequire.cache)) {
if (key.startsWith(BUILD_CJS_DIR)) {
Reflect.deleteProperty(nodeRequire.cache, key);
}
}
return nodeRequire(resolve(BUILD_CJS_DIR, 'orchestrion/bundler', `${entry}.js`));
}

/**
* The bundler entries must load in Node even when a `document` global exists, which is the case
* under jsdom/happy-dom: the vendored code must never treat `document` as proof of a browser.
* Runs against `build/cjs` because that guard lives in the emitted code, not the sources.
* Reference Issue: https://github.com/getsentry/sentry-javascript/issues/23789
*/
describe('built CJS bundler entries load under DOM test environments', () => {
afterEach(() => {
delete (globalThis as { document?: unknown }).document;
});

it.each(['webpack', 'webpack-loader', 'esbuild', 'vite', 'rollup'])(
'build/cjs/orchestrion/bundler/%s.js loads while a `document` global is defined',
entry => {
(globalThis as { document?: unknown }).document = { baseURI: 'http://localhost:3000/' };
expect(() => requireFresh(entry)).not.toThrow();
},
);
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/nextjs/src/config/diagnosticsChannelInjection.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { resolveOrchestrionRuntimeRequest } from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from './loadOrchestrionBundler';

/**
* Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own
Expand DownExpand Up@@ -58,6 +58,6 @@ export async function externalizeOrchestrionRuntimePackages({
return undefined;
}

const resolved = resolveOrchestrionRuntimeRequest(request);
const resolved = loadOrchestrionBundler().resolveOrchestrionRuntimeRequest(request);
return resolved ? `commonjs ${resolved}` : undefined;
}
29 changes: 29 additions & 0 deletions packages/nextjs/src/config/loadOrchestrionBundler.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
import { createRequire } from 'module';
import type * as orchestrionBundler from '@sentry/server-utils/orchestrion/webpack';

type OrchestrionBundlerModule = typeof orchestrionBundler;

// Use `createRequire` (never the CJS `require` alias) so bundlers don't emit a "Critical
// dependency" warning. Resolving from this file's own location keeps it working under pnpm
// isolated installations.
function getNodeRequire(): ReturnType<typeof createRequire> {
let nodeRequire: ReturnType<typeof createRequire>;
/*! rollup-include-cjs-only */
nodeRequire = createRequire(__filename);
/*! rollup-include-cjs-only-end */
/*! rollup-include-esm-only */
nodeRequire = createRequire(import.meta.url);
/*! rollup-include-esm-only-end */
return nodeRequire;
}

/**
* Loads `@sentry/server-utils/orchestrion/webpack` at call time instead of module scope. The
* runtime server entry re-exports `withSentryConfig`, so a static import would run the bundler
* plugins' module-scope side effects on every server-side SDK import (issues #23789, #22794).
* Synchronous because Next.js `webpack` config functions cannot be async. Node's require cache
* already returns the same module on repeated calls, so no memoization is needed.
*/
export function loadOrchestrionBundler(): OrchestrionBundlerModule {
return getNodeRequire()('@sentry/server-utils/orchestrion/webpack') as OrchestrionBundlerModule;
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import {
getOrchestrionLoaderPath,
getSentryInstrumentations,
serializeInstrumentations,
} from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from '../loadOrchestrionBundler';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type {
Expand DownExpand Up@@ -138,6 +134,8 @@ function maybeAddOrchestrionRule(
return rules;
}

const { getOrchestrionLoaderPath, getSentryInstrumentations, serializeInstrumentations } = loadOrchestrionBundler();

return safelyAddTurbopackRule(rules, {
matcher: '*.{js,mjs,cjs}',
rule: {
Expand Down
6 changes: 4 additions & 2 deletions packages/nextjs/src/config/webpack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@ import type {
WebpackEntryProperty,
WebpackPluginInstance,
} from './types';
import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from './loadOrchestrionBundler';
import { getNextjsVersion, getPackageModules } from './util';
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';

Expand DownExpand Up@@ -434,7 +434,9 @@ export function constructWebpackConfigFunction({

// Orchestrion code-transform loader — Node server runtime only, never the edge compilation
if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) {
newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as unknown as WebpackPluginInstance);
newConfig.plugins.push(
loadOrchestrionBundler().sentryOrchestrionWebpackPlugin() as unknown as WebpackPluginInstance,
);
prependOrchestrionRuntimeExternals(newConfig);
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import '../mocks';
import * as core from '@sentry/core';
import { describe, expect, it, vi } from 'vitest';
import * as getBuildPluginOptionsModule from '../../../src/config/getBuildPluginOptions';
import type * as loadOrchestrionBundlerModule from '../../../src/config/loadOrchestrionBundler';
import * as util from '../../../src/config/util';
import {
CLIENT_SDK_CONFIG_FILE,
Expand All@@ -16,12 +17,18 @@ import {
} from '../fixtures';
import { materializeFinalNextConfig, materializeFinalWebpackConfig } from '../testUtils';

// Only the plugin factory is stubbed — `resolveOrchestrionRuntimeRequest` must stay real because
// the externals handler under test uses it.
vi.mock('@sentry/server-utils/orchestrion/webpack', async importOriginal => ({
...(await importOriginal<Record<string, unknown>>()),
sentryOrchestrionWebpackPlugin: () => ({ _name: 'sentry-orchestrion-webpack-plugin' }),
}));
// Stub only the plugin factory. The externals handler under test needs the real
// `resolveOrchestrionRuntimeRequest`. The bundler module loads via native `require`, which
// `vi.mock` cannot intercept, so the stub goes on the loader.
vi.mock('../../../src/config/loadOrchestrionBundler', async importOriginal => {
const original = await importOriginal<typeof loadOrchestrionBundlerModule>();
return {
loadOrchestrionBundler: () => ({
...original.loadOrchestrionBundler(),
sentryOrchestrionWebpackPlugin: () => ({ _name: 'sentry-orchestrion-webpack-plugin' }),
}),
};
});

describe('constructWebpackConfigFunction()', () => {
it('includes expected properties', async () => {
Expand Down
32 changes: 32 additions & 0 deletions packages/nextjs/test/serverEntryBundlerGraph.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';

/**
* Importing the SDK server entry must not load the orchestrion bundler plugins. They are
* build-time-only, and their module-scope side effects break runtimes the build never sees,
* like jsdom/happy-dom test runs (issue #23789) and Cloudflare Workers cold starts (issue #22794).
* Runs in a child process for a clean module cache and real Node resolution.
*/
describe('built CJS server entry', () => {
const serverEntry = resolve(__dirname, '../build/cjs/index.server.js');

it('loads under a DOM test environment without pulling in the orchestrion bundler graph', () => {
const script = `
globalThis.document = { baseURI: 'http://localhost:3000/' };
require(${JSON.stringify(serverEntry)});
const toPosix = modulePath => modulePath.split(require('path').sep).join('/');
const bundlerModules = Object.keys(require.cache).map(toPosix).filter(
modulePath => modulePath.includes('code-transformer-bundler-plugins') || modulePath.includes('orchestrion/bundler'),
);
if (bundlerModules.length > 0) {
console.error('Bundler-plugin modules loaded at import time:\\n' + bundlerModules.join('\\n'));
process.exit(1);
}
`;

// On failure, stderr carries either the leaked module list or the import crash itself.
const result = spawnSync(process.execPath, ['-e', script], { encoding: 'utf8' });
expect(result.status, result.stderr).toBe(0);
});
});
15 changes: 14 additions & 1 deletion packages/server-utils/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,6 +53,19 @@ const debugNodeAlias = {
},
};

// This package only runs in Node, but rollup's default CJS replacement for `import.meta.url`
// picks browser behavior whenever a `document` global exists, and jsdom/happy-dom define
// `document` while tests run in Node. Always emit the unconditional Node form instead.
const importMetaUrlNodeShim = {
name: 'import-meta-url-node-shim',
resolveImportMeta(property, { format }) {
if (property === 'url' && format === 'cjs') {
return "require('node:url').pathToFileURL(__filename).href";
}
return null;
},
};

// Bundling files from the repo-root `node_modules` moves rollup's common source ancestor up to the
// repo root, so `preserveModules` names our own files `packages/server-utils/src/...` — strip that
// prefix to keep the `build/cjs/index.js` layout the `exports` map points at. And npm never packs
Expand DownExpand Up@@ -124,7 +137,7 @@ export default [
'src/orchestrion/bundler/esbuild.ts',
],
packageSpecificConfig: {
plugins: [debugNodeAlias, commonJSPlugin, thirdPartyLicensePlugin],
plugins: [debugNodeAlias, commonJSPlugin, importMetaUrlNodeShim, thirdPartyLicensePlugin],
output: {
// set exports to 'named' or 'auto' so that rollup doesn't warn
exports: 'named',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
import { createRequire } from 'node:module';
import { resolve } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';

const nodeRequire = createRequire(import.meta.url);
const BUILD_CJS_DIR = resolve(__dirname, '../../build/cjs');

// The five entries share vendored chunks, and the require cache would keep a chunk's module scope
// from running again after the first test. Drop everything under `build/cjs` first, so each test
// really executes the code it claims to.
function requireFresh(entry: string): unknown {
for (const key of Object.keys(nodeRequire.cache)) {
if (key.startsWith(BUILD_CJS_DIR)) {
Reflect.deleteProperty(nodeRequire.cache, key);
}
}
return nodeRequire(resolve(BUILD_CJS_DIR, 'orchestrion/bundler', `${entry}.js`));
}

/**
* The bundler entries must load in Node even when a `document` global exists, which is the case
* under jsdom/happy-dom: the vendored code must never treat `document` as proof of a browser.
* Runs against `build/cjs` because that guard lives in the emitted code, not the sources.
* Reference Issue: https://github.com/getsentry/sentry-javascript/issues/23789
*/
describe('built CJS bundler entries load under DOM test environments', () => {
afterEach(() => {
delete (globalThis as { document?: unknown }).document;
});

it.each(['webpack', 'webpack-loader', 'esbuild', 'vite', 'rollup'])(
'build/cjs/orchestrion/bundler/%s.js loads while a `document` global is defined',
entry => {
(globalThis as { document?: unknown }).document = { baseURI: 'http://localhost:3000/' };
expect(() => requireFresh(entry)).not.toThrow();
},
);
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/nextjs/src/config/diagnosticsChannelInjection.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { resolveOrchestrionRuntimeRequest } from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from './loadOrchestrionBundler';

/**
* Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own
Expand DownExpand Up@@ -58,6 +58,6 @@ export async function externalizeOrchestrionRuntimePackages({
return undefined;
}

const resolved = resolveOrchestrionRuntimeRequest(request);
const resolved = loadOrchestrionBundler().resolveOrchestrionRuntimeRequest(request);
return resolved ? `commonjs ${resolved}` : undefined;
}
29 changes: 29 additions & 0 deletions packages/nextjs/src/config/loadOrchestrionBundler.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
import { createRequire } from 'module';
import type * as orchestrionBundler from '@sentry/server-utils/orchestrion/webpack';

type OrchestrionBundlerModule = typeof orchestrionBundler;

// Use `createRequire` (never the CJS `require` alias) so bundlers don't emit a "Critical
// dependency" warning. Resolving from this file's own location keeps it working under pnpm
// isolated installations.
function getNodeRequire(): ReturnType<typeof createRequire> {
let nodeRequire: ReturnType<typeof createRequire>;
/*! rollup-include-cjs-only */
nodeRequire = createRequire(__filename);
/*! rollup-include-cjs-only-end */
/*! rollup-include-esm-only */
nodeRequire = createRequire(import.meta.url);
/*! rollup-include-esm-only-end */
return nodeRequire;
}

/**
* Loads `@sentry/server-utils/orchestrion/webpack` at call time instead of module scope. The
* runtime server entry re-exports `withSentryConfig`, so a static import would run the bundler
* plugins' module-scope side effects on every server-side SDK import (issues #23789, #22794).
* Synchronous because Next.js `webpack` config functions cannot be async. Node's require cache
* already returns the same module on repeated calls, so no memoization is needed.
*/
export function loadOrchestrionBundler(): OrchestrionBundlerModule {
return getNodeRequire()('@sentry/server-utils/orchestrion/webpack') as OrchestrionBundlerModule;
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import {
getOrchestrionLoaderPath,
getSentryInstrumentations,
serializeInstrumentations,
} from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from '../loadOrchestrionBundler';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type {
Expand DownExpand Up@@ -138,6 +134,8 @@ function maybeAddOrchestrionRule(
return rules;
}

const { getOrchestrionLoaderPath, getSentryInstrumentations, serializeInstrumentations } = loadOrchestrionBundler();

return safelyAddTurbopackRule(rules, {
matcher: '*.{js,mjs,cjs}',
rule: {
Expand Down
6 changes: 4 additions & 2 deletions packages/nextjs/src/config/webpack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@ import type {
WebpackEntryProperty,
WebpackPluginInstance,
} from './types';
import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from './loadOrchestrionBundler';
import { getNextjsVersion, getPackageModules } from './util';
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';

Expand DownExpand Up@@ -434,7 +434,9 @@ export function constructWebpackConfigFunction({

// Orchestrion code-transform loader — Node server runtime only, never the edge compilation
if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) {
newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as unknown as WebpackPluginInstance);
newConfig.plugins.push(
loadOrchestrionBundler().sentryOrchestrionWebpackPlugin() as unknown as WebpackPluginInstance,
);
prependOrchestrionRuntimeExternals(newConfig);
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import '../mocks';
import * as core from '@sentry/core';
import { describe, expect, it, vi } from 'vitest';
import * as getBuildPluginOptionsModule from '../../../src/config/getBuildPluginOptions';
import type * as loadOrchestrionBundlerModule from '../../../src/config/loadOrchestrionBundler';
import * as util from '../../../src/config/util';
import {
CLIENT_SDK_CONFIG_FILE,
Expand All@@ -16,12 +17,18 @@ import {
} from '../fixtures';
import { materializeFinalNextConfig, materializeFinalWebpackConfig } from '../testUtils';

// Only the plugin factory is stubbed — `resolveOrchestrionRuntimeRequest` must stay real because
// the externals handler under test uses it.
vi.mock('@sentry/server-utils/orchestrion/webpack', async importOriginal => ({
...(await importOriginal<Record<string, unknown>>()),
sentryOrchestrionWebpackPlugin: () => ({ _name: 'sentry-orchestrion-webpack-plugin' }),
}));
// Stub only the plugin factory. The externals handler under test needs the real
// `resolveOrchestrionRuntimeRequest`. The bundler module loads via native `require`, which
// `vi.mock` cannot intercept, so the stub goes on the loader.
vi.mock('../../../src/config/loadOrchestrionBundler', async importOriginal => {
const original = await importOriginal<typeof loadOrchestrionBundlerModule>();
return {
loadOrchestrionBundler: () => ({
...original.loadOrchestrionBundler(),
sentryOrchestrionWebpackPlugin: () => ({ _name: 'sentry-orchestrion-webpack-plugin' }),
}),
};
});

describe('constructWebpackConfigFunction()', () => {
it('includes expected properties', async () => {
Expand Down
32 changes: 32 additions & 0 deletions packages/nextjs/test/serverEntryBundlerGraph.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';

/**
* Importing the SDK server entry must not load the orchestrion bundler plugins. They are
* build-time-only, and their module-scope side effects break runtimes the build never sees,
* like jsdom/happy-dom test runs (issue #23789) and Cloudflare Workers cold starts (issue #22794).
* Runs in a child process for a clean module cache and real Node resolution.
*/
describe('built CJS server entry', () => {
const serverEntry = resolve(__dirname, '../build/cjs/index.server.js');

it('loads under a DOM test environment without pulling in the orchestrion bundler graph', () => {
const script = `
globalThis.document = { baseURI: 'http://localhost:3000/' };
require(${JSON.stringify(serverEntry)});
const toPosix = modulePath => modulePath.split(require('path').sep).join('/');
const bundlerModules = Object.keys(require.cache).map(toPosix).filter(
modulePath => modulePath.includes('code-transformer-bundler-plugins') || modulePath.includes('orchestrion/bundler'),
);
if (bundlerModules.length > 0) {
console.error('Bundler-plugin modules loaded at import time:\\n' + bundlerModules.join('\\n'));
process.exit(1);
}
`;

// On failure, stderr carries either the leaked module list or the import crash itself.
const result = spawnSync(process.execPath, ['-e', script], { encoding: 'utf8' });
expect(result.status, result.stderr).toBe(0);
});
});
15 changes: 14 additions & 1 deletion packages/server-utils/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,6 +53,19 @@ const debugNodeAlias = {
},
};

// This package only runs in Node, but rollup's default CJS replacement for `import.meta.url`
// picks browser behavior whenever a `document` global exists, and jsdom/happy-dom define
// `document` while tests run in Node. Always emit the unconditional Node form instead.
const importMetaUrlNodeShim = {
name: 'import-meta-url-node-shim',
resolveImportMeta(property, { format }) {
if (property === 'url' && format === 'cjs') {
return "require('node:url').pathToFileURL(__filename).href";
}
return null;
},
};

// Bundling files from the repo-root `node_modules` moves rollup's common source ancestor up to the
// repo root, so `preserveModules` names our own files `packages/server-utils/src/...` — strip that
// prefix to keep the `build/cjs/index.js` layout the `exports` map points at. And npm never packs
Expand DownExpand Up@@ -124,7 +137,7 @@ export default [
'src/orchestrion/bundler/esbuild.ts',
],
packageSpecificConfig: {
plugins: [debugNodeAlias, commonJSPlugin, thirdPartyLicensePlugin],
plugins: [debugNodeAlias, commonJSPlugin, importMetaUrlNodeShim, thirdPartyLicensePlugin],
output: {
// set exports to 'named' or 'auto' so that rollup doesn't warn
exports: 'named',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
import { createRequire } from 'node:module';
import { resolve } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';

const nodeRequire = createRequire(import.meta.url);
const BUILD_CJS_DIR = resolve(__dirname, '../../build/cjs');

// The five entries share vendored chunks, and the require cache would keep a chunk's module scope
// from running again after the first test. Drop everything under `build/cjs` first, so each test
// really executes the code it claims to.
function requireFresh(entry: string): unknown {
for (const key of Object.keys(nodeRequire.cache)) {
if (key.startsWith(BUILD_CJS_DIR)) {
Reflect.deleteProperty(nodeRequire.cache, key);
}
}
return nodeRequire(resolve(BUILD_CJS_DIR, 'orchestrion/bundler', `${entry}.js`));
}

/**
* The bundler entries must load in Node even when a `document` global exists, which is the case
* under jsdom/happy-dom: the vendored code must never treat `document` as proof of a browser.
* Runs against `build/cjs` because that guard lives in the emitted code, not the sources.
* Reference Issue: https://github.com/getsentry/sentry-javascript/issues/23789
*/
describe('built CJS bundler entries load under DOM test environments', () => {
afterEach(() => {
delete (globalThis as { document?: unknown }).document;
});

it.each(['webpack', 'webpack-loader', 'esbuild', 'vite', 'rollup'])(
'build/cjs/orchestrion/bundler/%s.js loads while a `document` global is defined',
entry => {
(globalThis as { document?: unknown }).document = { baseURI: 'http://localhost:3000/' };
expect(() => requireFresh(entry)).not.toThrow();
},
);
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/nextjs/src/config/diagnosticsChannelInjection.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { resolveOrchestrionRuntimeRequest } from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from './loadOrchestrionBundler';

/**
* Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own
Expand DownExpand Up@@ -58,6 +58,6 @@ export async function externalizeOrchestrionRuntimePackages({
return undefined;
}

const resolved = resolveOrchestrionRuntimeRequest(request);
const resolved = loadOrchestrionBundler().resolveOrchestrionRuntimeRequest(request);
return resolved ? `commonjs ${resolved}` : undefined;
}
29 changes: 29 additions & 0 deletions packages/nextjs/src/config/loadOrchestrionBundler.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
import { createRequire } from 'module';
import type * as orchestrionBundler from '@sentry/server-utils/orchestrion/webpack';

type OrchestrionBundlerModule = typeof orchestrionBundler;

// Use `createRequire` (never the CJS `require` alias) so bundlers don't emit a "Critical
// dependency" warning. Resolving from this file's own location keeps it working under pnpm
// isolated installations.
function getNodeRequire(): ReturnType<typeof createRequire> {
let nodeRequire: ReturnType<typeof createRequire>;
/*! rollup-include-cjs-only */
nodeRequire = createRequire(__filename);
/*! rollup-include-cjs-only-end */
/*! rollup-include-esm-only */
nodeRequire = createRequire(import.meta.url);
/*! rollup-include-esm-only-end */
return nodeRequire;
}

/**
* Loads `@sentry/server-utils/orchestrion/webpack` at call time instead of module scope. The
* runtime server entry re-exports `withSentryConfig`, so a static import would run the bundler
* plugins' module-scope side effects on every server-side SDK import (issues #23789, #22794).
* Synchronous because Next.js `webpack` config functions cannot be async. Node's require cache
* already returns the same module on repeated calls, so no memoization is needed.
*/
export function loadOrchestrionBundler(): OrchestrionBundlerModule {
return getNodeRequire()('@sentry/server-utils/orchestrion/webpack') as OrchestrionBundlerModule;
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import {
getOrchestrionLoaderPath,
getSentryInstrumentations,
serializeInstrumentations,
} from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from '../loadOrchestrionBundler';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type {
Expand DownExpand Up@@ -138,6 +134,8 @@ function maybeAddOrchestrionRule(
return rules;
}

const { getOrchestrionLoaderPath, getSentryInstrumentations, serializeInstrumentations } = loadOrchestrionBundler();

return safelyAddTurbopackRule(rules, {
matcher: '*.{js,mjs,cjs}',
rule: {
Expand Down
6 changes: 4 additions & 2 deletions packages/nextjs/src/config/webpack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@ import type {
WebpackEntryProperty,
WebpackPluginInstance,
} from './types';
import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from './loadOrchestrionBundler';
import { getNextjsVersion, getPackageModules } from './util';
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';

Expand DownExpand Up@@ -434,7 +434,9 @@ export function constructWebpackConfigFunction({

// Orchestrion code-transform loader — Node server runtime only, never the edge compilation
if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) {
newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as unknown as WebpackPluginInstance);
newConfig.plugins.push(
loadOrchestrionBundler().sentryOrchestrionWebpackPlugin() as unknown as WebpackPluginInstance,
);
prependOrchestrionRuntimeExternals(newConfig);
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import '../mocks';
import * as core from '@sentry/core';
import { describe, expect, it, vi } from 'vitest';
import * as getBuildPluginOptionsModule from '../../../src/config/getBuildPluginOptions';
import type * as loadOrchestrionBundlerModule from '../../../src/config/loadOrchestrionBundler';
import * as util from '../../../src/config/util';
import {
CLIENT_SDK_CONFIG_FILE,
Expand All@@ -16,12 +17,18 @@ import {
} from '../fixtures';
import { materializeFinalNextConfig, materializeFinalWebpackConfig } from '../testUtils';

// Only the plugin factory is stubbed — `resolveOrchestrionRuntimeRequest` must stay real because
// the externals handler under test uses it.
vi.mock('@sentry/server-utils/orchestrion/webpack', async importOriginal => ({
...(await importOriginal<Record<string, unknown>>()),
sentryOrchestrionWebpackPlugin: () => ({ _name: 'sentry-orchestrion-webpack-plugin' }),
}));
// Stub only the plugin factory. The externals handler under test needs the real
// `resolveOrchestrionRuntimeRequest`. The bundler module loads via native `require`, which
// `vi.mock` cannot intercept, so the stub goes on the loader.
vi.mock('../../../src/config/loadOrchestrionBundler', async importOriginal => {
const original = await importOriginal<typeof loadOrchestrionBundlerModule>();
return {
loadOrchestrionBundler: () => ({
...original.loadOrchestrionBundler(),
sentryOrchestrionWebpackPlugin: () => ({ _name: 'sentry-orchestrion-webpack-plugin' }),
}),
};
});

describe('constructWebpackConfigFunction()', () => {
it('includes expected properties', async () => {
Expand Down
32 changes: 32 additions & 0 deletions packages/nextjs/test/serverEntryBundlerGraph.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';

/**
* Importing the SDK server entry must not load the orchestrion bundler plugins. They are
* build-time-only, and their module-scope side effects break runtimes the build never sees,
* like jsdom/happy-dom test runs (issue #23789) and Cloudflare Workers cold starts (issue #22794).
* Runs in a child process for a clean module cache and real Node resolution.
*/
describe('built CJS server entry', () => {
const serverEntry = resolve(__dirname, '../build/cjs/index.server.js');

it('loads under a DOM test environment without pulling in the orchestrion bundler graph', () => {
const script = `
globalThis.document = { baseURI: 'http://localhost:3000/' };
require(${JSON.stringify(serverEntry)});
const toPosix = modulePath => modulePath.split(require('path').sep).join('/');
const bundlerModules = Object.keys(require.cache).map(toPosix).filter(
modulePath => modulePath.includes('code-transformer-bundler-plugins') || modulePath.includes('orchestrion/bundler'),
);
if (bundlerModules.length > 0) {
console.error('Bundler-plugin modules loaded at import time:\\n' + bundlerModules.join('\\n'));
process.exit(1);
}
`;

// On failure, stderr carries either the leaked module list or the import crash itself.
const result = spawnSync(process.execPath, ['-e', script], { encoding: 'utf8' });
expect(result.status, result.stderr).toBe(0);
});
});
15 changes: 14 additions & 1 deletion packages/server-utils/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,6 +53,19 @@ const debugNodeAlias = {
},
};

// This package only runs in Node, but rollup's default CJS replacement for `import.meta.url`
// picks browser behavior whenever a `document` global exists, and jsdom/happy-dom define
// `document` while tests run in Node. Always emit the unconditional Node form instead.
const importMetaUrlNodeShim = {
name: 'import-meta-url-node-shim',
resolveImportMeta(property, { format }) {
if (property === 'url' && format === 'cjs') {
return "require('node:url').pathToFileURL(__filename).href";
}
return null;
},
};

// Bundling files from the repo-root `node_modules` moves rollup's common source ancestor up to the
// repo root, so `preserveModules` names our own files `packages/server-utils/src/...` — strip that
// prefix to keep the `build/cjs/index.js` layout the `exports` map points at. And npm never packs
Expand DownExpand Up@@ -124,7 +137,7 @@ export default [
'src/orchestrion/bundler/esbuild.ts',
],
packageSpecificConfig: {
plugins: [debugNodeAlias, commonJSPlugin, thirdPartyLicensePlugin],
plugins: [debugNodeAlias, commonJSPlugin, importMetaUrlNodeShim, thirdPartyLicensePlugin],
output: {
// set exports to 'named' or 'auto' so that rollup doesn't warn
exports: 'named',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
import { createRequire } from 'node:module';
import { resolve } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';

const nodeRequire = createRequire(import.meta.url);
const BUILD_CJS_DIR = resolve(__dirname, '../../build/cjs');

// The five entries share vendored chunks, and the require cache would keep a chunk's module scope
// from running again after the first test. Drop everything under `build/cjs` first, so each test
// really executes the code it claims to.
function requireFresh(entry: string): unknown {
for (const key of Object.keys(nodeRequire.cache)) {
if (key.startsWith(BUILD_CJS_DIR)) {
Reflect.deleteProperty(nodeRequire.cache, key);
}
}
return nodeRequire(resolve(BUILD_CJS_DIR, 'orchestrion/bundler', `${entry}.js`));
}

/**
* The bundler entries must load in Node even when a `document` global exists, which is the case
* under jsdom/happy-dom: the vendored code must never treat `document` as proof of a browser.
* Runs against `build/cjs` because that guard lives in the emitted code, not the sources.
* Reference Issue: https://github.com/getsentry/sentry-javascript/issues/23789
*/
describe('built CJS bundler entries load under DOM test environments', () => {
afterEach(() => {
delete (globalThis as { document?: unknown }).document;
});

it.each(['webpack', 'webpack-loader', 'esbuild', 'vite', 'rollup'])(
'build/cjs/orchestrion/bundler/%s.js loads while a `document` global is defined',
entry => {
(globalThis as { document?: unknown }).document = { baseURI: 'http://localhost:3000/' };
expect(() => requireFresh(entry)).not.toThrow();
},
);
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/nextjs/src/config/diagnosticsChannelInjection.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { resolveOrchestrionRuntimeRequest } from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from './loadOrchestrionBundler';

/**
* Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own
Expand DownExpand Up@@ -58,6 +58,6 @@ export async function externalizeOrchestrionRuntimePackages({
return undefined;
}

const resolved = resolveOrchestrionRuntimeRequest(request);
const resolved = loadOrchestrionBundler().resolveOrchestrionRuntimeRequest(request);
return resolved ? `commonjs ${resolved}` : undefined;
}
29 changes: 29 additions & 0 deletions packages/nextjs/src/config/loadOrchestrionBundler.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
import { createRequire } from 'module';
import type * as orchestrionBundler from '@sentry/server-utils/orchestrion/webpack';

type OrchestrionBundlerModule = typeof orchestrionBundler;

// Use `createRequire` (never the CJS `require` alias) so bundlers don't emit a "Critical
// dependency" warning. Resolving from this file's own location keeps it working under pnpm
// isolated installations.
function getNodeRequire(): ReturnType<typeof createRequire> {
let nodeRequire: ReturnType<typeof createRequire>;
/*! rollup-include-cjs-only */
nodeRequire = createRequire(__filename);
/*! rollup-include-cjs-only-end */
/*! rollup-include-esm-only */
nodeRequire = createRequire(import.meta.url);
/*! rollup-include-esm-only-end */
return nodeRequire;
}

/**
* Loads `@sentry/server-utils/orchestrion/webpack` at call time instead of module scope. The
* runtime server entry re-exports `withSentryConfig`, so a static import would run the bundler
* plugins' module-scope side effects on every server-side SDK import (issues #23789, #22794).
* Synchronous because Next.js `webpack` config functions cannot be async. Node's require cache
* already returns the same module on repeated calls, so no memoization is needed.
*/
export function loadOrchestrionBundler(): OrchestrionBundlerModule {
return getNodeRequire()('@sentry/server-utils/orchestrion/webpack') as OrchestrionBundlerModule;
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import {
getOrchestrionLoaderPath,
getSentryInstrumentations,
serializeInstrumentations,
} from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from '../loadOrchestrionBundler';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type {
Expand DownExpand Up@@ -138,6 +134,8 @@ function maybeAddOrchestrionRule(
return rules;
}

const { getOrchestrionLoaderPath, getSentryInstrumentations, serializeInstrumentations } = loadOrchestrionBundler();

return safelyAddTurbopackRule(rules, {
matcher: '*.{js,mjs,cjs}',
rule: {
Expand Down
6 changes: 4 additions & 2 deletions packages/nextjs/src/config/webpack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@ import type {
WebpackEntryProperty,
WebpackPluginInstance,
} from './types';
import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from './loadOrchestrionBundler';
import { getNextjsVersion, getPackageModules } from './util';
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';

Expand DownExpand Up@@ -434,7 +434,9 @@ export function constructWebpackConfigFunction({

// Orchestrion code-transform loader — Node server runtime only, never the edge compilation
if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) {
newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as unknown as WebpackPluginInstance);
newConfig.plugins.push(
loadOrchestrionBundler().sentryOrchestrionWebpackPlugin() as unknown as WebpackPluginInstance,
);
prependOrchestrionRuntimeExternals(newConfig);
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import '../mocks';
import * as core from '@sentry/core';
import { describe, expect, it, vi } from 'vitest';
import * as getBuildPluginOptionsModule from '../../../src/config/getBuildPluginOptions';
import type * as loadOrchestrionBundlerModule from '../../../src/config/loadOrchestrionBundler';
import * as util from '../../../src/config/util';
import {
CLIENT_SDK_CONFIG_FILE,
Expand All@@ -16,12 +17,18 @@ import {
} from '../fixtures';
import { materializeFinalNextConfig, materializeFinalWebpackConfig } from '../testUtils';

// Only the plugin factory is stubbed — `resolveOrchestrionRuntimeRequest` must stay real because
// the externals handler under test uses it.
vi.mock('@sentry/server-utils/orchestrion/webpack', async importOriginal => ({
...(await importOriginal<Record<string, unknown>>()),
sentryOrchestrionWebpackPlugin: () => ({ _name: 'sentry-orchestrion-webpack-plugin' }),
}));
// Stub only the plugin factory. The externals handler under test needs the real
// `resolveOrchestrionRuntimeRequest`. The bundler module loads via native `require`, which
// `vi.mock` cannot intercept, so the stub goes on the loader.
vi.mock('../../../src/config/loadOrchestrionBundler', async importOriginal => {
const original = await importOriginal<typeof loadOrchestrionBundlerModule>();
return {
loadOrchestrionBundler: () => ({
...original.loadOrchestrionBundler(),
sentryOrchestrionWebpackPlugin: () => ({ _name: 'sentry-orchestrion-webpack-plugin' }),
}),
};
});

describe('constructWebpackConfigFunction()', () => {
it('includes expected properties', async () => {
Expand Down
32 changes: 32 additions & 0 deletions packages/nextjs/test/serverEntryBundlerGraph.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';

/**
* Importing the SDK server entry must not load the orchestrion bundler plugins. They are
* build-time-only, and their module-scope side effects break runtimes the build never sees,
* like jsdom/happy-dom test runs (issue #23789) and Cloudflare Workers cold starts (issue #22794).
* Runs in a child process for a clean module cache and real Node resolution.
*/
describe('built CJS server entry', () => {
const serverEntry = resolve(__dirname, '../build/cjs/index.server.js');

it('loads under a DOM test environment without pulling in the orchestrion bundler graph', () => {
const script = `
globalThis.document = { baseURI: 'http://localhost:3000/' };
require(${JSON.stringify(serverEntry)});
const toPosix = modulePath => modulePath.split(require('path').sep).join('/');
const bundlerModules = Object.keys(require.cache).map(toPosix).filter(
modulePath => modulePath.includes('code-transformer-bundler-plugins') || modulePath.includes('orchestrion/bundler'),
);
if (bundlerModules.length > 0) {
console.error('Bundler-plugin modules loaded at import time:\\n' + bundlerModules.join('\\n'));
process.exit(1);
}
`;

// On failure, stderr carries either the leaked module list or the import crash itself.
const result = spawnSync(process.execPath, ['-e', script], { encoding: 'utf8' });
expect(result.status, result.stderr).toBe(0);
});
});
15 changes: 14 additions & 1 deletion packages/server-utils/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,6 +53,19 @@ const debugNodeAlias = {
},
};

// This package only runs in Node, but rollup's default CJS replacement for `import.meta.url`
// picks browser behavior whenever a `document` global exists, and jsdom/happy-dom define
// `document` while tests run in Node. Always emit the unconditional Node form instead.
const importMetaUrlNodeShim = {
name: 'import-meta-url-node-shim',
resolveImportMeta(property, { format }) {
if (property === 'url' && format === 'cjs') {
return "require('node:url').pathToFileURL(__filename).href";
}
return null;
},
};

// Bundling files from the repo-root `node_modules` moves rollup's common source ancestor up to the
// repo root, so `preserveModules` names our own files `packages/server-utils/src/...` — strip that
// prefix to keep the `build/cjs/index.js` layout the `exports` map points at. And npm never packs
Expand DownExpand Up@@ -124,7 +137,7 @@ export default [
'src/orchestrion/bundler/esbuild.ts',
],
packageSpecificConfig: {
plugins: [debugNodeAlias, commonJSPlugin, thirdPartyLicensePlugin],
plugins: [debugNodeAlias, commonJSPlugin, importMetaUrlNodeShim, thirdPartyLicensePlugin],
output: {
// set exports to 'named' or 'auto' so that rollup doesn't warn
exports: 'named',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
import { createRequire } from 'node:module';
import { resolve } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';

const nodeRequire = createRequire(import.meta.url);
const BUILD_CJS_DIR = resolve(__dirname, '../../build/cjs');

// The five entries share vendored chunks, and the require cache would keep a chunk's module scope
// from running again after the first test. Drop everything under `build/cjs` first, so each test
// really executes the code it claims to.
function requireFresh(entry: string): unknown {
for (const key of Object.keys(nodeRequire.cache)) {
if (key.startsWith(BUILD_CJS_DIR)) {
Reflect.deleteProperty(nodeRequire.cache, key);
}
}
return nodeRequire(resolve(BUILD_CJS_DIR, 'orchestrion/bundler', `${entry}.js`));
}

/**
* The bundler entries must load in Node even when a `document` global exists, which is the case
* under jsdom/happy-dom: the vendored code must never treat `document` as proof of a browser.
* Runs against `build/cjs` because that guard lives in the emitted code, not the sources.
* Reference Issue: https://github.com/getsentry/sentry-javascript/issues/23789
*/
describe('built CJS bundler entries load under DOM test environments', () => {
afterEach(() => {
delete (globalThis as { document?: unknown }).document;
});

it.each(['webpack', 'webpack-loader', 'esbuild', 'vite', 'rollup'])(
'build/cjs/orchestrion/bundler/%s.js loads while a `document` global is defined',
entry => {
(globalThis as { document?: unknown }).document = { baseURI: 'http://localhost:3000/' };
expect(() => requireFresh(entry)).not.toThrow();
},
);
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/nextjs/src/config/diagnosticsChannelInjection.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { resolveOrchestrionRuntimeRequest } from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from './loadOrchestrionBundler';

/**
* Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own
Expand DownExpand Up@@ -58,6 +58,6 @@ export async function externalizeOrchestrionRuntimePackages({
return undefined;
}

const resolved = resolveOrchestrionRuntimeRequest(request);
const resolved = loadOrchestrionBundler().resolveOrchestrionRuntimeRequest(request);
return resolved ? `commonjs ${resolved}` : undefined;
}
29 changes: 29 additions & 0 deletions packages/nextjs/src/config/loadOrchestrionBundler.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
import { createRequire } from 'module';
import type * as orchestrionBundler from '@sentry/server-utils/orchestrion/webpack';

type OrchestrionBundlerModule = typeof orchestrionBundler;

// Use `createRequire` (never the CJS `require` alias) so bundlers don't emit a "Critical
// dependency" warning. Resolving from this file's own location keeps it working under pnpm
// isolated installations.
function getNodeRequire(): ReturnType<typeof createRequire> {
let nodeRequire: ReturnType<typeof createRequire>;
/*! rollup-include-cjs-only */
nodeRequire = createRequire(__filename);
/*! rollup-include-cjs-only-end */
/*! rollup-include-esm-only */
nodeRequire = createRequire(import.meta.url);
/*! rollup-include-esm-only-end */
return nodeRequire;
}

/**
* Loads `@sentry/server-utils/orchestrion/webpack` at call time instead of module scope. The
* runtime server entry re-exports `withSentryConfig`, so a static import would run the bundler
* plugins' module-scope side effects on every server-side SDK import (issues #23789, #22794).
* Synchronous because Next.js `webpack` config functions cannot be async. Node's require cache
* already returns the same module on repeated calls, so no memoization is needed.
*/
export function loadOrchestrionBundler(): OrchestrionBundlerModule {
return getNodeRequire()('@sentry/server-utils/orchestrion/webpack') as OrchestrionBundlerModule;
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import {
getOrchestrionLoaderPath,
getSentryInstrumentations,
serializeInstrumentations,
} from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from '../loadOrchestrionBundler';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type {
Expand DownExpand Up@@ -138,6 +134,8 @@ function maybeAddOrchestrionRule(
return rules;
}

const { getOrchestrionLoaderPath, getSentryInstrumentations, serializeInstrumentations } = loadOrchestrionBundler();

return safelyAddTurbopackRule(rules, {
matcher: '*.{js,mjs,cjs}',
rule: {
Expand Down
6 changes: 4 additions & 2 deletions packages/nextjs/src/config/webpack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@ import type {
WebpackEntryProperty,
WebpackPluginInstance,
} from './types';
import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from './loadOrchestrionBundler';
import { getNextjsVersion, getPackageModules } from './util';
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';

Expand DownExpand Up@@ -434,7 +434,9 @@ export function constructWebpackConfigFunction({

// Orchestrion code-transform loader — Node server runtime only, never the edge compilation
if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) {
newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as unknown as WebpackPluginInstance);
newConfig.plugins.push(
loadOrchestrionBundler().sentryOrchestrionWebpackPlugin() as unknown as WebpackPluginInstance,
);
prependOrchestrionRuntimeExternals(newConfig);
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import '../mocks';
import * as core from '@sentry/core';
import { describe, expect, it, vi } from 'vitest';
import * as getBuildPluginOptionsModule from '../../../src/config/getBuildPluginOptions';
import type * as loadOrchestrionBundlerModule from '../../../src/config/loadOrchestrionBundler';
import * as util from '../../../src/config/util';
import {
CLIENT_SDK_CONFIG_FILE,
Expand All@@ -16,12 +17,18 @@ import {
} from '../fixtures';
import { materializeFinalNextConfig, materializeFinalWebpackConfig } from '../testUtils';

// Only the plugin factory is stubbed — `resolveOrchestrionRuntimeRequest` must stay real because
// the externals handler under test uses it.
vi.mock('@sentry/server-utils/orchestrion/webpack', async importOriginal => ({
...(await importOriginal<Record<string, unknown>>()),
sentryOrchestrionWebpackPlugin: () => ({ _name: 'sentry-orchestrion-webpack-plugin' }),
}));
// Stub only the plugin factory. The externals handler under test needs the real
// `resolveOrchestrionRuntimeRequest`. The bundler module loads via native `require`, which
// `vi.mock` cannot intercept, so the stub goes on the loader.
vi.mock('../../../src/config/loadOrchestrionBundler', async importOriginal => {
const original = await importOriginal<typeof loadOrchestrionBundlerModule>();
return {
loadOrchestrionBundler: () => ({
...original.loadOrchestrionBundler(),
sentryOrchestrionWebpackPlugin: () => ({ _name: 'sentry-orchestrion-webpack-plugin' }),
}),
};
});

describe('constructWebpackConfigFunction()', () => {
it('includes expected properties', async () => {
Expand Down
32 changes: 32 additions & 0 deletions packages/nextjs/test/serverEntryBundlerGraph.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';

/**
* Importing the SDK server entry must not load the orchestrion bundler plugins. They are
* build-time-only, and their module-scope side effects break runtimes the build never sees,
* like jsdom/happy-dom test runs (issue #23789) and Cloudflare Workers cold starts (issue #22794).
* Runs in a child process for a clean module cache and real Node resolution.
*/
describe('built CJS server entry', () => {
const serverEntry = resolve(__dirname, '../build/cjs/index.server.js');

it('loads under a DOM test environment without pulling in the orchestrion bundler graph', () => {
const script = `
globalThis.document = { baseURI: 'http://localhost:3000/' };
require(${JSON.stringify(serverEntry)});
const toPosix = modulePath => modulePath.split(require('path').sep).join('/');
const bundlerModules = Object.keys(require.cache).map(toPosix).filter(
modulePath => modulePath.includes('code-transformer-bundler-plugins') || modulePath.includes('orchestrion/bundler'),
);
if (bundlerModules.length > 0) {
console.error('Bundler-plugin modules loaded at import time:\\n' + bundlerModules.join('\\n'));
process.exit(1);
}
`;

// On failure, stderr carries either the leaked module list or the import crash itself.
const result = spawnSync(process.execPath, ['-e', script], { encoding: 'utf8' });
expect(result.status, result.stderr).toBe(0);
});
});
15 changes: 14 additions & 1 deletion packages/server-utils/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,6 +53,19 @@ const debugNodeAlias = {
},
};

// This package only runs in Node, but rollup's default CJS replacement for `import.meta.url`
// picks browser behavior whenever a `document` global exists, and jsdom/happy-dom define
// `document` while tests run in Node. Always emit the unconditional Node form instead.
const importMetaUrlNodeShim = {
name: 'import-meta-url-node-shim',
resolveImportMeta(property, { format }) {
if (property === 'url' && format === 'cjs') {
return "require('node:url').pathToFileURL(__filename).href";
}
return null;
},
};

// Bundling files from the repo-root `node_modules` moves rollup's common source ancestor up to the
// repo root, so `preserveModules` names our own files `packages/server-utils/src/...` — strip that
// prefix to keep the `build/cjs/index.js` layout the `exports` map points at. And npm never packs
Expand DownExpand Up@@ -124,7 +137,7 @@ export default [
'src/orchestrion/bundler/esbuild.ts',
],
packageSpecificConfig: {
plugins: [debugNodeAlias, commonJSPlugin, thirdPartyLicensePlugin],
plugins: [debugNodeAlias, commonJSPlugin, importMetaUrlNodeShim, thirdPartyLicensePlugin],
output: {
// set exports to 'named' or 'auto' so that rollup doesn't warn
exports: 'named',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
import { createRequire } from 'node:module';
import { resolve } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';

const nodeRequire = createRequire(import.meta.url);
const BUILD_CJS_DIR = resolve(__dirname, '../../build/cjs');

// The five entries share vendored chunks, and the require cache would keep a chunk's module scope
// from running again after the first test. Drop everything under `build/cjs` first, so each test
// really executes the code it claims to.
function requireFresh(entry: string): unknown {
for (const key of Object.keys(nodeRequire.cache)) {
if (key.startsWith(BUILD_CJS_DIR)) {
Reflect.deleteProperty(nodeRequire.cache, key);
}
}
return nodeRequire(resolve(BUILD_CJS_DIR, 'orchestrion/bundler', `${entry}.js`));
}

/**
* The bundler entries must load in Node even when a `document` global exists, which is the case
* under jsdom/happy-dom: the vendored code must never treat `document` as proof of a browser.
* Runs against `build/cjs` because that guard lives in the emitted code, not the sources.
* Reference Issue: https://github.com/getsentry/sentry-javascript/issues/23789
*/
describe('built CJS bundler entries load under DOM test environments', () => {
afterEach(() => {
delete (globalThis as { document?: unknown }).document;
});

it.each(['webpack', 'webpack-loader', 'esbuild', 'vite', 'rollup'])(
'build/cjs/orchestrion/bundler/%s.js loads while a `document` global is defined',
entry => {
(globalThis as { document?: unknown }).document = { baseURI: 'http://localhost:3000/' };
expect(() => requireFresh(entry)).not.toThrow();
},
);
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/nextjs/src/config/diagnosticsChannelInjection.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import { resolveOrchestrionRuntimeRequest } from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from './loadOrchestrionBundler';

/**
* Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own
Expand DownExpand Up@@ -58,6 +58,6 @@ export async function externalizeOrchestrionRuntimePackages({
return undefined;
}

const resolved = resolveOrchestrionRuntimeRequest(request);
const resolved = loadOrchestrionBundler().resolveOrchestrionRuntimeRequest(request);
return resolved ? `commonjs ${resolved}` : undefined;
}
29 changes: 29 additions & 0 deletions packages/nextjs/src/config/loadOrchestrionBundler.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
import { createRequire } from 'module';
import type * as orchestrionBundler from '@sentry/server-utils/orchestrion/webpack';

type OrchestrionBundlerModule = typeof orchestrionBundler;

// Use `createRequire` (never the CJS `require` alias) so bundlers don't emit a "Critical
// dependency" warning. Resolving from this file's own location keeps it working under pnpm
// isolated installations.
function getNodeRequire(): ReturnType<typeof createRequire> {
let nodeRequire: ReturnType<typeof createRequire>;
/*! rollup-include-cjs-only */
nodeRequire = createRequire(__filename);
/*! rollup-include-cjs-only-end */
/*! rollup-include-esm-only */
nodeRequire = createRequire(import.meta.url);
/*! rollup-include-esm-only-end */
return nodeRequire;
}

/**
* Loads `@sentry/server-utils/orchestrion/webpack` at call time instead of module scope. The
* runtime server entry re-exports `withSentryConfig`, so a static import would run the bundler
* plugins' module-scope side effects on every server-side SDK import (issues #23789, #22794).
* Synchronous because Next.js `webpack` config functions cannot be async. Node's require cache
* already returns the same module on repeated calls, so no memoization is needed.
*/
export function loadOrchestrionBundler(): OrchestrionBundlerModule {
return getNodeRequire()('@sentry/server-utils/orchestrion/webpack') as OrchestrionBundlerModule;
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
import { debug } from '@sentry/core';
import * as path from 'path';
import {
getOrchestrionLoaderPath,
getSentryInstrumentations,
serializeInstrumentations,
} from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from '../loadOrchestrionBundler';
import type { VercelCronsConfig } from '../../common/types';
import type { RouteManifest } from '../manifest/types';
import type {
Expand DownExpand Up@@ -138,6 +134,8 @@ function maybeAddOrchestrionRule(
return rules;
}

const { getOrchestrionLoaderPath, getSentryInstrumentations, serializeInstrumentations } = loadOrchestrionBundler();

return safelyAddTurbopackRule(rules, {
matcher: '*.{js,mjs,cjs}',
rule: {
Expand Down
6 changes: 4 additions & 2 deletions packages/nextjs/src/config/webpack.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,7 @@ import type {
WebpackEntryProperty,
WebpackPluginInstance,
} from './types';
import { sentryOrchestrionWebpackPlugin } from '@sentry/server-utils/orchestrion/webpack';
import { loadOrchestrionBundler } from './loadOrchestrionBundler';
import { getNextjsVersion, getPackageModules } from './util';
import type { VercelCronsConfigResult } from './withSentryConfig/getFinalConfigObjectUtils';

Expand DownExpand Up@@ -434,7 +434,9 @@ export function constructWebpackConfigFunction({

// Orchestrion code-transform loader — Node server runtime only, never the edge compilation
if (runtime === 'server' && userSentryOptions._experimental?.useDiagnosticsChannelInjection) {
newConfig.plugins.push(sentryOrchestrionWebpackPlugin() as unknown as WebpackPluginInstance);
newConfig.plugins.push(
loadOrchestrionBundler().sentryOrchestrionWebpackPlugin() as unknown as WebpackPluginInstance,
);
prependOrchestrionRuntimeExternals(newConfig);
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,7 @@ import '../mocks';
import * as core from '@sentry/core';
import { describe, expect, it, vi } from 'vitest';
import * as getBuildPluginOptionsModule from '../../../src/config/getBuildPluginOptions';
import type * as loadOrchestrionBundlerModule from '../../../src/config/loadOrchestrionBundler';
import * as util from '../../../src/config/util';
import {
CLIENT_SDK_CONFIG_FILE,
Expand All@@ -16,12 +17,18 @@ import {
} from '../fixtures';
import { materializeFinalNextConfig, materializeFinalWebpackConfig } from '../testUtils';

// Only the plugin factory is stubbed — `resolveOrchestrionRuntimeRequest` must stay real because
// the externals handler under test uses it.
vi.mock('@sentry/server-utils/orchestrion/webpack', async importOriginal => ({
...(await importOriginal<Record<string, unknown>>()),
sentryOrchestrionWebpackPlugin: () => ({ _name: 'sentry-orchestrion-webpack-plugin' }),
}));
// Stub only the plugin factory. The externals handler under test needs the real
// `resolveOrchestrionRuntimeRequest`. The bundler module loads via native `require`, which
// `vi.mock` cannot intercept, so the stub goes on the loader.
vi.mock('../../../src/config/loadOrchestrionBundler', async importOriginal => {
const original = await importOriginal<typeof loadOrchestrionBundlerModule>();
return {
loadOrchestrionBundler: () => ({
...original.loadOrchestrionBundler(),
sentryOrchestrionWebpackPlugin: () => ({ _name: 'sentry-orchestrion-webpack-plugin' }),
}),
};
});

describe('constructWebpackConfigFunction()', () => {
it('includes expected properties', async () => {
Expand Down
32 changes: 32 additions & 0 deletions packages/nextjs/test/serverEntryBundlerGraph.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
import { spawnSync } from 'node:child_process';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';

/**
* Importing the SDK server entry must not load the orchestrion bundler plugins. They are
* build-time-only, and their module-scope side effects break runtimes the build never sees,
* like jsdom/happy-dom test runs (issue #23789) and Cloudflare Workers cold starts (issue #22794).
* Runs in a child process for a clean module cache and real Node resolution.
*/
describe('built CJS server entry', () => {
const serverEntry = resolve(__dirname, '../build/cjs/index.server.js');

it('loads under a DOM test environment without pulling in the orchestrion bundler graph', () => {
const script = `
globalThis.document = { baseURI: 'http://localhost:3000/' };
require(${JSON.stringify(serverEntry)});
const toPosix = modulePath => modulePath.split(require('path').sep).join('/');
const bundlerModules = Object.keys(require.cache).map(toPosix).filter(
modulePath => modulePath.includes('code-transformer-bundler-plugins') || modulePath.includes('orchestrion/bundler'),
);
if (bundlerModules.length > 0) {
console.error('Bundler-plugin modules loaded at import time:\\n' + bundlerModules.join('\\n'));
process.exit(1);
}
`;

// On failure, stderr carries either the leaked module list or the import crash itself.
const result = spawnSync(process.execPath, ['-e', script], { encoding: 'utf8' });
expect(result.status, result.stderr).toBe(0);
});
});
15 changes: 14 additions & 1 deletion packages/server-utils/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,6 +53,19 @@ const debugNodeAlias = {
},
};

// This package only runs in Node, but rollup's default CJS replacement for `import.meta.url`
// picks browser behavior whenever a `document` global exists, and jsdom/happy-dom define
// `document` while tests run in Node. Always emit the unconditional Node form instead.
const importMetaUrlNodeShim = {
name: 'import-meta-url-node-shim',
resolveImportMeta(property, { format }) {
if (property === 'url' && format === 'cjs') {
return "require('node:url').pathToFileURL(__filename).href";
}
return null;
},
};

// Bundling files from the repo-root `node_modules` moves rollup's common source ancestor up to the
// repo root, so `preserveModules` names our own files `packages/server-utils/src/...` — strip that
// prefix to keep the `build/cjs/index.js` layout the `exports` map points at. And npm never packs
Expand DownExpand Up@@ -124,7 +137,7 @@ export default [
'src/orchestrion/bundler/esbuild.ts',
],
packageSpecificConfig: {
plugins: [debugNodeAlias, commonJSPlugin, thirdPartyLicensePlugin],
plugins: [debugNodeAlias, commonJSPlugin, importMetaUrlNodeShim, thirdPartyLicensePlugin],
output: {
// set exports to 'named' or 'auto' so that rollup doesn't warn
exports: 'named',
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
import { createRequire } from 'node:module';
import { resolve } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';

const nodeRequire = createRequire(import.meta.url);
const BUILD_CJS_DIR = resolve(__dirname, '../../build/cjs');

// The five entries share vendored chunks, and the require cache would keep a chunk's module scope
// from running again after the first test. Drop everything under `build/cjs` first, so each test
// really executes the code it claims to.
function requireFresh(entry: string): unknown {
for (const key of Object.keys(nodeRequire.cache)) {
if (key.startsWith(BUILD_CJS_DIR)) {
Reflect.deleteProperty(nodeRequire.cache, key);
}
}
return nodeRequire(resolve(BUILD_CJS_DIR, 'orchestrion/bundler', `${entry}.js`));
}

/**
* The bundler entries must load in Node even when a `document` global exists, which is the case
* under jsdom/happy-dom: the vendored code must never treat `document` as proof of a browser.
* Runs against `build/cjs` because that guard lives in the emitted code, not the sources.
* Reference Issue: https://github.com/getsentry/sentry-javascript/issues/23789
*/
describe('built CJS bundler entries load under DOM test environments', () => {
afterEach(() => {
delete (globalThis as { document?: unknown }).document;
});

it.each(['webpack', 'webpack-loader', 'esbuild', 'vite', 'rollup'])(
'build/cjs/orchestrion/bundler/%s.js loads while a `document` global is defined',
entry => {
(globalThis as { document?: unknown }).document = { baseURI: 'http://localhost:3000/' };
expect(() => requireFresh(entry)).not.toThrow();
},
);
});
Loading