Skip to content

feat(compile): lower static literal require in user modules#5447

Closed
JagritGumber wants to merge 1 commit into
PerryTS:mainfrom
JagritGumber:codex/static-literal-require-support
Closed

feat(compile): lower static literal require in user modules#5447
JagritGumber wants to merge 1 commit into
PerryTS:mainfrom
JagritGumber:codex/static-literal-require-support

Conversation

@JagritGumber

@JagritGumber JagritGumber commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a broader static require(...) lowering pass for user-compiled modules. The old createRequire-only transform is replaced with a static literal require transform that runs before SWC/HIR lowering and feeds eligible require edges into Perry's normal import graph.

Supported forms include:

  • const mod = require("./local")
  • const { value } = require("./local")
  • require("allowed-package").member
  • const req = createRequire(import.meta.url); req("allowed-package")
  • renamed createRequire as makeRequire

The transform deliberately leaves builtins on the existing native builtin path, ignores comments/strings, respects local require shadowing, and only lowers package specifiers already allowed by perry.compilePackages.

Non-goals

  • Dynamic require(expr)
  • require.resolve(...)
  • Runtime CommonJS loader/cache semantics
  • Native .node addon loading
  • Bypassing compilePackages / allow.compilePackages

Test plan

  • cargo fmt --check
  • cargo test -p perry static_require_transform
  • cargo test -p perry --test create_require_package
  • cargo test -p perry --test module_import_forms create_require_package_specifier_resolves_to_compiled_namespace
  • cargo test -p perry --test issue_5257_require_adopt_no_default_namespace

Summary by CodeRabbit

  • New Features

    • Improved CommonJS-to-ESM transformation with enhanced static require() call detection and conversion.
  • Bug Fixes

    • Fixed handling of require() calls when accessing compiled packages in CommonJS code.
  • Tests

    • Updated test suite to validate improved compilation behavior for package resolution patterns.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Replaces create_require_transform.rs (200 lines, deleted) with a new static_require_transform.rs (288 lines) that handles both global require and createRequire-aliased calls. Updates collect_modules.rs wiring to use the new transform, broadens cjs_wrap/detect module visibility, and updates integration and unit tests to expect successful ESM namespace resolution.

Changes

Static Require Transform Replacement

Layer / File(s) Summary
Module visibility and collect_modules wiring
crates/perry/src/commands/compile/cjs_wrap/mod.rs, crates/perry/src/commands/compile/collect_modules.rs
Broadens detect submodule to pub(in crate::commands::compile), swaps the create_require_transform submodule for static_require_transform, updates the imported transform to transform_static_literal_requires, and adjusts the debug-symbol line-offset computation to use the new transform.
static_require_transform implementation
crates/perry/src/commands/compile/collect_modules/static_require_transform.rs
Adds transform_static_literal_requires and all helpers: createRequire alias collection from imports and declarations, regex factories for call and declaration patterns, shadowing detection, specifier eligibility, unique temp-name generation, and shebang-preserving import prepending. Includes unit tests covering all transform cases. Deletes create_require_transform.rs.
Integration and module-import-form test updates
crates/perry/tests/create_require_package.rs, crates/perry/tests/module_import_forms.rs
Refactors create_require_package.rs with write_minicord_fixture/compile_and_run helpers and adds a new direct-literal-require test. Updates module_import_forms.rs to assert successful ESM namespace resolution instead of an unsupported-interoperability error.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • PerryTS/perry#5276: Both PRs modify the cjs_wrap layer — this PR broadens detect module visibility while that PR extends cjs_wrap/detect.rs to recognize bracket/computed-string CommonJS export forms.

Poem

🐇 Hippity-hop, the old createRequire is gone,
A static transform hops proudly along!
It masks the strings, it checks the shadows too,
Prepends its imports in a shebang-safe queue.
No more unsupported errors in the log —
The rabbit rewired require, and it's all quite crisp! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(compile): lower static literal require in user modules' clearly and specifically summarizes the main change—introducing a static literal require lowering pass.
Description check ✅ Passed The description covers all key required sections: Summary (what changed and why), Changes (concrete modifications), Test plan (verified testing approach), and Checklist affirmation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch codex/static-literal-require-support

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/perry/src/commands/compile/collect_modules.rs (1)

