Skip to content

fix(cjs): recognize bracket/computed-string-literal export forms (#5275)#5276

Merged
proggeramlug merged 2 commits into
mainfrom
fix/cjs-bracket-exports
Jun 17, 2026
Merged

fix(cjs): recognize bracket/computed-string-literal export forms (#5275)#5276
proggeramlug merged 2 commits into
mainfrom
fix/cjs-bracket-exports

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Root cause (#5275)

Perry recognized the dot forms module.exports = … and exports.foo = … as CommonJS export assignments, but not the bracket / computed-string-literal forms module['exports'] = … / module["exports"] = … (default) and exports['name'] = … / exports["name"] = … (named).

@colors/colors/lib/custom/trap.js does module['exports'] = function runTheTrap(…){…}. Because that file was not recognized as CJS, it fell through to the ESM-only pipeline: the bare module/exports identifiers threw at module init (module is not defined) and the default export was never registered. This blocked winston (which depends on @colors/colors).

Fix

Two-part, both confined to the cjs_wrap layer (crates/perry/src/commands/compile/cjs_wrap/):

  1. Detection (detect.rs): is_commonjs now recognizes the bracket export forms. Because strip_comments_and_strings blanks the quoted key (module['exports']module[ ]), the new has_bracket_cjs_export helper matches on the original source via a regex that requires an = (a real assignment) and a string-literal key. A genuinely dynamic module[k] = … (non-literal key) does not match and stays on the ESM/runtime path.
  2. Export-assignment recognition (extract_exports.rs):
    • extract_single_module_exports_assignment accepts module['exports'] = Ident (the class-identity default path), equivalent to module.exports = Ident.
    • extract_exports_from_source surfaces exports['name'] = … / module.exports['name'] = … as named exports (with the same .-boundary guard the dot matcher uses to avoid inner-module e.exports['X']). Dynamic keys are not extracted.

No HIR lowering change is needed: inside the IIFE wrap, module/exports are ordinary local vars, so the bracket assignment executes correctly at runtime and export default _cjs; / export const name = _cjs.name; resolve.

Test evidence

  • 8 new cjs_wrap unit tests (detection, extraction, dynamic-key rejection, end-to-end wrap+parse). Full cjs_wrap suite: 83 passed, 0 failed.
  • End-to-end (matches Node node main.ts):
    • module['exports'] = function greet default require → hi x
    • exports['foo'] = … named import → foo x
    • double-quoted module["exports"]dq x
    • regression: dot module.exports + module.exports.extradot x extra y
    • regression: dynamic o[k]=… object access in a CJS module → 42

winston before/after

  • Before: @colors/colors bracket export not recognized → module is not defined.
  • After: PERRY_NO_AUTO_OPTIMIZE=1 perry drivers/winston_.ts -o /tmp/wWrote executable. Running it gets past the @colors/colors wall; the next (unrelated, out-of-scope) wall is SyntaxError: Invalid regular expression: /[0m/: invalid pattern (an ANSI-escape regex-engine gap). Not fixed here.

Notes

  • Per the external-contributor / maintainer convention, no [workspace.package] version, Current Version, CHANGELOG.md, or Cargo.lock edits — the maintainer folds version + changelog in at merge.
  • Files changed (all under the 2000-line cap): cjs_wrap/detect.rs, cjs_wrap/extract_exports.rs, cjs_wrap/mod.rs.

Risk

Low. Detection only widens to additional CJS signals (gated by an = assignment and a string-literal key; has_top_level_esm still wins first, so real ESM files are unaffected). Extraction reuses the existing dot-matcher boundary guards. Dynamic non-literal keys are explicitly excluded and verified.

Summary by CodeRabbit

  • Bug Fixes

    • Improved CommonJS detection and CommonJS export extraction to recognize bracket/computed-string-literal assignment forms, including module['exports'] / module["exports"] and exports['name'] / exports["name"].
    • Dynamic/non-literal bracket keys (e.g., module[k]) are still not treated as CommonJS signals.
  • Tests

    • Added regression tests for bracket-notation detection and named-export extraction, including end-to-end verification that wrapped output is correctly classified as CommonJS and remains valid TypeScript.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 86a4e38b-268a-4d9a-9ddf-9f9a95d83fba

📥 Commits

Reviewing files that changed from the base of the PR and between b900da9 and 148f896.

📒 Files selected for processing (3)
  • crates/perry/src/commands/compile/cjs_wrap/detect.rs
  • crates/perry/src/commands/compile/cjs_wrap/extract_exports.rs
  • crates/perry/src/commands/compile/cjs_wrap/mod.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/perry/src/commands/compile/cjs_wrap/detect.rs
  • crates/perry/src/commands/compile/cjs_wrap/mod.rs
  • crates/perry/src/commands/compile/cjs_wrap/extract_exports.rs

📝 Walkthrough

Walkthrough

The CJS wrap pipeline in cjs_wrap is extended to recognize bracket/computed-string-literal export patterns (module['exports'], exports['name']). Detection, single-assignment extraction, and named-export extraction each gain new regex matchers for these forms; dynamic non-literal keys are explicitly excluded. New unit and regression tests cover all added paths.

Changes

Bracket-notation CJS detection and export extraction

Layer / File(s) Summary
Bracket-notation detection in is_commonjs
crates/perry/src/commands/compile/cjs_wrap/detect.rs
is_commonjs gains a has_bracket_cjs_export fallback that uses two regexes on the raw source to match module['exports']/module["exports"] (default export) and exports['name'] (named exports). The helper explicitly excludes dynamic non-literal bracket keys like module[k].
Bracket-notation extraction in export collection
crates/perry/src/commands/compile/cjs_wrap/extract_exports.rs
extract_single_module_exports_assignment regex extended to accept bracket-notation module['exports']/module["exports"] assignments. extract_exports_from_source gains a bracket_re matcher that collects named exports from exports['name']/exports["name"] and module.exports['name']/module.exports["name"] patterns, using the same reserved-word filtering and push_unique deduplication as the dot-notation path.
Unit and regression tests for bracket-notation CJS
crates/perry/src/commands/compile/cjs_wrap/mod.rs
Tests verify detection of bracket-form module['exports']/module["exports"] and exports['foo'], extraction of bracket-notation named exports while rejecting dynamic keys (module[k], exports[k]), acceptance of bracket forms in extract_single_module_exports_assignment, and that wrap output for a bracket-form assignment includes export default _cjs; and parses as valid TypeScript.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related issues

Poem

🐇 Hop, hop! No more dot-only dreams,
Brackets and quotes join the CJS schemes!
module['exports'] now passes the test,
Regex regex, we do what is best.
Named exports flow through the bracket-shaped gate —
This bunny's delighted the parsing is great! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: extending CJS detection to recognize bracket/computed-string-literal export forms (#5275).
Description check ✅ Passed The description comprehensively covers root cause, fix details, test evidence, and risk assessment, though it lacks explicit completion checkmarks for the test plan section.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cjs-bracket-exports

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: 2

🤖 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/cjs_wrap/detect.rs`:
- Around line 83-91: The has_bracket_cjs_export function applies regex patterns
directly to raw source code, causing false positives when the patterns appear in
comments or strings. Modify the function to preprocess the source code to remove
comments (while preserving string literals for accurate quoted-key matching), or
add a lexical guard that validates regex matches are not within comment blocks
before returning true. This ensures that patterns like module['exports'] in
comments like // module['exports'] = x are not incorrectly classified as real
CommonJS exports.
- Around line 86-89: The regex patterns for detecting CommonJS exports don't
account for optional whitespace between the identifier and the opening bracket,
so they miss valid JavaScript like `module ['exports'] = …` and `exports
['name'] = …`. Update both regex patterns: in the first pattern that currently
requires `module[` with no gap, add `\s*` after `module` to allow optional
whitespace before the bracket, and in the second pattern that requires
`exports[` with no gap, add `\s*` after `exports` to allow optional whitespace
before the bracket.
🪄 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: 552e79ed-bf4d-40f4-b3a0-c132171550cb

📥 Commits

Reviewing files that changed from the base of the PR and between d46feff and 08df849.

📒 Files selected for processing (3)
  • crates/perry/src/commands/compile/cjs_wrap/detect.rs
  • crates/perry/src/commands/compile/cjs_wrap/extract_exports.rs
  • crates/perry/src/commands/compile/cjs_wrap/mod.rs

Comment on lines +83 to +91
fn has_bracket_cjs_export(source: &str) -> bool {
// `module['exports'] = …` / `module["exports"] = …` (default export).
let module_default =
regex::Regex::new(r#"\bmodule\[\s*['"]exports['"]\s*\]\s*="#).unwrap();
// `exports['name'] = …` / `module.exports['name'] = …` (named export).
let named =
regex::Regex::new(r#"\bexports\[\s*['"][A-Za-z_$][A-Za-z0-9_$]*['"]\s*\]\s*="#).unwrap();
module_default.is_match(source) || named.is_match(source)
}

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

Bracket CJS detection can false-positive on comment/string text.

The new matcher runs on raw source, so text like // module['exports'] = x (or a string literal containing that snippet) can classify the file as CJS and force wrapping even when no real export assignment exists.

Please run the bracket matcher on a comment-stripped view (while preserving string-literal text needed for quoted-key matching), or add a lightweight lexical guard before accepting regex hits.

🤖 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/cjs_wrap/detect.rs` around lines 83 - 91,
The has_bracket_cjs_export function applies regex patterns directly to raw
source code, causing false positives when the patterns appear in comments or
strings. Modify the function to preprocess the source code to remove comments
(while preserving string literals for accurate quoted-key matching), or add a
lexical guard that validates regex matches are not within comment blocks before
returning true. This ensures that patterns like module['exports'] in comments
like // module['exports'] = x are not incorrectly classified as real CommonJS
exports.

Comment thread crates/perry/src/commands/compile/cjs_wrap/detect.rs Outdated
Ralph Küpper added 2 commits June 17, 2026 05:09
`module['exports'] = X` / `module["exports"] = X` and
`exports['name'] = X` / `exports["name"] = X` are semantically
identical to the dot forms but were not recognized as CJS export
assignments, so a file using them (e.g. @colors/colors's
lib/custom/trap.js: `module['exports'] = function runTheTrap`) fell
through to the ESM pipeline — bare `module`/`exports` threw at init and
the default export never registered. This blocked winston (via
@colors/colors).

Two-part fix, both in the cjs_wrap layer:

1. Detection (detect.rs): is_commonjs now recognizes the bracket forms.
   strip_comments_and_strings blanks the quoted key, so the match runs
   on the original source via a regex that requires an `=` (assignment)
   and a string-literal key — a dynamic `module[k] = X` stays ESM.
2. Extraction (extract_exports.rs): extract_single_module_exports_assignment
   accepts `module['exports'] = Ident` (class-identity default path), and
   extract_exports_from_source surfaces `exports['name']`/`module.exports['name']`
   named exports. Dynamic (non-literal) keys are not extracted.

Inside the IIFE wrap, `module`/`exports` are ordinary locals, so the
bracket assignment executes correctly at runtime and `export default _cjs`
/ `export const name = _cjs.name` resolve — no HIR lowering change needed.

Tests: 8 new cjs_wrap unit tests; e2e mb repro (`hi x`), named
`exports['foo']` (`foo x`), double-quote `module["exports"]` (`dq x`),
dot-form regression, dynamic-key regression — all match Node.

No version/CHANGELOG/Cargo.lock edits (maintainer folds in at merge).
@proggeramlug proggeramlug force-pushed the fix/cjs-bracket-exports branch from b900da9 to 148f896 Compare June 17, 2026 03:10
@proggeramlug proggeramlug merged commit 26a68b2 into main Jun 17, 2026
14 of 15 checks passed
@proggeramlug proggeramlug deleted the fix/cjs-bracket-exports branch June 17, 2026 03:52
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.

1 participant