-
-
Notifications
You must be signed in to change notification settings - Fork 159
fix(cjs-wrap): resolve built-in requires through createRequire, not dropped ESM imports #8341
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 |
|---|---|---|
|
|
@@ -152,7 +152,20 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( | |
| if !dead_platform_requires.is_empty() { | ||
| require_specs.retain(|spec| !dead_platform_requires.contains(spec)); | ||
| } | ||
|
|
||
| // #sdxgen: Identify Node.js built-in requires (`require("process")`, | ||
| // `require("os")`, etc.) so the synthetic `require` function can resolve | ||
| // them via `createRequire` at runtime instead of relying on the hoisted | ||
| // static import binding (which the codegen does not initialize for | ||
| // native modules inside CJS-wrapped modules). | ||
| let builtin_requires: Vec<String> = require_specs | ||
| .iter() | ||
| .filter(|spec| { | ||
| let normalized = spec.strip_prefix("node:").unwrap_or(spec); | ||
| let base = normalized.split('/').next().unwrap_or(normalized); | ||
| perry_hir::is_node_builtin_module(base) | ||
| }) | ||
| .cloned() | ||
| .collect(); | ||
| // Issue #652: hoist top-level `class X { ... }` declarations OUT of the | ||
| // IIFE so the consumer's `import { X } from "pkg"` resolves to the real | ||
| // class instead of a runtime property access on `_cjs.X`. | ||
|
|
@@ -284,6 +297,17 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( | |
| // Don't adopt a function-local alias — keep it lazy (see above). | ||
| continue; | ||
| } | ||
| // #sdxgen: Don't adopt aliases for Node.js built-in modules. The | ||
| // codegen doesn't initialize native-module import bindings inside | ||
| // CJS-wrapped modules, so an adopted alias would be undefined at | ||
| // runtime. Keeping the alias un-adopted means the declaration stays | ||
| // in the IIFE body and `require("process")` goes through the | ||
| // synthetic require, which resolves builtins via createRequire. | ||
| let normalized = spec.strip_prefix("node:").unwrap_or(spec); | ||
| let base = normalized.split('/').next().unwrap_or(normalized); | ||
| if perry_hir::is_node_builtin_module(base) { | ||
| continue; | ||
| } | ||
| if import_local_names.iter().any(|n| n == alias) { | ||
| continue; | ||
| } | ||
|
|
@@ -391,7 +415,7 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( | |
| .into_iter() | ||
| .map(|property| { | ||
| format!( | ||
| "if (childBefore && childBefore.loaded === false) process.emitWarning(\"Accessing non-existent property '{property}' of module exports inside circular dependency\"); " | ||
| "if (childBefore && childBefore.loaded === false) globalThis.process?.emitWarning?.(\"Accessing non-existent property '{property}' of module exports inside circular dependency\"); " | ||
| ) | ||
| }) | ||
| .collect::<String>() | ||
|
|
@@ -406,7 +430,14 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( | |
| } else { | ||
| None | ||
| }; | ||
| let required_value = if needs_runtime_record { | ||
| let required_value = if builtin_requires.contains(spec) { | ||
| // #sdxgen: For Node.js built-in modules, resolve via createRequire | ||
| // at runtime instead of the hoisted import binding (which the | ||
| // codegen does not initialize for native modules in CJS-wrapped | ||
| // modules). createRequire calls js_create_native_module_namespace | ||
| // under the hood — the same path Node.js uses for require("process"). | ||
| format!("{link_child}return __perry_cjs_create_require({:?})(specifier);", source_path.to_string_lossy()) | ||
| } else if needs_runtime_record { | ||
| runtime_require.clone().unwrap_or_else(|| format!("return {local};")) | ||
| } else { | ||
| format!("{link_child}return {local};") | ||
|
|
@@ -722,6 +753,14 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( | |
| .into_iter() | ||
| .filter(|(_, spec, _)| require_specs.iter().any(|s| s == spec)) | ||
| .filter(|(alias, _, _)| !identifier_is_reassigned(source, alias)) | ||
| // #sdxgen: Don't blank alias declarations for Node.js built-in | ||
| // modules — let them stay in the IIFE body and resolve through | ||
| // the synthetic require (which uses createRequire for builtins). | ||
| .filter(|(_, spec, _)| { | ||
| let normalized = spec.strip_prefix("node:").unwrap_or(spec); | ||
| let base = normalized.split('/').next().unwrap_or(normalized); | ||
| !perry_hir::is_node_builtin_module(base) | ||
| }) | ||
| .map(|(_, _, range)| range) | ||
| .collect::<Vec<_>>(); | ||
| (lines, ranges) | ||
|
|
@@ -902,6 +941,13 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset( | |
| if (typeof specifier !== 'string') throw __perry_cjs_require_error('type', 'ERR_INVALID_ARG_TYPE', 'The "id" argument must be of type string.'); | ||
| if (specifier === '') throw __perry_cjs_require_error('type', 'ERR_INVALID_ARG_VALUE', 'The argument "id" must be a non-empty string.'); | ||
| {require_cases} | ||
| // #sdxgen: Node.js built-in modules that were NOT hoisted as static | ||
| // imports (see the builtin_requires filter above). Resolve them via | ||
| // createRequire at runtime, which calls js_create_native_module_namespace | ||
| // under the hood — the same path Node.js uses for require("process"). | ||
| if (__perry_cjs_require_is_builtin(specifier)) {{ | ||
| return __perry_cjs_create_require({module_path_literal})(specifier); | ||
| }} | ||
|
Comment on lines
+944
to
+950
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. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 3 'NODE_BUILTIN_MODULES|supported_builtin_module_name|__perry_cjs_require_is_builtin' \
crates/perry-hir/src/ir/constants.rs \
crates/perry-runtime/src/process/node_module.rs \
crates/perry/src/commands/compile/cjs_wrap/wrap.rsRepository: PerryTS/perry Length of output: 4531 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- shared Node built-in list ---'
sed -n '455,520p' crates/perry-hir/src/ir/constants.rs
printf '%s\n' '--- generated CJS predicate ---'
sed -n '885,940p' crates/perry/src/commands/compile/cjs_wrap/wrap.rs
printf '%s\n' '--- runtime supported-builtin predicate ---'
rg -n -A80 -B15 'fn supported_builtin_module_name|MODULE_BUILTIN_MODULES|supported_builtin_module_name' crates/perry-runtime crates/perry
printf '%s\n' '--- CJS wrapper call sites and builtin classification ---'
sed -n '700,780p' crates/perry/src/commands/compile/cjs_wrap/wrap.rs
sed -n '940,1025p' crates/perry/src/commands/compile/cjs_wrap/wrap.rs
rg -n -C4 'builtin_requires|is_node_builtin_module|NODE_BUILTIN_MODULES' crates/perry/src/commands/compile crates/perry-hirRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- wrapper require fallback ---'
sed -n '938,1022p' crates/perry/src/commands/compile/cjs_wrap/wrap.rs
printf '%s\n' '--- wrapper switch generation ---'
rg -n -C8 'require_is_builtin|builtin_requires|NODE_BUILTIN_MODULES|case .assert' crates/perry/src/commands/compile/cjs_wrap crates/perry/src
printf '%s\n' '--- exact list comparison ---'
python3 - <<'PY'
from pathlib import Path
import re
constants = Path("crates/perry-hir/src/ir/constants.rs").read_text()
wrapper = Path("crates/perry/src/commands/compile/cjs_wrap/wrap.rs").read_text()
shared_block = re.search(
r'pub const NODE_BUILTIN_MODULES: &\[&str\] = &\[(.*?)\];',
constants, re.S
).group(1)
shared = re.findall(r'"([^"]+)"', shared_block)
switch_block = re.search(
r'function __perry_cjs_require_is_builtin\(specifier\) \{\{(.*?)default:\s*return false;',
wrapper, re.S
).group(1)
generated = sorted(set(re.findall(r"case '([^']+)'", switch_block)))
bare = sorted(x[5:] if x.startswith("node:") else x for x in generated)
print("shared count:", len(shared))
print("wrapper bare count:", len(set(bare)))
print("in shared but absent from wrapper:", sorted(set(shared) - set(bare)))
print("wrapper bare names absent from shared:", sorted(set(bare) - set(shared)))
PYRepository: PerryTS/perry Length of output: 29055 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- runtime allowlist and normalization ---'
rg -n -A45 -B8 'MODULE_BUILTIN_MODULES|supported_builtin_module_name' crates/perry-runtime/src/process crates/perry-runtime/src
printf '%s\n' '--- literal require-case construction ---'
sed -n '340,450p' crates/perry/src/commands/compile/cjs_wrap/wrap.rs
printf '%s\n' '--- tests for computed builtin require and resolve ---'
rg -n -C5 'computed.*require|require.*computed|require\.resolve|fs/promises|diagnostics_channel|stream/web|tls|dgram|wasi' crates/perry/tests crates/perry/src/commands/compile/cjs_wrapRepository: PerryTS/perry Length of output: 50370 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- complete runtime builtin list ---'
sed -n '191,265p' crates/perry-runtime/src/process.rs
printf '%s\n' '--- literal require case generation ---'
sed -n '390,445p' crates/perry/src/commands/compile/cjs_wrap/wrap.rs
printf '%s\n' '--- runtime predicate behavior for wrapper gap ---'
python3 - <<'PY'
from pathlib import Path
import re
runtime = Path("crates/perry-runtime/src/process.rs").read_text()
wrapper = Path("crates/perry/src/commands/compile/cjs_wrap/wrap.rs").read_text()
runtime_block = re.search(
r'pub\(crate\) const MODULE_BUILTIN_MODULES: &\[&str\] = &\[(.*?)\];',
runtime, re.S
).group(1)
runtime_entries = re.findall(r'"([^"]+)"', runtime_block)
switch_block = re.search(
r'function __perry_cjs_require_is_builtin\(specifier\) \{\{(.*?)default:\s*return false;',
wrapper, re.S
).group(1)
wrapper_cases = set(re.findall(r"case '([^']+)'", switch_block))
def runtime_supported(spec):
if spec.startswith("node:node:") or spec.removeprefix("node:").startswith("_"):
return False
name = spec[5:] if spec.startswith("node:") else spec
if name in {"sea", "sqlite", "test", "test/reporters"}:
return spec.startswith("node:")
return name in runtime_entries
candidates = sorted({
entry if entry.startswith("node:") else entry
for entry in runtime_entries
if not entry.startswith("_")
} | {
"node:" + entry
for entry in runtime_entries
if not entry.startswith("_") and not entry.startswith("node:")
})
missing = [spec for spec in candidates if runtime_supported(spec) and spec not in wrapper_cases]
print("runtime-supported spellings absent from wrapper:", missing)
print("wrapper spellings that runtime rejects:", sorted(
spec for spec in wrapper_cases if not runtime_supported(spec)
))
PYRepository: PerryTS/perry Length of output: 5525 Align computed CJS built-in detection with runtime-supported specifiers.
🤖 Prompt for AI Agents |
||
| // Runtime `require(path)` of a module Perry AOT-compiled but that is | ||
| // only reachable via a computed path. Next's webpack runtime uses both | ||
| // absolute page paths and relative chunk paths (`./chunks/` + id). | ||
|
|
||
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.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 3446
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 31881
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 18819
🏁 Script executed:
Repository: PerryTS/perry
Length of output: 31588
Match the complete normalized specifier.
The exact predicate classifies valid entries such as
fs/promisesandpath/win32, but the base-name checks also classify unsupported paths such asfs/unknownandpath/unknownas built-ins. Those paths bypass compiled-module resolution and reachcreateRequire.Use
normalizedinstead ofbaseat lines 165, 308, and 762.🤖 Prompt for AI Agents