430-446: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Prefix-line correction drops negative deltas, which can mis-map debug lines.

At Line 440, saturating_sub only handles added lines. If the static-require rewrite removes line breaks (e.g., multiline require(...) call collapsed to one identifier), the delta is negative and currently treated as 0, so prefix_line_count becomes too large.

Suggested fix
-            let lines_after_transform = source.bytes().filter(|&b| b == b'\n').count();
-            let added_lines = lines_after_transform.saturating_sub(lines_before_transform) as u32;
+            let lines_after_transform = source.bytes().filter(|&b| b == b'\n').count();
+            let line_delta = lines_after_transform as i64 - lines_before_transform as i64;
+            let adjusted_prefix = if line_delta >= 0 {
+                prefix_lines.saturating_add(line_delta as u32)
+            } else {
+                prefix_lines.saturating_sub((-line_delta) as u32)
+            };
             ctx.cjs_wrap_debug_sources.insert(
                 canonical.clone(),
                 super::types::CjsWrapDebugSource {
                     wrapped_source: source.clone(),
-                    prefix_line_count: prefix_lines + added_lines,
+                    prefix_line_count: adjusted_prefix,
                 },
             );
🤖 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/src/commands/compile/collect_modules.rs` around lines 430 - 446,
The calculation of the line delta in the block after
transform_static_literal_requires is called uses saturating_sub which treats
removed lines as zero, causing incorrect prefix_line_count when the
static-require transform collapses multiline requires into single identifiers.
Replace the saturating_sub calculation of added_lines with logic that properly
computes the signed delta between lines_after_transform and
lines_before_transform, allowing both positive and negative deltas, then apply
this delta appropriately to prefix_lines to ensure prefix_line_count accounts
for lines that were both added and removed during the transform.
🧹 Nitpick comments (1)
crates/perry/src/commands/compile/collect_modules/static_require_transform.rs (1)

28-53: ⚡ Quick win

HashSet iteration makes generated import/temp ordering nondeterministic.

At Line 28, iterating require_aliases directly can vary between runs, changing import insertion order and temp-id assignment. That hurts reproducibility and can create noisy cache/output diffs.

Suggested fix
-    for alias in require_aliases {
+    let mut aliases: Vec<_> = require_aliases.into_iter().collect();
+    aliases.sort();
+    for alias in aliases {
         let call_re = literal_require_call_re(&alias);
         for cap in call_re.captures_iter(source) {
             ...
         }
     }
🤖 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/src/commands/compile/collect_modules/static_require_transform.rs`
around lines 28 - 53, The iteration over require_aliases at the start of the
loop is nondeterministic because HashSet iteration order varies between runs,
causing the order of imports and temp-id assignments to change unpredictably.
Sort the require_aliases collection before iterating over it in the for loop to
ensure a consistent, deterministic ordering of the imports and temporary
variable names generated during each compilation run.
🤖 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.

Inline comments:
In
`@crates/perry/src/commands/compile/collect_modules/static_require_transform.rs`:
- Around line 16-18: The current implementation of
require_is_shadowed_by_non_create_require uses a file-global boolean check that
does not respect lexical scoping, causing it to miss true local shadowing (such
as require as a function parameter) and incorrectly suppress valid rewrites in
outer scopes. Refactor the shadowing detection logic to be scope-aware by
implementing token or AST-based analysis that checks whether require is shadowed
at the specific lexical position of each require call, rather than maintaining a
single file-wide shadow flag. This ensures that the condition checked in the if
statement at line 16-18 accurately validates shadowing based on local bindings
at the exact location where require is being rewritten.

---

