fix(compile): adopt require() of named-only package as namespace, not default (#5257)#5261
Conversation
… default (#5257) The CJS→ESM wrap rewrites `const x = require('pkg')` (and `const { a } = require('pkg')`) to a default import `import _req_N from 'pkg'`. When `pkg` resolves to a named-only / CJS module (or an `__esModule`-marked module with no `default`), the package-default-export gate in bootstrap.rs bailed the whole build: Error: The requested package 'isexe' does not provide an export named 'default' (imported as '_req_0' in .../which/lib/index.js). But `require('pkg')` returns the module's exports object (its namespace), so this is valid CJS. Codegen already routes a no-`default` default import through the namespace machinery (#4872); the gate was simply too eager. Fix: tag every import synthesized by the CJS wrap with `is_adopted_require` (set in collect_modules where the wrap origin is known) and exempt those from the default-export gate. A genuine user `import X from 'pkg'` keeps Node's static-ESM error. Unblocks cross-spawn, which, execa, joi (the latter two now reach unrelated, later errors). Regression tests: adopted require of a named-only package binds the exports object (whole-value member access + destructuring) and runs; `module.exports = fn` default require still adopts as a callable default. Note for maintainer: no version / CHANGELOG / Cargo.lock edits per external-contributor flow — please fold in at merge.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds an ChangesCJS→ESM adopted-require bypass (issue
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/perry-hir/src/stable_hash/tests.rs (1)
231-257: ⚡ Quick winAdd a direct hash-sensitivity assertion for
is_adopted_require.
module_metadata_affects_hashnow includes the new field in the fixture, but it still doesn’t directly assert that flipping onlyis_adopted_requirechanges the hash. Adding that targeted check would lock the new stable-hash contract down explicitly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/perry-hir/src/stable_hash/tests.rs` around lines 231 - 257, The test `module_metadata_affects_hash` now includes the `is_adopted_require` field in the test fixture, but lacks a direct assertion verifying that changing only this field affects the hash. Add a new test case following the same pattern as the existing ones (like the name check and import check): create a modified module with `is_adopted_require` toggled to a different value than the base, then assert that its hash differs from the base hash. This will explicitly lock in the stable-hash contract for the `is_adopted_require` field.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@crates/perry-hir/src/stable_hash/tests.rs`:
- Around line 231-257: The test `module_metadata_affects_hash` now includes the
`is_adopted_require` field in the test fixture, but lacks a direct assertion
verifying that changing only this field affects the hash. Add a new test case
following the same pattern as the existing ones (like the name check and import
check): create a modified module with `is_adopted_require` toggled to a
different value than the base, then assert that its hash differs from the base
hash. This will explicitly lock in the stable-hash contract for the
`is_adopted_require` field.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 18a4b485-ec00-4245-b76e-4dffa0341b5e
📒 Files selected for processing (8)
crates/perry-hir/src/ir/decl.rscrates/perry-hir/src/lower/module_decl.rscrates/perry-hir/src/stable_hash/module.rscrates/perry-hir/src/stable_hash/tests.rscrates/perry-transform/src/inline/mod.rscrates/perry/src/commands/compile/bootstrap.rscrates/perry/src/commands/compile/collect_modules.rscrates/perry/tests/issue_5257_require_adopt_no_default_namespace.rs
Bug (#5257)
When perry natively compiles a package that does
const x = require('pkg')(orconst { a } = require('pkg')), the CJS→ESM wrap (#665/#1721) rewrites it to a default importimport _req_N from 'pkg'. Ifpkgexposes nodefault(a CJS module, or an__esModule-marked module with named-only exports), the package-default-export gate bails the entire build:Per CJS semantics
require('pkg')returns the exports object (its namespace), andisexe.sync(...)/const { sync } = require('isexe')work. Blocks cross-spawn, execa, which (via isexe), joi (via @hapi/hoek).Root cause
enforce_package_default_exports(bootstrap.rs) enforces Node's static-ESM "no default export" rule for any bare-package default import. But an import synthesized by the CJS wrap from arequire()is not a static-ESM default import —require()hands back the exports object. Codegen already routes a no-defaultdefault import through the namespace machinery (#4872, compile.rs); the gate was simply too eager and threw before that path could run.Fix
Import::is_adopted_require(perry-hir). Set it on every import of a CJS-wrapped module incollect_modules(where the wrap origin is known — every import there came from arequire()).is_adopted_requireimports from the default-export gate in bootstrap.rs. They fall through to the namespace machinery: member reads / destructuring resolve per-export; a whole-value read materializes the exports object.import X from 'pkg'(flagfalse) keeps Node's error.Threaded the new field through the stable-hash and the other
Import {...}literals.Test evidence
New
crates/perry/tests/issue_5257_require_adopt_no_default_namespace.rs(2 tests, both green):adopted_require_of_named_only_package_binds_namespace_and_runs— mirrors the exact isexe shape: a"type":"module"package whoseexports.requireresolves to a bundled__esModuleCJS with named-only exports (no default), consumed viarequire("foo").bar()and destructuringconst { bar, baz } = require("foo"). Asserts the gate error is gone and the binary prints91.module_exports_fn_default_require_still_works— regression guard:module.exports = fnstill adopts as a callable default (prints42).cargo test -p perry-hirandcargo test -p perry --libgreen. Related suites green: issue_4872_barrel_default_reexports, create_require_package, module_import_forms, issue_4841_namespace_cjs_reexport.Real corpus (before → after)
cross_spawn_.tsdoes not provide an export named 'default'cross-spawn object)which_.tswhich object)execa_.tsnpm-run-path)joi_.tsString.trim takes no args, got 2in joi/lib/validator.js)The next errors for execa/joi are out of scope for #5257 and not fixed here.
Risk
Low. The change only removes a build-time bail for require-originated imports; the namespace fallback they route into already existed (#4872). Default-shaped requires (
module.exports = fn) are unaffected (regression-tested). Real user ESMimport X from 'pkg'still errors like Node.Maintainer note
No version /
CHANGELOG.md/Current Version/Cargo.lockedits per the external-contributor flow — please fold the version bump + changelog entry in at merge.Summary by CodeRabbit
require()“adopted require” imports from dependencies that expose only named exports (nodefault), ensuring they fall back to namespace-style bindings instead of triggering default export resolution errors.#5257, including namespace/destructuring require cases and a callable default-shapedmodule.exportsscenario.