Skip to content

Instantly share code, notes, and snippets.

@bartlomieju
Last active April 8, 2026 09:32
Show Gist options
  • Select an option

  • Save bartlomieju/7b5af287b4cc395677f9d0d8fafe5d19 to your computer and use it in GitHub Desktop.

Select an option

Save bartlomieju/7b5af287b4cc395677f9d0d8fafe5d19 to your computer and use it in GitHub Desktop.
Investigation: Why vue-tsc ignores .vue files under Deno (denoland/deno#30977)

Why vue-tsc ignores .vue files under Deno

Investigation of denoland/deno#30977

Summary

vue-tsc -b under Deno silently ignores .vue files because Deno's CJS require system bypasses fs.readFileSync, which @volar/typescript relies on to inject its TypeScript compiler patches.

How vue-tsc works

vue-tsc uses @volar/typescript's runTsc() function. The patching mechanism is:

  1. Monkey-patch fs.readFileSync to intercept reads of TypeScript's tsc.js
  2. Call require(tscPath) which triggers the patched readFileSync
  3. The intercepted read returns a transformed version of tsc.js with:
    • .vue added to supportedTSExtensions, supportedJSExtensions, allSupportedExtensions
    • createProgram replaced with Volar's proxyCreateProgram wrapper
    • changeExtension patched to handle .vue extensions
  4. The modified tsc.js runs with full Vue SFC support

The relevant code from @volar/typescript/lib/quickstart/runTsc.ts:

const readFileSync = fs.readFileSync;

(fs as any).readFileSync = (...args: any[]) => {
  if (args[0] === tscPath) {
    let tsc = (readFileSync as any)(...args) as string;
    return transformTscContent(tsc, proxyApiPath, extraSupportedExtensions, ...);
  }
  return (readFileSync as any)(...args);
};

try {
  return require(tscPath);  // This triggers the patched readFileSync
} finally {
  (fs as any).readFileSync = readFileSync;
  delete require.cache[tscPath];
}

The root cause

Deno's CJS require system reads files using op_require_read_file -- a direct Rust op that bypasses the node:fs module entirely.

From ext/node/polyfills/01_require.js:

function loadMaybeCjs(module, filename) {
  const content = op_require_read_file(filename);  // Direct Rust op, NOT fs.readFileSync!
  const format = op_require_is_maybe_cjs(filename) ? undefined : "module";
  module._compile(content, filename, format);
}

When require(tscPath) is called inside runTsc:

  • Node.js: reads tsc.js via fs.readFileSync -> intercepted by Volar's monkey-patch -> returns transformed source with Vue support -> vue-tsc works
  • Deno: reads tsc.js via op_require_read_file (Rust op) -> monkey-patch is completely bypassed -> vanilla tsc.js is loaded -> .vue files ignored

Impact

This doesn't just affect vue-tsc. Any tool that relies on monkey-patching fs.readFileSync to intercept require() file reads will be broken under Deno. This is a fundamental incompatibility in how Deno's CJS loader reads source files.

Possible fixes

  1. Use fs.readFileSync in the CJS loader: Change op_require_read_file calls in Module._extensions handlers to go through the node:fs polyfill instead, so monkey-patches are respected. This would match Node.js behavior but could have performance implications.

  2. Add a hook point: Provide a way for Module._extensions handlers to use a customizable file reader that defaults to the fast Rust op but can be overridden.

  3. Detect the specific pattern: Check if fs.readFileSync has been monkey-patched and fall back to using it when loading CJS modules (similar to how wrapSafe() already detects Module.wrap patches via the patched flag).

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