Investigation of denoland/deno#30977
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.
vue-tsc uses @volar/typescript's runTsc() function. The patching mechanism is:
- Monkey-patch
fs.readFileSyncto intercept reads of TypeScript'stsc.js - Call
require(tscPath)which triggers the patchedreadFileSync - The intercepted read returns a transformed version of
tsc.jswith:.vueadded tosupportedTSExtensions,supportedJSExtensions,allSupportedExtensionscreateProgramreplaced with Volar'sproxyCreateProgramwrapperchangeExtensionpatched to handle.vueextensions
- The modified
tsc.jsruns 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];
}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.jsviafs.readFileSync-> intercepted by Volar's monkey-patch -> returns transformed source with Vue support -> vue-tsc works - Deno: reads
tsc.jsviaop_require_read_file(Rust op) -> monkey-patch is completely bypassed -> vanillatsc.jsis loaded -> .vue files ignored
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.
-
Use
fs.readFileSyncin the CJS loader: Changeop_require_read_filecalls inModule._extensionshandlers to go through thenode:fspolyfill instead, so monkey-patches are respected. This would match Node.js behavior but could have performance implications. -
Add a hook point: Provide a way for
Module._extensionshandlers to use a customizable file reader that defaults to the fast Rust op but can be overridden. -
Detect the specific pattern: Check if
fs.readFileSynchas been monkey-patched and fall back to using it when loading CJS modules (similar to howwrapSafe()already detectsModule.wrappatches via thepatchedflag).