|
| 1 | +import * as path from 'node:path'; |
| 2 | +import { fileURLToPath } from 'node:url'; |
| 3 | +import { createLoadHookTransform } from '@sentry/server-utils/orchestrion/load-hook'; |
| 4 | +import { remixV3Config } from '@sentry/server-utils/orchestrion/config'; |
| 5 | + |
| 6 | +/** Node's synchronous `load` hook shape, which `@remix-run/assets` reuses for `scripts.loaders`. */ |
| 7 | +export interface ModuleLoadContext { |
| 8 | + moduleUrl?: string; |
| 9 | + [key: string]: unknown; |
| 10 | +} |
| 11 | + |
| 12 | +export interface ModuleLoadResult { |
| 13 | + format: string | null | undefined; |
| 14 | + shortCircuit?: boolean; |
| 15 | + source?: string | ArrayBuffer | ArrayBufferView; |
| 16 | +} |
| 17 | + |
| 18 | +export type ModuleLoader = ( |
| 19 | + url: string, |
| 20 | + context: ModuleLoadContext, |
| 21 | + nextLoad: (url: string, context?: Partial<ModuleLoadContext>) => ModuleLoadResult, |
| 22 | +) => ModuleLoadResult; |
| 23 | + |
| 24 | +/** |
| 25 | + * An asset server loader that applies orchestrion's transform to browser modules. Remix 3 has no |
| 26 | + * bundler, so the asset server's loader chain is the only compile time hook there is. |
| 27 | + * |
| 28 | + * `dcModuleUrl`, the shim's public URL, must also be in `scripts.external`, so the compiler leaves |
| 29 | + * the injected import alone. |
| 30 | + */ |
| 31 | +export function orchestrionLoader(dcModuleUrl: string): ModuleLoader { |
| 32 | + const cached = loaders.get(dcModuleUrl); |
| 33 | + if (cached) { |
| 34 | + return cached; |
| 35 | + } |
| 36 | + |
| 37 | + // Only the Remix 3 configs. The others target server packages, and their injected snippet imports |
| 38 | + // `@sentry/server-utils`, which cannot resolve in a browser. |
| 39 | + const transform = createLoadHookTransform({ dcModule: dcModuleUrl, instrumentations: remixV3Config }); |
| 40 | + |
| 41 | + const loader: ModuleLoader = (url, context, nextLoad) => { |
| 42 | + const result = nextLoad(url, context); |
| 43 | + if (result.format !== 'module' || typeof result.source !== 'string') { |
| 44 | + return result; |
| 45 | + } |
| 46 | + |
| 47 | + const transformed = transform(fileURLToPath(url), result.source); |
| 48 | + return transformed === undefined ? result : { ...result, source: transformed }; |
| 49 | + }; |
| 50 | + |
| 51 | + loaders.set(dcModuleUrl, loader); |
| 52 | + return loader; |
| 53 | +} |
| 54 | + |
| 55 | +// One loader per shim URL, so options that pass through `withDebugIdOptions` twice keep one copy. |
| 56 | +const loaders = new Map<string, ModuleLoader>(); |
| 57 | + |
| 58 | +export function isOrchestrionLoader(loader: unknown): boolean { |
| 59 | + for (const known of loaders.values()) { |
| 60 | + if (known === loader) { |
| 61 | + return true; |
| 62 | + } |
| 63 | + } |
| 64 | + return false; |
| 65 | +} |
| 66 | + |
| 67 | +/** |
| 68 | + * Public URL of the shipped shim under the asset server's `node_modules` mount, percent encoded per |
| 69 | + * segment as the asset server encodes rewritten imports. `@sentry` and `%40sentry` are two different |
| 70 | + * modules to a browser, so a mismatch loads two shims and instrumentation quietly does nothing. |
| 71 | + */ |
| 72 | +export function getShimUrl( |
| 73 | + basePath: string, |
| 74 | + rootDir: string, |
| 75 | + mounts: Readonly<Record<string, string>> | undefined, |
| 76 | + shimPath: string, |
| 77 | +): string | undefined { |
| 78 | + const nodeModulesRoot = path.join(rootDir, 'node_modules'); |
| 79 | + const relative = path.relative(nodeModulesRoot, shimPath).split(path.sep); |
| 80 | + // Outside the mount, as with a `link:` install, the asset server cannot serve it. |
| 81 | + if (relative[0] === '..' || path.isAbsolute(relative[0] ?? '')) { |
| 82 | + return undefined; |
| 83 | + } |
| 84 | + const encoded = relative.map(segment => encodeURIComponent(segment)).join('/'); |
| 85 | + return `${basePath.replace(/\/$/, '')}/${npmMount(mounts)}/${encoded}`; |
| 86 | +} |
| 87 | + |
| 88 | +// The default mounts are `{ app: 'app', npm: 'node_modules' }`, but an app can rename them. |
| 89 | +function npmMount(mounts: Readonly<Record<string, string>> | undefined): string { |
| 90 | + const entry = mounts && Object.entries(mounts).find(([, dir]) => dir === 'node_modules' || dir === './node_modules'); |
| 91 | + return entry ? entry[0] : 'npm'; |
| 92 | +} |
| 93 | + |
| 94 | +// The shim is built next to this module, so its path follows from this module's own under any |
| 95 | +// install layout. A package self reference would not work: the `./v3` subpaths are import only. |
| 96 | +export function resolveShimPath(): string { |
| 97 | + let here: string; |
| 98 | + /*! rollup-include-cjs-only */ |
| 99 | + here = __filename; |
| 100 | + /*! rollup-include-cjs-only-end */ |
| 101 | + /*! rollup-include-esm-only */ |
| 102 | + here = fileURLToPath(import.meta.url); |
| 103 | + /*! rollup-include-esm-only-end */ |
| 104 | + return path.join(path.dirname(here), 'client', 'diagnosticsChannelShim.js'); |
| 105 | +} |
0 commit comments