Skip to content

Commit fc910dc

Browse files
Andaristcodex
andcommitted
fix(bundler-plugins): Inject into Webpack child compilers
Co-Authored-By: OpenAI Codex <codex@openai.com>
1 parent 4485fc1 commit fc910dc

3 files changed

Lines changed: 123 additions & 88 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { randomUUID } from 'node:crypto';
2+
import { getDebugIdSnippet, stringToUUID } from '../core/index';
3+
import type { CodeInjection } from '../core/index';
4+
import { getCodeInjectionPosition } from '../core/get-code-injection-position';
5+
6+
type WebpackSource = {
7+
source: () => string | Uint8Array;
8+
};
9+
10+
type WebpackReplaceSource = WebpackSource & {
11+
insert: (position: number, value: string) => void;
12+
};
13+
14+
type WebpackCompilation = {
15+
chunks: Iterable<{
16+
files: Iterable<string>;
17+
hash?: string;
18+
contentHash?: { javascript?: string };
19+
}>;
20+
hooks: {
21+
processAssets: {
22+
tap: (
23+
options: { name: string; stage: number },
24+
callback: (assets: Record<string, WebpackSource>) => void,
25+
) => void;
26+
};
27+
};
28+
updateAsset: (name: string, source: WebpackSource) => void;
29+
};
30+
31+
export type WebpackCodeInjectionCompiler = {
32+
hooks: {
33+
compilation: {
34+
tap: (name: string, callback: (compilation: WebpackCompilation) => void) => void;
35+
};
36+
};
37+
webpack?: {
38+
Compilation?: { PROCESS_ASSETS_STAGE_ADDITIONS: number };
39+
sources?: { ReplaceSource: new (source: WebpackSource) => WebpackReplaceSource };
40+
};
41+
};
42+
43+
type WebpackApi = {
44+
Compilation?: { PROCESS_ASSETS_STAGE_ADDITIONS: number };
45+
sources?: { ReplaceSource: new (source: WebpackSource) => WebpackReplaceSource };
46+
};
47+
48+
const WEBPACK_JAVASCRIPT_ASSET_REGEX = /\.(?:js|ts|jsx|tsx|mjs|cjs|mts|cts)(?:\?[^?]*)?(?:#[^#]*)?$/;
49+
50+
export function addCodeInjection(
51+
compiler: WebpackCodeInjectionCompiler,
52+
webpackApi: WebpackApi,
53+
staticInjectionCode: CodeInjection,
54+
sourcemapsEnabled: boolean,
55+
logger: { warn: (message: string) => void },
56+
): void {
57+
if (staticInjectionCode.isEmpty() && !sourcemapsEnabled) {
58+
return;
59+
}
60+
61+
const ReplaceSource = compiler.webpack?.sources?.ReplaceSource || webpackApi.sources?.ReplaceSource;
62+
const processAssetsStage =
63+
compiler.webpack?.Compilation?.PROCESS_ASSETS_STAGE_ADDITIONS ??
64+
webpackApi.Compilation?.PROCESS_ASSETS_STAGE_ADDITIONS;
65+
66+
if (!ReplaceSource || processAssetsStage === undefined) {
67+
logger.warn(
68+
'Webpack sources are not available. Skipping code injection. This usually means webpack is not properly configured.',
69+
);
70+
return;
71+
}
72+
73+
compiler.hooks.compilation.tap('sentry-webpack-plugin-injection', compilation => {
74+
compilation.hooks.processAssets.tap(
75+
{
76+
name: 'sentry-webpack-plugin-injection',
77+
stage: processAssetsStage,
78+
},
79+
assets => {
80+
for (const chunk of compilation.chunks) {
81+
for (const assetName of chunk.files) {
82+
if (!WEBPACK_JAVASCRIPT_ASSET_REGEX.test(assetName)) {
83+
continue;
84+
}
85+
86+
const source = assets[assetName];
87+
if (!source) {
88+
continue;
89+
}
90+
91+
const sourceContents = source.source();
92+
const codeString =
93+
typeof sourceContents === 'string' ? sourceContents : Buffer.from(sourceContents).toString();
94+
const codeToInject = staticInjectionCode.clone();
95+
if (sourcemapsEnabled) {
96+
const hash = chunk.contentHash?.javascript ?? chunk.hash;
97+
codeToInject.append(getDebugIdSnippet(hash ? stringToUUID(hash) : randomUUID()));
98+
}
99+
100+
const injectionPosition = getCodeInjectionPosition(codeString);
101+
const injection =
102+
injectionPosition === codeString.length ? `\n${codeToInject.code()}` : `${codeToInject.code()}\n`;
103+
const updatedSource = new ReplaceSource(source);
104+
updatedSource.insert(injectionPosition, injection);
105+
compilation.updateAsset(assetName, updatedSource);
106+
}
107+
}
108+
},
109+
);
110+
});
111+
}

packages/bundler-plugins/src/webpack/webpack4and5.ts

Lines changed: 11 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,17 @@ import {
33
createSentryBuildPluginManager,
44
generateReleaseInjectorCode,
55
generateModuleMetadataInjectorCode,
6-
stringToUUID,
76
createComponentNameAnnotateHooks,
87
CodeInjection,
9-
getDebugIdSnippet,
108
createDebugIdUploadFunction,
119
isJsFile,
1210
stampDebugId,
1311
} from '../core/index';
14-
import { getCodeInjectionPosition } from '../core/get-code-injection-position';
12+
import { addCodeInjection } from './webpack-code-injection';
13+
import type { WebpackCodeInjectionCompiler } from './webpack-code-injection';
1514
import * as path from 'node:path';
1615
import { fileURLToPath } from 'node:url';
1716
import { createRequire } from 'node:module';
18-
import { randomUUID } from 'node:crypto';
1917

2018
const _req = createRequire(import.meta.url);
2119

@@ -54,37 +52,13 @@ type WebpackSources = {
5452
RawSource?: WebpackRawSource;
5553
};
5654

57-
type WebpackModule = {
58-
resource?: string;
59-
};
60-
61-
type WebpackLoaderCallback = (err: Error | null, content?: string, sourceMap?: unknown) => void;
62-
63-
type WebpackLoaderContext = {
64-
callback: WebpackLoaderCallback;
65-
};
66-
6755
type WebpackCompilationContext = {
6856
chunks: Iterable<{
6957
files: Iterable<string>;
7058
hash?: string;
7159
contentHash?: { javascript?: string };
7260
}>;
73-
compiler: {
74-
webpack?: {
75-
NormalModule?: {
76-
getCompilationHooks: (compilation: WebpackCompilationContext) => {
77-
loader: {
78-
tap: (name: string, callback: (loaderContext: WebpackLoaderContext, module: WebpackModule) => void) => void;
79-
};
80-
};
81-
};
82-
};
83-
};
8461
hooks: {
85-
normalModuleLoader?: {
86-
tap: (name: string, callback: (loaderContext: WebpackLoaderContext, module: WebpackModule) => void) => void;
87-
};
8862
processAssets: {
8963
tap: (
9064
options: { name: string; stage: number },
@@ -113,13 +87,9 @@ type WebpackRawSource = {
11387
new (source: string): WebpackSource;
11488
};
11589

116-
type WebpackReplaceSource = WebpackSource & {
117-
insert: (position: number, value: string) => void;
118-
};
119-
120-
const WEBPACK_JAVASCRIPT_ASSET_REGEX = /\.(?:js|ts|jsx|tsx|mjs|cjs|mts|cts)(?:\?[^?]*)?(?:#[^#]*)?$/;
90+
type WebpackReplaceSource = WebpackSource & { insert: (position: number, value: string) => void };
12191

122-
type WebpackCompiler = {
92+
type WebpackCompiler = WebpackCodeInjectionCompiler & {
12393
options: {
12494
plugins?: unknown[];
12595
mode?: string;
@@ -282,59 +252,13 @@ export function sentryWebpackPluginFactory({
282252
const DefinePlugin = compiler?.webpack?.DefinePlugin || UnsafeDefinePlugin;
283253

284254
// Injecting through BannerPlugin would place executable code before directive prologues.
285-
if (!staticInjectionCode.isEmpty() || sourcemapsEnabled) {
286-
const ReplaceSource = compiler.webpack?.sources?.ReplaceSource || unsafeSources?.ReplaceSource;
287-
const processAssetsStage =
288-
compiler.webpack?.Compilation?.PROCESS_ASSETS_STAGE_ADDITIONS ??
289-
UnsafeCompilation?.PROCESS_ASSETS_STAGE_ADDITIONS;
290-
291-
if (!ReplaceSource || processAssetsStage === undefined) {
292-
logger.warn(
293-
'Webpack sources are not available. Skipping code injection. This usually means webpack is not properly configured.',
294-
);
295-
} else {
296-
compiler.hooks.thisCompilation.tap('sentry-webpack-plugin-injection', compilation => {
297-
compilation.hooks.processAssets.tap(
298-
{
299-
name: 'sentry-webpack-plugin-injection',
300-
stage: processAssetsStage,
301-
},
302-
assets => {
303-
for (const chunk of compilation.chunks) {
304-
for (const assetName of chunk.files) {
305-
if (!WEBPACK_JAVASCRIPT_ASSET_REGEX.test(assetName)) {
306-
continue;
307-
}
308-
309-
const source = assets[assetName];
310-
if (!source) {
311-
continue;
312-
}
313-
314-
const sourceContents = source.source();
315-
const codeString =
316-
typeof sourceContents === 'string' ? sourceContents : Buffer.from(sourceContents).toString();
317-
const codeToInject = staticInjectionCode.clone();
318-
if (sourcemapsEnabled) {
319-
const hash = chunk.contentHash?.javascript ?? chunk.hash;
320-
codeToInject.append(getDebugIdSnippet(hash ? stringToUUID(hash) : randomUUID()));
321-
}
322-
323-
const injectionPosition = getCodeInjectionPosition(codeString);
324-
const injection =
325-
injectionPosition === codeString.length
326-
? `\n${codeToInject.code()}`
327-
: `${codeToInject.code()}\n`;
328-
const updatedSource = new ReplaceSource(source);
329-
updatedSource.insert(injectionPosition, injection);
330-
compilation.updateAsset(assetName, updatedSource);
331-
}
332-
}
333-
},
334-
);
335-
});
336-
}
337-
}
255+
addCodeInjection(
256+
compiler,
257+
{ Compilation: UnsafeCompilation, sources: unsafeSources },
258+
staticInjectionCode,
259+
sourcemapsEnabled,
260+
logger,
261+
);
338262

339263
// The upload routine (which stamps debug IDs into temp copies of the artifacts) is skipped
340264
// with `disable-upload`, so the emitted artifacts get stamped in the asset pipeline instead.

packages/bundler-plugins/test/webpack/webpack4and5.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function runWebpackSourceInjection(
2424
sources: { ReplaceSource: webpack.sources.ReplaceSource },
2525
},
2626
hooks: {
27-
thisCompilation: {
27+
compilation: {
2828
tap: (_name: string, callback: (compilation: unknown) => void) => {
2929
compilationCallback = callback;
3030
},

0 commit comments

Comments
 (0)