Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions crates/perry-hir/src/destructuring/var_decl/native_fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,19 @@ pub(crate) fn register_native_fetch_and_streams(
// / unresolvable specifiers fall through to the legacy compile-time
// refusal in `expr_call::intrinsics::try_require_literal`.
if let Some(init_expr) = &decl.init {
if let Some(module_name) = require_resolvable_native_specifier(init_expr) {
register_require_namespace_binding(ctx, name, &module_name);
return true;
// #8342: inside a CJS-wrapped module the wrap's synthetic
// `function require(...)` (with a `createRequire`-backed built-in arm)
// shadows the bare global `require`. Don't steal `let x =
// require("process")` into a native-module namespace binding here — the
// native namespace isn't initialized in a CJS-wrapped module, so `x`
// would be undefined at runtime (`ReferenceError: node_process is not
// defined`). Let the call flow through to the synthetic require, which
// resolves builtins via `createRequire` (see `cjs_wrap::wrap`).
if !require_is_shadowed_by_local(ctx) {
if let Some(module_name) = require_resolvable_native_specifier(init_expr) {
register_require_namespace_binding(ctx, name, &module_name);
return true;
}
}
}

Expand Down
31 changes: 31 additions & 0 deletions crates/perry-hir/src/destructuring/var_decl_sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,26 @@ pub(crate) fn require_resolvable_native_specifier(init: &ast::Expr) -> Option<St
resolvable_native_module_for_spec(&require_literal_specifier(init)?)
}

/// #8342: is the bare global `require` shadowed by a local / function-scoped /
/// imported binding named `require`? This is exactly the situation inside a
/// CJS-wrapped module, where the wrap injects a synthetic
/// `function require(specifier) { ... }` (with a `createRequire`-backed
/// built-in arm, see `cjs_wrap::wrap`) into the IIFE body. Mirrors the guard in
/// `expr_call::intrinsics::try_require_literal` — which bails on the same
/// shadowing — but the destructuring `var`/`let`/`const` paths run BEFORE call
/// lowering and intercept `let x = require("process")` first, so without this
/// check they would register `x` as a native-module namespace binding and drop
/// the runtime local. In a CJS-wrapped module the native-module namespace is
/// not initialized, so `x` resolves to nothing at runtime
/// (`ReferenceError: node_process is not defined`). Returning `true` here tells
/// the callers to let the `require(...)` call flow through to the synthetic
/// require at runtime, which resolves builtins via `createRequire`.
pub(crate) fn require_is_shadowed_by_local(ctx: &LoweringContext) -> bool {
ctx.lookup_local("require").is_some()
|| ctx.lookup_func("require").is_some()
|| ctx.lookup_imported_func("require").is_some()
}

/// #5216: the canonical (`node:`-stripped) native module name for a require
/// specifier `raw`, iff it resolves to a Perry-supported native/Node-builtin
/// module; otherwise `None`. `node:`-prefixed specifiers must name a real Node
Expand Down Expand Up @@ -149,6 +169,17 @@ pub(super) fn register_destructured_stream_ctors(
return Vec::new();
};

// #8342: inside a CJS-wrapped module the wrap's synthetic
// `function require(...)` shadows the bare global `require`, and its
// built-in arm resolves `require("process")` etc. via `createRequire` at
// runtime. Don't register destructured members as native-module aliases
// here — the native namespace isn't initialized in a CJS-wrapped module,
// so the bindings would be undefined at runtime. Let the destructure run
// off the runtime `require(...)` call result instead.
if require_is_shadowed_by_local(ctx) && require_literal_specifier(init).is_some() {
return Vec::new();
}

// #5216: `const { createInterface } = require("readline")` — when the RHS is
// a `require("<native-spec>")` literal, register EVERY destructured member
// as a native named member, exactly as `import { createInterface } from
Expand Down
50 changes: 50 additions & 0 deletions crates/perry/src/commands/compile/cjs_wrap/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1933,3 +1933,53 @@ fn hoist_keeps_inheritance_chain_with_iife_local_parent_together() {
hoisted2
);
}

