Skip to content

Commit 1ba8963

Browse files
mydeaclaude
andcommitted
fix(nextjs): Seed Turbopack orchestrion marker merge-safely
The Turbopack value-injection seeded `__SENTRY_ORCHESTRION__` by assigning a fresh `{ bundler: [] }` object, replacing any existing marker. Unlike the Bun banner and `buildInjectPrologue`, this was not merge-safe, so a hybrid runtime+bundler setup could wipe already-recorded runtime module names before integrations read them. The value-injection loader can only emit plain assignments, so it now accepts an optional raw `prefixCode` and the marker is injected as a merge-safe snippet that only creates the global/`bundler` array when absent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent dcccddd commit 1ba8963

4 files changed

Lines changed: 73 additions & 21 deletions

File tree

‎packages/nextjs/src/config/loaders/valueInjectionLoader.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import type { LoaderThis } from './types';
22

33
export type ValueInjectionLoaderOptions = {
44
values: Record<string, unknown>;
5+
// Raw JS prepended before the value assignments. Unlike `values` (plain `globalThis[key] = ...`
6+
// assignments), this is emitted verbatim, so it can run merge-safe logic that a plain assignment
7+
// can't express (e.g. seeding a global without clobbering an existing one).
8+
prefixCode?: string;
59
};
610

711
/**
@@ -154,7 +158,7 @@ function findDirectiveTerminator(userCode: string, startIndex: number): number |
154158
*/
155159
export default function valueInjectionLoader(this: LoaderThis<ValueInjectionLoaderOptions>, userCode: string): string {
156160
// We know one or the other will be defined, depending on the version of webpack being used
157-
const { values } = 'getOptions' in this ? this.getOptions() : this.query;
161+
const { values, prefixCode } = 'getOptions' in this ? this.getOptions() : this.query;
158162

159163
// We do not want to cache injected values across builds
160164
this.cacheable(false);
@@ -163,6 +167,7 @@ export default function valueInjectionLoader(this: LoaderThis<ValueInjectionLoad
163167
const injectedCode =
164168
// eslint-disable-next-line prefer-template
165169
';' +
170+
(prefixCode ?? '') +
166171
Object.entries(values)
167172
.map(([key, value]) => `globalThis["${key}"] = ${JSON.stringify(value)};`)
168173
.join('');

‎packages/nextjs/src/config/turbopack/generateValueInjectionRules.ts‎

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ import type { RouteManifest } from '../manifest/types';
44
import type { JSONValue, TurbopackMatcherWithRule } from '../types';
55
import { getPackageModules, supportsTurbopackRuleCondition } from '../util';
66

7+
// Marks the bundler (build-time) orchestrion injection as active at boot, without clobbering any
8+
// marker an earlier injector already set. It only creates `__SENTRY_ORCHESTRION__`/`.bundler` if
9+
// absent, so in a hybrid setup the runtime hook's `runtime` list (and any module already recorded by
10+
// the per-module loader prologue) survives. Mirrors the merge-safe pattern of the Bun banner and
11+
// `buildInjectPrologue`. Emitted as a single line so it never shifts the module's source-map mappings.
12+
const ORCHESTRION_BUNDLER_MARKER =
13+
';(function(){try{var g=(globalThis.__SENTRY_ORCHESTRION__=globalThis.__SENTRY_ORCHESTRION__||{});g.bundler=g.bundler||[];}catch(e){}})();';
14+
715
/**
816
* Generate the value injection rules for client and server in turbopack config.
917
*/
@@ -47,15 +55,6 @@ export function generateValueInjectionRules({
4755
// Use process.cwd() to get the project directory at build time
4856
serverValues.__SENTRY_SERVER_MODULES__ = getPackageModules(process.cwd());
4957

50-
// Mark the bundler (build-time) orchestrion injection as active at boot. This runs at the top of the
51-
// server `instrumentation` file — before `Sentry.init()` and before the runtime hook — so
52-
// `isOrchestrionInjected()` is reliable for bundler-only setups too. Turbopack has no plugin/boot
53-
// hook to emit the full transformed-module list the way the webpack plugin does, so we seed an empty
54-
// array here; each transformed module then appends itself as it loads (see the orchestrion loader).
55-
if (injectOrchestrionBundlerMarker) {
56-
serverValues.__SENTRY_ORCHESTRION__ = { bundler: [] };
57-
}
58-
5958
if (Object.keys(isomorphicValues).length > 0) {
6059
clientValues = { ...clientValues, ...isomorphicValues };
6160
serverValues = { ...serverValues, ...isomorphicValues };
@@ -96,6 +95,11 @@ export function generateValueInjectionRules({
9695
loader: path.resolve(__dirname, '..', 'loaders', 'valueInjectionLoader.js'),
9796
options: {
9897
values: serverValues,
98+
// Runs at the top of the server `instrumentation` file — before `Sentry.init()` — so
99+
// `isOrchestrionInjected()` is reliable for bundler-only setups too. Turbopack has no
100+
// plugin/boot hook to emit the full transformed-module list the way the webpack plugin
101+
// does; each transformed module appends itself as it loads (see the orchestrion loader).
102+
...(injectOrchestrionBundlerMarker ? { prefixCode: ORCHESTRION_BUNDLER_MARKER } : {}),
99103
},
100104
},
101105
],

‎packages/nextjs/test/config/turbopack/generateValueInjectionRules.test.ts‎

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,22 +71,31 @@ describe('generateValueInjectionRules', () => {
7171
});
7272

7373
describe('orchestrion bundler marker', () => {
74+
function getServerLoaderOptions(result: ReturnType<typeof generateValueInjectionRules>): {
75+
values: Record<string, unknown>;
76+
prefixCode?: string;
77+
} {
78+
const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
79+
return (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown>; prefixCode?: string } }] })
80+
.loaders[0].options;
81+
}
82+
7483
it('does not inject the orchestrion marker by default', () => {
75-
const result = generateValueInjectionRules({});
84+
const options = getServerLoaderOptions(generateValueInjectionRules({}));
7685

77-
const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
78-
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
79-
.options.values;
80-
expect(values).not.toHaveProperty('__SENTRY_ORCHESTRION__');
86+
expect(options.values).not.toHaveProperty('__SENTRY_ORCHESTRION__');
87+
expect(options).not.toHaveProperty('prefixCode');
8188
});
8289

83-
it('seeds an empty bundler array at boot when injectOrchestrionBundlerMarker is set', () => {
84-
const result = generateValueInjectionRules({ injectOrchestrionBundlerMarker: true });
90+
it('seeds the bundler marker merge-safely at boot when injectOrchestrionBundlerMarker is set', () => {
91+
const options = getServerLoaderOptions(generateValueInjectionRules({ injectOrchestrionBundlerMarker: true }));
8592

86-
const serverRule = result.find(rule => rule.matcher === '**/instrumentation.*');
87-
const values = (serverRule?.rule as { loaders: [{ options: { values: Record<string, unknown> } }] }).loaders[0]
88-
.options.values;
89-
expect(values.__SENTRY_ORCHESTRION__).toEqual({ bundler: [] });
93+
// The marker must not be a plain assignment (which would clobber an existing marker), so it is
94+
// never emitted as a `values` entry.
95+
expect(options.values).not.toHaveProperty('__SENTRY_ORCHESTRION__');
96+
// It only creates the global if absent, preserving a runtime injector's `runtime` list.
97+
expect(options.prefixCode).toContain('globalThis.__SENTRY_ORCHESTRION__=globalThis.__SENTRY_ORCHESTRION__||{}');
98+
expect(options.prefixCode).toContain('g.bundler=g.bundler||[]');
9099
});
91100
});
92101

‎packages/nextjs/test/config/valueInjectionLoader.test.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,40 @@ describe.each([[clientConfigLoaderThis], [instrumentationLoaderThis]])('valueInj
219219
});
220220
});
221221