Outside diff comments:
In `@crates/perry/src/commands/compile/collect_modules.rs`:
- Around line 430-446: The calculation of the line delta in the block after
transform_static_literal_requires is called uses saturating_sub which treats
removed lines as zero, causing incorrect prefix_line_count when the
static-require transform collapses multiline requires into single identifiers.
Replace the saturating_sub calculation of added_lines with logic that properly
computes the signed delta between lines_after_transform and
lines_before_transform, allowing both positive and negative deltas, then apply
this delta appropriately to prefix_lines to ensure prefix_line_count accounts
for lines that were both added and removed during the transform.

---

Nitpick comments:
In
`@crates/perry/src/commands/compile/collect_modules/static_require_transform.rs`:
- Around line 28-53: The iteration over require_aliases at the start of the loop
is nondeterministic because HashSet iteration order varies between runs, causing
the order of imports and temp-id assignments to change unpredictably. Sort the
require_aliases collection before iterating over it in the for loop to ensure a
consistent, deterministic ordering of the imports and temporary variable names
generated during each compilation run.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: aebfa977-37fd-4d7e-87f8-acc9a32c063a

📥 Commits

Reviewing files that changed from the base of the PR and between 1db8e6b and da99c68.

📒 Files selected for processing (6)
  • crates/perry/src/commands/compile/cjs_wrap/mod.rs
  • crates/perry/src/commands/compile/collect_modules.rs
  • crates/perry/src/commands/compile/collect_modules/create_require_transform.rs
  • crates/perry/src/commands/compile/collect_modules/static_require_transform.rs
  • crates/perry/tests/create_require_package.rs
  • crates/perry/tests/module_import_forms.rs
💤 Files with no reviewable changes (1)
  • crates/perry/src/commands/compile/collect_modules/create_require_transform.rs

Comment on lines +16 to +18
if !require_is_shadowed_by_non_create_require(source, &require_aliases) {
require_aliases.insert("require".to_string());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Shadow handling is not lexical, so some require(...) calls are rewritten incorrectly.

At Line 166, the current check is a file-global boolean. It can miss true local shadowing (e.g., function f(require) { ... }) and also over-suppress valid top-level rewrites when shadowing exists only in nested scopes. This breaks the “local require shadowing is respected” contract.

Suggested direction

Use scope-aware detection (token/AST-based) so each match is validated against lexical bindings at that position, instead of a single file-wide shadow flag.

Also applies to: 166-179

🤖 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/src/commands/compile/collect_modules/static_require_transform.rs`
around lines 16 - 18, The current implementation of
require_is_shadowed_by_non_create_require uses a file-global boolean check that
does not respect lexical scoping, causing it to miss true local shadowing (such
as require as a function parameter) and incorrectly suppress valid rewrites in
outer scopes. Refactor the shadowing detection logic to be scope-aware by
implementing token or AST-based analysis that checks whether require is shadowed
at the specific lexical position of each require call, rather than maintaining a
single file-wide shadow flag. This ensures that the condition checked in the if
statement at line 16-18 accurately validates shadowing based on local bindings
at the exact location where require is being rewritten.

proggeramlug pushed a commit that referenced this pull request Jun 19, 2026
Static-literal require(...) in user-compiled modules now lowers into the
import graph instead of staying a kept-local let. Clears Next.js W6 (#5437):
const uw = require('.../shared-cache-controls.external.js') resolves via the
module getter, so new uw.SharedCacheControls(...) no longer throws
'undefined is not a constructor' (bundle error count 18 -> 0).

Authored by JagritGumber (#5447).
@proggeramlug

Copy link
Copy Markdown
Contributor

Landed on main as 87f9a44. The PR branch had conflicted with main (test-file overlap with the #5389 createRequire tests), so I rebased+resolved locally and squash-merged with admin bypass, folding in the version bump (0.5.1193) + CHANGELOG. Validated against the live Next.js standalone bundle: this clears the W6 wall (#5437) — new uw.SharedCacheControls(...) error count 18 → 0. Thanks @JagritGumber!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants