Skip to content

fix: #845 — cjs_wrap no longer corrupts const X = require('Y').Z; and module.exports.default#881

Merged
proggeramlug merged 4 commits into
mainfrom
worktree-fix-845-cjs-wrap-parse-error
May 16, 2026
Merged

fix: #845 — cjs_wrap no longer corrupts const X = require('Y').Z; and module.exports.default#881
proggeramlug merged 4 commits into
mainfrom
worktree-fix-845-cjs-wrap-parse-error

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

Closes #845. Two parse-failure sub-bugs + one ergonomics miss under perry.compilePackages, both surfaced via real mysql2 / pino packages.

  • mysql2 — TS1109. extract_require_aliases_with_ranges greedy-matched const EventEmitter = require('events').EventEmitter; as const EventEmitter = require('events') (the regex's ;? happily stopped at )). The hoisted-class blanking pass then replaced that match with spaces, leaving .EventEmitter; dangling at column 0 of the wrap output. Fix: require ; or end-of-line after ) — trailing .X member access disqualifies the match.
  • pino — ExpectedIdent. module.exports.default = pino (ESM-interop) flowed into named_export_decls as export const default = _cjs.default;, which is invalid syntax. Fix: new is_js_reserved_word helper; push_unique skips reserved-word names (same precedent as the existing __esModule skip). The separate default_export_decl still covers default exports. direct_named_reexports (export { _req_N as NAME }) stays unfiltered — as default IS valid in export clauses.
  • Ergonomics. New annotate_parse_error helper fires only when cjs_wrap actually ran. Appends a note explaining the wrap, both byte sizes (wrap output vs on-disk), how to dump it (PERRY_DEBUG_CJS_WRAP=1), and a 5-line excerpt with the offending line marked >>>. Pass-through for non-wrapped sources.

Example post-fix message for a deliberately-broken compilePackages target:

Error: Failed to parse .../badpkg/index.js: Parse error: Error { error: (241..242, ExpectedIdent) }
note: this file is inside a `compilePackages` target and was rewritten by Perry's CJS-to-ESM wrap before parsing. The error offset above refers to the post-wrap source (350 bytes), NOT the 45-byte file on disk. Re-run with `PERRY_DEBUG_CJS_WRAP=1` to see the full wrap output.
wrap-output excerpt around the error offset:
       11 |
       12 |     exports.foo = 1;
>>>    13 | class @!!! invalidsyntax {}
       14 |

Test plan

  • cargo test --release -p perry --bin perry commands::compile::cjs_wrap — 41/41 green (4 new tests: regex tightening, full wrap→parse for mysql2 shape, reserved-word filter, full wrap→parse for pino shape).
  • cargo test --release --workspace minus cross-host UI crates — 836/0 green.
  • cargo test --release -p perry-runtime --lib -- --test-threads=1 — 255/0 green (a parallel-run TUI test is a pre-existing shared-SLOTS flake, unrelated).
  • /tmp/run_gap_tests.sh — 35/36, unchanged from baseline (lone fail is the pre-existing test_gap_console_methods).
  • Real-package repro at /tmp/repro845 with mysql2 from local node_modules: pre-fix produces the exact issue error (35073..35074, TS1109) at offset past EOF; post-fix advances past the parse step (hits a separate downstream HIR bug at _connectionId++ — out of scope here).
  • Real-package repro at /tmp/repro845 with pino: pre-fix produces (8282..8289, ExpectedIdent) past EOF; post-fix compiles to a 2.5 MB executable.
  • Ergonomics-path repro at /tmp/repro845b with a deliberately-broken compilePackages target: produces the annotated error message shown above.

Per CLAUDE.md's external-contributor convention, this PR does NOT touch Cargo.toml [workspace.package].version, CLAUDE.md's **Current Version:** line, or CHANGELOG.md. The maintainer bumps + writes the changelog at merge time.

Refs #804 (drizzle e2e — mysql2 was the first parse blocker), #793 (compat roadmap).

…nd `module.exports.default`

Two parse-failure sub-bugs and one ergonomics miss surfaced by mysql2 and
pino under `perry.compilePackages`. All three produced byte offsets past
EOF of the original file, because the SWC error refers to the post-wrap
source while the message names the on-disk path.

mysql2 — TS1109 "Expression expected". The require-alias regex in
extract_require_aliases_with_ranges greedy-matched
`const EventEmitter = require('events').EventEmitter;` as
`const EventEmitter = require('events')` (`;?` stopped at `)`). The
blanking pass that strips inner `var X = require(...)` (so the hoisted-
class machinery's module-scope alias doesn't get shadowed) then replaced
the match with spaces, leaving `.EventEmitter;` dangling at column 0 of
the wrap output. Fix: require either `;` or end-of-line after `)` so
trailing `.X` member access disqualifies the match — those bind a
property, not the module object, so the alias-rename pass would be wrong
anyway. Whole-statement aliases (incl. same-line follow-on statements
like `var dep = require('./dep'); module.exports = dep.value;`) still
match.

pino — ExpectedIdent on `default`. pino.js ends with
`module.exports.default = pino` (ESM-interop convention), and
extract_exports_from_source surfaced `default` as a named export;
named_export_decls then emitted `export const default = _cjs.default;`,
which is invalid syntax. Fix: new is_js_reserved_word helper; push_unique
skips reserved-word names (same precedent as the existing __esModule
skip). `default` is the only common real-world case; the separate
default_export_decl emits `export default _cjs;` so nothing is lost. The
direct_named_reexport emission (`export { _req_N as NAME }`) is
intentionally unfiltered — `as default` / `as class` IS valid in export
clauses.

Ergonomics — new annotate_parse_error helper in collect_modules.rs that
fires only when cjs_wrap actually ran. It regex-pulls the `(lo..hi, …)`
span out of SWC's Debug-format, then appends a note explaining the wrap,
both byte sizes (wrap output vs on-disk), and a 5-line excerpt of the
wrap output around the offending offset with the failing line marked
`>>>`. Plus a `PERRY_DEBUG_CJS_WRAP=1` pointer. Pass-through for
non-wrapped sources.

Tests: four new unit tests in cjs_wrap::tests
(require_alias_extract_skips_trailing_member_access,
wrap_does_not_dangle_member_access_after_blanking,
extract_exports_skips_default_reserved_word, wrap_pino_shape_parses_cleanly).
End-to-end: real mysql2 / pino packages from local node_modules — pre-fix
produces the issue's exact two errors; post-fix pino compiles to an
executable and mysql2 advances past the parse step (hits a downstream
HIR bug at `_connectionId++` — separate, out of scope).

Closes #845. Refs #804 (drizzle e2e — mysql2 was the first parse blocker),
#793 (compat roadmap).
@proggeramlug proggeramlug merged commit 5eda9a5 into main May 16, 2026
1 check passed
@proggeramlug proggeramlug deleted the worktree-fix-845-cjs-wrap-parse-error branch May 16, 2026 20:12
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.

mysql2 fails to compile under perry.compilePackages: TS1109 parse error in base/connection.js (and error offset is past EOF)

1 participant