Skip to content

Fix require.cache/extensions missing in bundle-served CJS modules#53

Merged
ChALkeR merged 1 commit into
mainfrom
claude/npm-corpus-bundle-test-vd901o
Jul 1, 2026
Merged

Fix require.cache/extensions missing in bundle-served CJS modules#53
ChALkeR merged 1 commit into
mainfrom
claude/npm-corpus-bundle-test-vd901o

Conversation

@exo-nikita

Copy link
Copy Markdown
Collaborator

Summary

Fixes a Node.js compatibility issue (node:59666) where CommonJS modules served from bundles via --bundle=load receive a re-invented require() function that lacks the .cache and .extensions properties. This causes crashes in packages like import-fresh that rely on these properties.

Changes

  • Added CJS_REQUIRE_REPAIR prelude: A minimal inline repair that restores require.cache and require.extensions by pointing them to Node's internal Module._cache and Module._extensions from the node:module builtin.

  • Implemented repairCjsRequire() function: Intelligently injects the repair prelude into CommonJS source code while preserving line numbers and respecting shebangs and 'use strict' directives. The prelude is inserted after any leading shebang and optional 'use strict' directive to avoid demoting the directive.

  • Applied repair in load hook: When serving CommonJS modules (both commonjs and commonjs-typescript formats) via the load hook, the source is now automatically repaired before being returned to Node's ESM→CJS translator.

  • Added comprehensive test coverage: Two new tests verify:

    1. Bundle-served CJS modules receive a properly shaped require() with .cache and .extensions properties
    2. The import-fresh-style pattern of deleting from require.cache and re-requiring works without crashing

Implementation Details

  • The repair is guarded with typeof and try/catch to gracefully degrade if require is missing or the builtin module is unavailable
  • Line numbers are preserved by avoiding newline insertion, ensuring stack traces and source maps remain accurate
  • The fix is confined to executable CommonJS formats; ESM modules are unaffected since they have no require

https://claude.ai/code/session_01GPC57FRzjwxtfqJGP69Bsk

@exo-nikita exo-nikita force-pushed the claude/npm-corpus-bundle-test-vd901o branch 5 times, most recently from f6dae35 to 009f49a Compare July 1, 2026 13:01
When a load hook supplies `source` for a CommonJS module, Node's ESM->CJS
translator (loadCJSModule) hands the module a re-invented require() that sets
only `.resolve`/`.main` and omits `.cache` and `.extensions` (node:59666) --
unlike the require a disk-loaded CJS module gets. bundle=load is the only mode
that serves CJS via a source override, so packages that read those properties
(e.g. import-fresh, which does `require.cache[require.resolve(id)]`) crashed
with "Cannot read properties of undefined" only under bundle=load.

Repair the two properties from node:module before user code runs, via a
single-line prelude prepended to the served CJS source. To keep a `'use strict'`
directive in effect, the prelude is inserted after the module's Directive
Prologue and prefixed with `;`; it adds no line break so stack traces / source
maps keep their line numbering.

Telling a directive apart from the head of a leading string expression is the
ASI problem, so the prologue is walked by a small scanner (whitespace, line/block
comments, and string-literal directive STATEMENTS). A leading string is a
directive only when a real statement boundary follows it: `;`, `}`, a line
comment, EOF, or a LINE boundary -- a line terminator, or a block comment that
contains one -- whose next token does not continue the expression (punctuators
like `.`/`+`/`,` or the `in`/`instanceof` keyword operators). So a module that
opens with a string expression -- `'x'.toUpperCase()`, `'a' + b`, `'a,b'`
newline-then-`.split()`, `'x'` newline-then-`instanceof Y` -- is left intact
instead of being spliced mid-expression (a SyntaxError or silently wrong bytes),
and a real directive followed by a multi-line banner comment still keeps strict
mode. The load hook keys the repair off a named CJS_FORMATS set, consistent with
the file's other format sets.

Tests:
- a bundle-served CJS module now sees require.cache / require.extensions that
  are the real Module._cache/_extensions;
- the import-fresh-style `delete require.cache[...]; require(...)` pattern
  completes under bundle=load;
- directive-prologue edge cases: a directive behind a leading comment stays
  strict, a comment-trailed directive loads without crashing, single-line and
  multi-line leading string expressions (including `.`/`in` continuations) are
  left intact, and a directive followed by a multi-line block comment stays
  strict;
- an end-to-end test exercises the real import-fresh@3.3.1 package (vendored
  with its resolve-from / parent-module / callsites deps under the fixture's
  node_modules), reproducing identical output from a bundle with node_modules
  removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GPC57FRzjwxtfqJGP69Bsk
@exo-nikita exo-nikita force-pushed the claude/npm-corpus-bundle-test-vd901o branch from 009f49a to ebde842 Compare July 1, 2026 13:21
@ChALkeR ChALkeR merged commit 4b3e489 into main Jul 1, 2026
5 checks passed
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.

3 participants