Repository navigation
Description
- Repository:
pingdotgg/ts-rust - Target commit:
0ffd46271f41f0c448b10ee154339b88cd34ee8a - File:
npm/wasm/node-run.js:67
Summary
nodeFileSystem().writeFile first tries the write; when the parent is missing it creates the directory and retries. The error path that formats the final failure always uses the literal operation open, regardless of whether the failure happened in the open/parent-creation phase or in the actual write syscall. An ENOSPC during the write therefore surfaces as open /dev/full: no space left on device, while the Go/Rust contract for that failure names write, so callers cannot distinguish a path problem from a full device.
Affected code
npm/wasm/node-run.js:
59 function statOrUndefined(stat, path) {
60 try {
61 return stat(path);
62 } catch {
63 return undefined;
64 }
65 }
66
67 /** The real file system through node:fs, for `runTsc`. */
68 export function nodeFileSystem() {
69 const kind = entry =>
70 entry.isDirectory() ? "directory" : entry.isFile() ? "file" : entry.isSymbolicLink() ? "symlink" : "other";
71 return {
72 readFile(path) {
73 try {
74 return fs.readFileSync(path);
75 } catch {
76 return undefined;
77 }
78 },
79 stat(path) {
80 try {
81 const stat = fs.statSync(path, { bigint: true });
82 return {
83 isDirectory: stat.isDirectory(),
84 isFile: stat.isFile(),
85 size: Number(stat.size),
86 mtimeNs: stat.mtimeNs,
87 };
88 } catch {
89 return undefined;
90 }
91 },
92 readDirectory(path) {
93 try {
94 return fs.readdirSync(path, { withFileTypes: true }).map(entry => ({ name: entry.name, kind: kind(entry) }));
95 } catch {
96 return undefined;
97 }
98 },
99 realpath(path) {
100 try {
101 return fs.realpathSync.native(path).replaceAll("\\", "/");
102 } catch {
103 return undefined;
104 }
105 },
106 // As Go's osvfs writeFileEnsuringDir (internal/vfs/osvfs/os.go:163):
107 // write, and when that fails, make the directory and write again.
108 writeFile(path, data, append) {
109 const write = () => (append ? fs.appendFileSync : fs.writeFileSync)(path, data);
110 try {
111 return write();
112 } catch {
113 // Make the directory below.
114 }
115 const mkdirError = mkdirAll(dirname(path));
116 if (mkdirError) return mkdirError;
117 try {
118 return write();
119 } catch (error) {
120 return goError("open", path, error);
121 }
122 },
123 remove(path) {
124 try {
125 fs.rmSync(path, { recursive: true, force: true });
126 } catch {
127 return false;
128 }
129 },
130 chtimes(path, atimeNs, mtimeNs) {
131 try {
132 const stat = fs.statSync(path, { bigint: true });
133 const seconds = ns => Number(ns) / 1e9;
134 fs.utimesSync(path, seconds(atimeNs ?? stat.atimeNs), seconds(mtimeNs ?? stat.mtimeNs));
135 } catch (error) {
136 return goError("chtimes", path, error);
137 }
138 },
139 };
140 }
141
142 /** Go's osvfs check: the file system ignores case when the executable is also found with its case swapped. */
143 function caseInsensitive() {
144 const swapped = process.execPath.replace(/\w/g, c => (c === c.toUpperCase() ? c.toLowerCase() : c.toUpperCase()));
145 return swapped !== process.execPath && fs.existsSync(swapped);
146 }
Expected behavior
Failed host writes must report Go's operation-specific error text, using 'open' for open failures and 'write' for write failures, so the diagnostic consumed by tsc matches Go's error text.
Actual behavior
When a host write reaches the write syscall and fails there, e.g. ENOSPC during fs.writeFileSync or appendFileSync after the parent directory exists, nodeFileSystem.writeFile catches the error and unconditionally emits 'open : '. The supplied reference Go implementation would emit 'write : ' for that failure. core.d.ts requires Go's error text from writeFile, and writeResult, host_error, fs_error_text, and emitter-js transfer the already-mislabeled text to the TS5033 diagnostic, producing a producer/consumer contract mismatch.
How to reproduce
Call nodeFileSystem().writeFile("/dev/full","hello",false) from Node; the error text starts with "open" rather than "write".
Root cause
nodeFileSystem().writeFile first tries the write; when the parent is missing it creates the directory and retries. The error path that formats the final failure always uses the literal operation open, regardless of whether the failure happened in the open/parent-creation phase or in the actual write syscall. An ENOSPC during the write therefore surfaces as open /dev/full: no space left on device, while the Go/Rust contract for that failure names write, so callers cannot distinguish a path problem from a full device.
Suggested fix
Keep the failing operation in nodeFileSystem().writeFile. When the write syscall fails, format the error as write <path>: <errno>; retain open only for failures during the open/parent-creation phase. Add a test writing to /dev/full that asserts the prefix.
Activity