|
4 | 4 | // needs its own entrypoint/subpath rather than being reachable from another module. |
5 | 5 | import codeTransformerLoaderImpl from '@apm-js-collab/code-transformer-bundler-plugins/webpack-loader'; |
6 | 6 |
|
7 | | -// Explicitly typed so the emitted declaration doesn't reference the bundled devDependency. |
8 | | -// (Nothing imports this subpath from TS — bundlers load it by file path — so the loose |
9 | | -// signature is never consumed.) |
10 | | -const codeTransformerLoader: (this: unknown, code: string, inputSourceMap?: unknown) => void = |
11 | | - codeTransformerLoaderImpl; |
| 7 | +// The loader context we rely on beyond the transform itself: `resourcePath` to |
| 8 | +// name the module, `async` for the transformed result, and `_compilation` to |
| 9 | +// tell webpack (present) from Turbopack (absent). |
| 10 | +interface LoaderContext { |
| 11 | + resourcePath: string; |
| 12 | + async: () => (error: unknown, code?: string, map?: unknown) => void; |
| 13 | + _compilation?: unknown; |
| 14 | +} |
| 15 | + |
| 16 | +type LoaderFn = (this: LoaderContext, code: string, inputSourceMap?: unknown) => void; |
| 17 | + |
| 18 | +const upstreamLoader = codeTransformerLoaderImpl as unknown as LoaderFn; |
| 19 | + |
| 20 | +/** |
| 21 | + * The npm package name for a module path. |
| 22 | + * Reads the segment after the LAST `node_modules`, so pnpm's nested layout |
| 23 | + * resolves to the real package. Matches the name that channel integrations |
| 24 | + * await. |
| 25 | + */ |
| 26 | +function packageNameFromPath(resourcePath: string): string | undefined { |
| 27 | + const marker = '/node_modules/'; |
| 28 | + const normalized = resourcePath.replace(/\\/g, '/'); |
| 29 | + const index = normalized.lastIndexOf(marker); |
| 30 | + if (index === -1) { |
| 31 | + return undefined; |
| 32 | + } |
| 33 | + |
| 34 | + const [scopeOrName, name] = normalized.slice(index + marker.length).split('/'); |
| 35 | + if (!scopeOrName) { |
| 36 | + return undefined; |
| 37 | + } |
| 38 | + |
| 39 | + return scopeOrName.startsWith('@') && name ? `${scopeOrName}/${name}` : scopeOrName; |
| 40 | +} |
| 41 | + |
| 42 | +/** |
| 43 | + * Announce a runtime-injected module the way the runtime `--import` hook does, |
| 44 | + * so the lazily-registered channel integrations subscribe. Appended to each |
| 45 | + * transformed module's code, it runs when that module loads. |
| 46 | + */ |
| 47 | +function onInjectSnippet(moduleName: string): string { |
| 48 | + return ( |
| 49 | + ';(function(){' + |
| 50 | + 'var g=globalThis.__SENTRY_ORCHESTRION__;' + |
| 51 | + `if(g&&typeof g.onInject==='function')g.onInject(${JSON.stringify(moduleName)});` + |
| 52 | + '})();\n' |
| 53 | + ); |
| 54 | +} |
| 55 | + |
| 56 | +/** |
| 57 | + * Wraps the upstream code-transform loader. |
| 58 | + * |
| 59 | + * Under Turbopack the transform runs as a loader, but the webpack *plugin* that |
| 60 | + * emits the `injectDiagnostics` boot banner never runs, because Turbopack takes |
| 61 | + * loaders, not plugins. That banner is what calls `onInject` for bundled |
| 62 | + * modules, so without it the channel integrations never learn their module |
| 63 | + * loaded and never subscribe. When there is no webpack compilation (Turbopack |
| 64 | + * case), append the `onInject` call to each transformed module here instead. |
| 65 | + * Under webpack leave it to the banner, so signal fires exactly once per module |
| 66 | + */ |
| 67 | +const codeTransformerLoader: LoaderFn = function (code, inputSourceMap) { |
| 68 | + if (this._compilation) { |
| 69 | + upstreamLoader.call(this, code, inputSourceMap); |
| 70 | + return; |
| 71 | + } |
| 72 | + |
| 73 | + const realAsync = this.async.bind(this); |
| 74 | + const { resourcePath } = this; |
| 75 | + |
| 76 | + this.async = () => { |
| 77 | + const callback = realAsync(); |
| 78 | + return (error: unknown, outputCode?: string, outputMap?: unknown): void => { |
| 79 | + // The upstream loader returns the input code unchanged when it did not |
| 80 | + // transform the module, so a changed string means a channel-publishing |
| 81 | + // module we must announce. |
| 82 | + const transformed = !error && typeof outputCode === 'string' && outputCode !== code; |
| 83 | + const moduleName = transformed ? packageNameFromPath(resourcePath) : undefined; |
| 84 | + const finalCode = moduleName ? `${outputCode}${onInjectSnippet(moduleName)}` : outputCode; |
| 85 | + callback(error, finalCode, outputMap); |
| 86 | + }; |
| 87 | + }; |
| 88 | + |
| 89 | + upstreamLoader.call(this, code, inputSourceMap); |
| 90 | +}; |
12 | 91 |
|
13 | 92 | export default codeTransformerLoader; |
0 commit comments