222+
describe('valueInjectionLoader prefixCode', () => {
223+
const prefixLoaderThis = {
224+
...defaultLoaderThis,
225+
resourcePath: './instrumentation.js',
226+
getOptions() {
227+
return {
228+
values: { foo: 'bar' },
229+
prefixCode: ';globalThis.__MARKER__ = globalThis.__MARKER__ || {};',
230+
};
231+
},
232+
} satisfies LoaderThis<ValueInjectionLoaderOptions>;
233+
234+
it('emits prefixCode verbatim before the value assignments', () => {
235+
const userCode = "import * as Sentry from '@sentry/nextjs';\nSentry.init();";
236+
237+
const result = valueInjectionLoader.call(prefixLoaderThis, userCode);
238+
239+
const prefixIndex = result.indexOf('globalThis.__MARKER__ = globalThis.__MARKER__ || {};');
240+
const valueIndex = result.indexOf('globalThis["foo"] = "bar";');
241+
242+
expect(prefixIndex).toBeGreaterThanOrEqual(0);
243+
expect(valueIndex).toBeGreaterThan(prefixIndex);
244+
});
245+
246+
it('does not emit any prefix when prefixCode is omitted', () => {
247+
const userCode = "import * as Sentry from '@sentry/nextjs';\nSentry.init();";
248+
249+
const result = valueInjectionLoader.call(clientConfigLoaderThis, userCode);
250+
251+
expect(result).toContain(';globalThis["foo"] = "bar";');
252+
expect(result).not.toContain('__MARKER__');
253+
});
254+
});
255+
222256
describe('findInjectionIndexAfterDirectives', () => {
223257
it('returns the position immediately after the last directive', () => {
224258
const userCode = '"use strict";\n"use client";\nimport React from \'react\';';

0 commit comments

Comments
 (0)