Skip to content

fix(node): use fs.readFileSync in CJS module loader for monkey-patch compat#33210

Open
bartlomieju wants to merge 1 commit intomainfrom
fix/require-use-fs-read-file-sync
Open

fix(node): use fs.readFileSync in CJS module loader for monkey-patch compat#33210
bartlomieju wants to merge 1 commit intomainfrom
fix/require-use-fs-read-file-sync

Conversation

@bartlomieju
Copy link
Copy Markdown
Member

Summary

  • Deno's CJS require system was using op_require_read_file (a direct Rust op)
    to read module source files, bypassing the node:fs module entirely. This
    broke tools like vue-tsc (@volar/typescript) that monkey-patch
    fs.readFileSync to intercept and transform source files during require().
  • Switch to fs.readFileSync in the CJS/JSON extension handlers so that
    monkey-patches are respected. Fall back to op_require_read_file for virtual
    files (like $deno$eval.cjs) that don't exist on disk.

Root cause

@volar/typescript's runTsc() works by monkey-patching fs.readFileSync to
intercept reads of TypeScript's tsc.js and return a transformed version with
Volar's proxyCreateProgram injected. Under Node.js this works because CJS
Module._extensions handlers use fs.readFileSync to read source files. Under
Deno, the handlers used op_require_read_file which goes directly to Rust,
so the monkey-patch was never triggered and tsc.js ran unmodified without
Vue SFC support.

Closes #30977

Test plan

  • New spec test require_fs_readfilesync_intercept verifies that
    monkey-patching fs.readFileSync is respected when require() loads
    .cjs and .json files
  • Existing CJS spec tests pass (cjs_sub_path, cjs_with_deps,
    dual_cjs_esm, translate_cjs_to_esm, deno_run_cowsay, etc.)
  • Virtual file loading (deno eval with CJS) still works via fallback

🤖 Generated with Claude Code

…compat

Deno's CJS require system was using `op_require_read_file` (a direct Rust
op) to read module source files, bypassing the `node:fs` module entirely.
This broke tools like vue-tsc (@volar/typescript) that monkey-patch
`fs.readFileSync` to intercept and transform source files during require().

Switch to `fs.readFileSync` in the CJS/JSON extension handlers so that
monkey-patches are respected. Fall back to `op_require_read_file` for
virtual files (like `$deno$eval.cjs`) that don't exist on disk.

Closes #30977

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@bartlomieju
Copy link
Copy Markdown
Member Author

The CI failures are all CJS module loading regressions across every platform. The common pattern is that files requiring transpilation (.ts, .cts, .tsx, .jsx) are being fed to V8 as raw source instead of transpiled JavaScript.

Failing tests:

  • specs::compile::cjsrequire("./subtract.ts") gets raw TS → SyntaxError: Unexpected token ':'
  • specs::compile::package_json_type — same
  • specs::node::unstable_detect_cjs::no_type_fieldSyntaxError: Cannot use import statement outside a module
  • specs::npm::dual_cjs_esm::ts_referrer_type_cjs — same
  • specs::run::cts::import_export_equals::main.cts with import/export not transpiled
  • specs::run::package_json_type::commonjs::jsx — JSX not transpiled
  • specs::run::wasm_module::cjs_importing

Root cause: op_require_read_file is an internal op that returns transpiled content (TS→JS, JSX→JS, etc.), while fs.readFileSync reads raw source from disk. The readFileForRequire() function needs to only use fs.readFileSync for file types that don't need transpilation (.js, .cjs, .json) and keep using op_require_read_file for everything else (.ts, .cts, .tsx, .jsx, .mts).

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.

Deno's Vue template is broken and typescript checking/linting doesn't work for .Vue files

1 participant