// #8342: a CJS-wrapped module that does `let node_process = require("process");
// node_process = __toESM(node_process, 1)` (the rolldown-bundled
// `@socketsecurity/lib` shape) must NOT hoist a static
// `import _req_N from 'process'` for the Node.js built-in. The codegen does not
// initialize native-module import bindings inside CJS-wrapped modules, so the
// hoisted binding would be undefined at runtime and the HIR's destructuring
// var-decl pass would steal `node_process` into a native-module namespace
// binding (dropping the runtime local) — producing
// `ReferenceError: node_process is not defined`. The wrap must instead keep the
// body's `let node_process = require("process")` declaration and route the
// built-in through the synthetic require's `createRequire` arm.
#[test]
fn cjs_wrap_builtin_require_not_hoisted_as_static_import() {
let src = "let node_process = require(\"process\");\n\
node_process = __toESM(node_process, 1);\n\
module.exports = { platform: node_process.platform };\n";
let path = PathBuf::from("/tmp/x/external-pack.js");
let wrapped = wrap_commonjs(src, &path);

// (1) No static ESM import of the built-in is emitted. The only import
// from a `node:` spec should be the wrap's own `createRequire` helper.
assert!(
!wrapped.contains("from 'process'"),
"built-in `process` must not be hoisted as a static import; got:\n{wrapped}"
);
assert!(
!wrapped.contains("import _req_"),
"no synthetic `_req_N` import should be emitted for built-ins; got:\n{wrapped}"
);

// (2) The body's `let node_process = require(\"process\")` declaration
// survives (is not blanked) so it runs inside the IIFE and calls the
// synthetic require at runtime.
assert!(
wrapped.contains("let node_process = require(\"process\");"),
"the built-in require declaration must stay in the IIFE body; got:\n{wrapped}"
);

// (3) The synthetic require's per-spec case for `process` resolves via
// `createRequire`, not by returning the (non-existent) import local.
assert!(
wrapped.contains("__perry_cjs_create_require("),
"the built-in require case must use createRequire; got:\n{wrapped}"
);
assert!(
!wrapped.contains("return _req_0;"),
"the built-in require case must not reference the dropped import local; got:\n{wrapped}"
);
}
76 changes: 72 additions & 4 deletions crates/perry/src/commands/compile/cjs_wrap/wrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -332,6 +356,13 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset(
let imports = require_specs
.iter()
.zip(import_local_names.iter())
// #8342: don't emit a static `import _req_N from 'process'` for Node.js
// built-in specs. The codegen does not initialize native-module import
// bindings inside CJS-wrapped modules, so the binding would be dropped
// by the HIR / left undefined at runtime. Builtins resolve through the
// synthetic require's `createRequire` arm instead (see `require_cases`),
// which never references the import local.
.filter(|(spec, _)| !builtin_requires.contains(spec))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
.map(|(spec, local)| {
// #4904: Node's underscore-prefixed internal http modules are
// require-only re-exports of the public `http` surface
Expand Down Expand Up @@ -391,7 +422,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>()
Expand All @@ -406,12 +437,26 @@ 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};")
};
if require_site_in_try(source, spec) {
if builtin_requires.contains(spec) {
// #8342: builtins have no static import binding (we skip emitting
// one above), so never reference `{local}` here — always go through
// the `createRequire`-backed `required_value`. The try-site
// `typeof {local} === 'boolean'` sentinel guard does not apply
// (builtins are never the pruned-build TRUE sentinel).
format!(" if (specifier === '{spec}') {{ {required_value} }}")
} else if require_site_in_try(source, spec) {
format!(
" if (specifier === '{spec}') {{ if (typeof {local} === 'boolean') \
throw __perry_cjs_require_error('error', 'MODULE_NOT_FOUND', \
Expand Down Expand Up @@ -703,6 +748,14 @@ pub(in crate::commands::compile) fn wrap_commonjs_with_body_offset(
// immutable module-scope `const alias = _req_N;` (the const write
// would throw) nor strip its declaration below.
.filter(|(alias, _, _)| !identifier_is_reassigned(source, alias))
// #8342: don't surface `const alias = _req_N;` for Node.js built-in
// specs — we no longer emit a static `import _req_N from '<builtin>'`
// (the codegen doesn't initialize native-module import bindings in
// CJS-wrapped modules), so `_req_N` doesn't exist. The body's own
// `<kw> alias = require('<builtin>')` stays (builtins are excluded
// from the blanking filter below) and resolves through the synthetic
// require's `createRequire` arm at runtime.
.filter(|(_, spec, _)| !builtin_requires.contains(spec))
.filter_map(|(alias, spec, _range)| {
let idx = require_specs.iter().position(|s| s == spec)?;
// When the alias is already the spec's import local name
Expand All @@ -722,6 +775,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)
Expand Down Expand Up @@ -902,6 +963,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 thread
coderabbitai[bot] marked this conversation as resolved.
// 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).
Expand Down
Loading
Loading