feat(js-exec): give fs node's shapes: Stats and Dirent, readdir options, errno errors, fs/promises - #429
feat(js-exec): give fs node's shapes: Stats and Dirent, readdir options, errno errors, fs/promises#429mutewinter wants to merge 3 commits into
Conversation
…ns, errno errors, fs/promises
statSync returned { isFile: boolean, ..., mtime: string }, so node code calling stat.isFile() got `not a function`; readdirSync ignored its options, so { withFileTypes: true } handed back strings whose isDirectory() was not a function and { recursive: true } listed the top level only, silently; an fs error was a plain Error with no code and a path relative to its mount; and fs/promises was a missing module.
Stats and Dirent are now node's, with methods and Dates. withFileTypes and recursive are answered in one bridge call, the recursive walk through traverseFileTree under the traversal limits, without following symlinked directories. An error carries code, errno, syscall, path, and dest, and its message names the path as passed. fs/promises resolves for require and import.
Two things fell out of the error shape. run copies a guest error's own code onto the RunError, so the RUN_ERROR check misread any coded guest error as a host failure and printed it without a location; anything outside run's RUN_* codes is now the guest's. And a failure inside the runtime's own shims was located at a line inside them (fail.js:1:71 for a call on line 2), so the location now skips the setup frames.
|
@mutewinter is attempting to deploy a commit to the Vercel Labs Team on Vercel. A member of the Team first needs to authorize it. |
🤖 auto-maintain reviewAutomated, advisory triage for
Review panel: 🟡 medium highest severity just-bash maintainer code review: 🟡 medium
General code review: 🟡 medium
Adversarial security: 🟡 medium
Adversarial security (second opinion): 🟡 medium
Standard Bash and host portability: 🟢 low
Posted by auto-maintain. This automated code review is advisory; a human maintainer makes the call. |
… settle the review's edges A recursive listing of a symlink to a directory returned [] because traverseFileTree refuses to enter a symlinked root; the root is now realpath'd first, and symlinks below it are still listed rather than entered, as node does. The lstat-per-entry fallback runs under a FileTraversalBudget so one bridge call cannot outrun the traversal limits. The errno regex takes an apostrophe in a path, each Stats time is its own Date, fs.promises.unlink and rmdir report their own syscall, and a guest error whose code is not a string no longer breaks the reporter.
|
All six findings held, and the second commit takes them, with a test for each. The symlinked root was the real one: The lstat-per-entry fallback (a filesystem without The rest: js-exec suite: 319 passed across 14 files, up from 315; |
traverseFileTree on a file visits the root alone, which the listing skips, so readdirSync(file, { recursive: true }) answered [] where the other two branches throw ENOTDIR.
Problem
js-exec --helplistsstatSyncandreaddirSyncunder "Node.js Compatibility", and code written for node fails on both:The recursive and error-code cases are silent: the script runs to completion with the wrong answer. Both bit an agent in a real transcript before the loud ones did.
Cause
The guest
fsinrun-runtime.tsreturnshost.fsStat's serialized{ isFile: boolean, ..., mtime: string }as the stat, passesreaddirSync's second argument nowhere, and throwsnew Error(result.error)whereresult.erroris the filesystem's message with the path relative to the mount it landed on.BUILTIN_EXPORTShasfsbut notfs/promises. The README documents the boolean shape and the tests assert it (console.log(s.isFile)printstrue), so this changes a documented shape: node's is the one the help text promises.Two more surfaced once errors carried a
code.runcopies a guest error's owncodeonto theRunError(errors.js,serializeError), soisGuestError = RunError.isInstance(error) && error.code === "RUN_ERROR"misreads any coded guest error as a host failure and printsjs-exec: <message>with no location; that is reachable today by any script that setserror.codeitself. AndformatGuestErrortakes the innermost frame, which for a failure inside the runtime's own shims is a line in the entry source above the script, clamped to line 1: an uncaughtfs.statSyncon line 2 offail.jsreportedat /home/user/fail.js:1:71.Fix
fs.Statsandfs.Direntwith node's methods (isFile(),isDirectory(),isSymbolicLink(), and the device queries returning false) and Date times (mtime,mtimeMs,atime,ctime,birthtime).readdirSync(path, { withFileTypes, recursive })is answered in one bridge call: the host usesreaddirWithFileTypeswhen the filesystem offers it andreaddirpluslstatotherwise, and the recursive walk goes throughtraverseFileTreeunder the traversal limits without following symlinked directories, as node does not.parentPathis the path as given, joined. A failed call throws an error withcode,errno,syscall,path(anddestfor the two-path calls), its message rebuilt asENOENT: no such file or directory, open '<path as passed>'.fs/promisesresolves forrequireandimport. Guest errors are anything outside run's ownRUN_*codes, and the location parser skips frames in the setup source and the bootstrap module.Scope
Unchanged: the names
readdirSyncreturns without options,existsSync, the callback-form errors, thefs.promiseswrappers (they now route through the same shapes), and the bridge and traversal limits, which the recursive listing consumes rather than bypasses.Breaking:
stat.isFileread as a boolean is now a function, so always truthy. The README, the directory-tree example, and four test assertions are updated; anything else reading the boolean needs the call. If that is unwelcome,Stats/Direntsplit out cleanly and the rest (errors,fs/promises, the two located bugs) stands on its own; say the word.Not addressed, deliberately:
readdirSyncon a file still returns[]rather than ENOTDIR;unlinkSync/rmdirSynckeep theirrmSyncsemantics (only the syscall name in the message differs); an uncaught error's message still passes throughsanitizeErrorMessage, which rewrites/home/...and/tmp/...paths to<path>on the assumption that they are host paths, so the new test for the uncaught case lives under/work.Tests
js-exec.fs.test.ts: aStatswith Dates andinstanceof,withFileTypes,recursivewith and withoutwithFileTypes,parentPathfor a relative path,code/errno/syscall/pathon a missing file,pathanddestonrenameSync,fs.promises.accessrejecting with the same shape, and an uncaught error located atfail.js:2:9.js-exec.node-compat.test.ts:fs/promisesbyrequireand byimport, identical tofs.promises. What they cannot prove: behavior on a filesystem withoutreaddirWithFileTypesbeyond the in-memory one, and the recursive walk against the traversal limits on a large tree. js-exec wasm suite 315 passed across 14 files; the three js-exec security suites 13 passed;tsc, biome, andlint:bannedclean.Authored with Claude Opus 5