-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
fix(server-runtime-injection): Keep ES modules working on Deno #24669
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import { consoleSandbox, debug, getClient, GLOBAL_OBJ, parseSemver } from '@sentry/core'; | ||
| import { existsSync } from 'node:fs'; | ||
| import { existsSync, readFileSync } from 'node:fs'; | ||
| import * as Module from 'node:module'; | ||
| import { dirname, join } from 'node:path'; | ||
| import { fileURLToPath, pathToFileURL } from 'node:url'; | ||
|
|
@@ -28,20 +28,63 @@ function hasStableSyncModuleHooks(isDeno: boolean): boolean { | |
| return major > 25 || (major === 25 && minor >= 1) || (major === 24 && minor >= 13); | ||
| } | ||
|
|
||
| /** `"type"` of the nearest `package.json`, keyed by the directory the lookup started in. */ | ||
| const packageTypeByDir = new Map<string, string | undefined>(); | ||
|
|
||
| function getPackageType(dir: string): string | undefined { | ||
| if (packageTypeByDir.has(dir)) { | ||
| return packageTypeByDir.get(dir); | ||
| } | ||
|
|
||
| let type: string | undefined; | ||
| const packageJsonPath = join(dir, 'package.json'); | ||
| if (existsSync(packageJsonPath)) { | ||
| try { | ||
| type = (JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { type?: string }).type; | ||
| } catch { | ||
| type = undefined; | ||
| } | ||
| } else if (dirname(dir) !== dir) { | ||
| type = getPackageType(dirname(dir)); | ||
| } | ||
|
|
||
| packageTypeByDir.set(dir, type); | ||
| return type; | ||
| } | ||
|
|
||
| /** The `format` Node would report for `url`, for the formats Deno leaves out. */ | ||
| function getMissingDenoFormat(url: string): string | undefined { | ||
| if (url.endsWith('.json')) { | ||
| return 'json'; | ||
| } | ||
| if (url.endsWith('.mjs')) { | ||
| return 'module'; | ||
| } | ||
| if (url.startsWith('file:') && url.endsWith('.js') && getPackageType(dirname(fileURLToPath(url))) === 'module') { | ||
| return 'module'; | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Deno's `nextLoad` reports no `format` for a `.json` file, where Node reports `'json'`. With any | ||
| * load hook installed, Deno's CJS loader then compiles the JSON as JavaScript and `require()` of it | ||
| * throws `SyntaxError: Unexpected token ':'`. Restoring the format is enough, and only Deno needs | ||
| * it: on Node the format is never missing. | ||
| * Deno's `nextLoad` reports no `format` for a `.json` file or an ES module, where Node reports | ||
| * `'json'` or `'module'`. Without the format, Deno's CJS loader compiles JSON as JavaScript | ||
| * (`SyntaxError: Unexpected token ':'`), and the transform treats an ES module as CommonJS and | ||
| * injects a `require()` into it (`ReferenceError: require is not defined`). The format is restored | ||
| * on the `nextLoad` result, so the transform sees it too. Only Deno needs this. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth adding a comment here and in the PR description as well, that it's only needed as long as we support Deno versions that do not have the fix in denoland/deno#36849. We're already gating on the presence/lack of a format, so we don't need any version sniffing, I don't think. But it'd be nice to know when we can cut the fix out entirely, certainly not before v12. Actually, come to think of it, could also add a |
||
| */ | ||
| function withDenoJsonFormat(loadHook: Function): Function { | ||
| return (url: string, context: unknown, nextLoad: Function) => { | ||
| const result = loadHook(url, context, nextLoad) as { format?: string }; | ||
| if (result?.format === undefined && url.endsWith('.json')) { | ||
| result.format = 'json'; | ||
| } | ||
| return result; | ||
| }; | ||
| function withDenoFormats(loadHook: Function): Function { | ||
| return (url: string, context: unknown, nextLoad: Function) => | ||
| loadHook(url, context, (nextUrl: string, nextContext: unknown) => { | ||
| const result = nextLoad(nextUrl, nextContext) as { format?: string | null } | undefined; | ||
| if (result && result.format == null) { | ||
| const format = getMissingDenoFormat(nextUrl); | ||
| if (format) { | ||
| result.format = format; | ||
| } | ||
| } | ||
| return result; | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -181,7 +224,7 @@ export function registerDiagnosticsChannelInjection(): void { | |
| try { | ||
| if (typeof mod.registerHooks === 'function' && stableSyncHooks) { | ||
| initialize({ instrumentations: SENTRY_RUNTIME_INSTRUMENTATIONS }); | ||
| mod.registerHooks({ resolve, load: globalAny.Deno ? withDenoJsonFormat(load) : load }); | ||
| mod.registerHooks({ resolve, load: globalAny.Deno ? withDenoFormats(load) : load }); | ||
| debug.log('Registered diagnostics-channel injection via Module.registerHooks()'); | ||
| } else if (typeof mod.register === 'function' && !globalAny.Bun && !globalAny.Deno) { | ||
| // `Module.register` + the `_compile` patch is Node 18.19–24.12 / 25.0 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: The
getPackageTypefunction incorrectly cachesundefinedand fails to check parent directories if reading an existingpackage.jsonfile fails.Severity: LOW
Suggested Fix
When
readFileSyncfails within thetry...catchblock, the function should not cacheundefined. Instead, it should fall back to checking the parent directory, similar to the logic used whenexistsSyncreturnsfalse.Prompt for AI Agent
Did we get this right? 👍 / 👎 to inform future reviews.