feat!: lint through an injected file system in a host-supplied worker - #21
Merged
Merged
Conversation
readBuildInfo() read dist/build-info.json with node:fs, which ties the build info to Node and to a file that has to travel with the module. `npm run build` now turns dist/build-info.json into a typed BUILD_INFO constant, exported from the package entry, and fails if the JSON does not describe dist/shellcheck.wasm or the pinned ShellCheck version. build-info.json stays a GitHub Release asset but leaves the tarball. BREAKING CHANGE: the ./build-info subpath and readBuildInfo() are removed; import BUILD_INFO from the package entry instead.
The extension is moving to lint documents of any URI scheme by reading
them through vscode.workspace.fs, and later to run on the web, so the
package can no longer reach for node:fs or hand back a synchronous
runner that blocks the caller.
createShellCheck({ module, createWorker }) now owns one Worker at a
time and runs lints through it in FIFO order. Each lint may carry a
ShellCheckFileSystem (async stat, readFile, readDirectory) that is
mounted read-only at guest `/`. The guest's synchronous WASI calls reach
it through a SharedArrayBuffer bridge: the Worker posts a request and
waits in Atomics.wait while the caller's thread runs the async call,
with payloads over 1 MiB sent in chunks and answers cached for the rest
of the lint. `..` is folded lexically and may not leave `/`; symlink
containment is now the file system's job. An AbortSignal drops a queued
lint or terminates the Worker of the running one; a lost Worker is
replaced on the next lint. The Worker side ships as `./worker`
(startWorker), and no shipped module imports a Node built-in.
BREAKING CHANGE: the `./node` entry (loadModule, createReadOnlyPreopen,
wasmPath) and the synchronous run() export are removed. Use
createShellCheck() with a Worker whose entry calls startWorker(), and
pass the files ShellCheck may read as LintRequest.fs. LintResult carries
stdout and stderr as strings.
The bridge adds a round trip to the caller's thread for every file ShellCheck touches, so the redesign has to show it costs no more than 10% over the 0.1.1 node:fs runner. `npm run bench` installs 0.1.1 into .cache/bench, runs both in worker_threads Workers on generated 23, 307 and 1503-line scripts with a .shellcheckrc and a sourced file, and reports native ShellCheck and a `+RTS -A64m` run as data points.
A version with a prerelease suffix (0.2.0-next.0) would otherwise become `latest` on npm and a regular GitHub Release. Publish it with `--tag next` and mark its GitHub Release as a prerelease; stable versions publish as before.
Rewrite the README around createShellCheck, the Worker entry and the ShellCheckFileSystem contract, including the web requirement of cross-origin isolation for SharedArrayBuffer. Add ADR 0006 (the package is file-system agnostic), amend ADR 0005 now that the Worker and bridge live in the package while policy stays in the host, and note in ADR 0003 that the evidence against a reactor build measured a third-party build rather than the reactor model.
timonwong
added a commit
to vscode-shellcheck/vscode-shellcheck
that referenced
this pull request
Sep 29, 2026
Supersedes #1952. This branch contains all of #1952's commits; #1952 stays open until this lands. ## Summary Adds the experimental `shellcheck.runtime: "wasm"` from #1952, but the wasm runtime now reads every file through `vscode.workspace.fs` instead of `node:fs`. Documents of any URI scheme can be linted in wasm mode, including virtual workspaces. Nothing on the wasm path uses `node:fs`, `node:path` or `process.platform`, so VS Code for the Web (#478) only needs a Worker adapter and cross-origin isolation later. It consumes `@vscode-shellcheck/shellcheck-wasm@0.2.0-next.0` (vscode-shellcheck/shellcheck-wasm#21). That package now owns the Worker protocol and a SharedArrayBuffer bridge that turns the guest's synchronous WASI file calls into async `stat` / `readFile` / `readDirectory` calls on the extension host thread. ## Changes - **Worker:** `src/runtime/wasm/worker.ts` is now a 5-line `parentPort` adapter around the package's `startWorker`. The compiled Module is read with `workspace.fs.readFile` and posted to the Worker. #1952's `DataCloneError` fallback is gone: it was only defensive, and posting the Module works in the Electron extension host. - **File system adapter** (`workspace-fs.ts`): serves guest paths from the mount root over `workspace.fs` and maps `FileSystemError` codes. Symlinks are followed wherever the file system provider follows them; the realpath containment of #1952 is gone with `node:fs`. - **Mounts** (all schemes): a document inside a workspace folder mounts that folder; otherwise it mounts its own directory; `untitled:` documents get stdin only. `PWD` is computed on `Uri.path`, which replaces the win32 host-to-guest path mapping. - **Scheduling:** one lint in flight, latest request only per document (a newer request aborts the running one), the active editor's document jumps the queue, and a 30 s watchdog via `AbortSignal`. A lost Worker or a guest trap fails only that lint; the next lint gets a fresh Worker. - **Virtual workspaces:** `capabilities.virtualWorkspaces` is `"limited"`. With `runtime=native`, documents that are neither `file:` nor `untitled:` are skipped and logged, with no prompt. `ignoreFileSchemes` (git, gitfs, output) still applies to both runtimes. - **Docs:** `docs/plans/wasm-runtime-workspace-fs.md` records the design, including why `ms-vscode.wasm-wasi-core` was rejected. `docs/plans/wasm-runtime.md` is marked superseded. ## Reviewer notes - The bridge costs at most +5% per lint compared with the 0.1.1 `node:fs` runner (measured on Node 22/24; numbers in vscode-shellcheck/shellcheck-wasm#21). - The case-insensitive workspace-folder prefix match in `toGuestPath` exists for Windows drive-letter and path casing. It is only covered by unit tests, so the Windows CI job is the real check. - The native-runtime skip in virtual workspaces has no automated test, because the test harness cannot open a virtual workspace.
felipecrs
pushed a commit
to vscode-shellcheck/vscode-shellcheck
that referenced
this pull request
Sep 29, 2026
## [0.41.0](v0.40.1...v0.41.0) (2026-09-29) ### Features * experimental wasm variant of shellcheck ([#1954](#1954)) ([da735c6](da735c6)), closes [#1952](#1952) [#1952](#1952) [#478](#478) [vscode-shellcheck/shellcheck-wasm#21](vscode-shellcheck/shellcheck-wasm#21) [#1952](#1952) [vscode-shellcheck/shellcheck-wasm#21](vscode-shellcheck/shellcheck-wasm#21)
|
🎉 This PR is included in version 0.2.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
vscode-shellcheck is moving its experimental wasm runtime to
vscode.workspace.fs, so documents of any URI scheme (remote, virtual) can be linted, with VS Code for the Web as a later step. This makes the package file-system agnostic: no shipped entry importsnode:*, and the guest only sees files through aShellCheckFileSystemthe host passes with each lint.Changes
createShellCheck({ module, createWorker }).lint({ args, stdin, env, fs }, { signal }), one lint at a time in FIFO order. Aborting a running lint terminates its Worker and the next lint respawns it../workerexportsstartWorker(port). The host creates the Worker, so the package never touchesworker_threadsor DOM Worker APIs.Atomics.waiton one SharedArrayBuffer per Worker while the host thread services asyncstat/readFile/readDirectory(1 MiB chunks, per-lint cache). The read-only preopen is built on it...above/isENOTCAPABLE; symlinks are left to the file-system backend.BUILD_INFOis compiled in;./build-info,./node,run()and thenode:fspreopen are removed.build-info.jsonis no longer in the tarball (still attached to GitHub Releases).nextdist-tag and marks the GitHub Release as a prerelease. Version bumped to0.2.0-next.0.ms-vscode.wasm-wasi-coreor@vscode/sync-api-*), an amendment to ADR 0005 (Worker and bridge now live here; scheduling policy stays in the host), and a note on ADR 0003 (the earlier "reactor is slower" evidence came from a differently built third-party package).Performance
npm run bench(median ms per lint, mount with.shellcheckrc+ one sourced file):node:fsSmall scripts vary by about ±5% run to run. The Worker polls for up to 1 ms before sleeping on each answer; without it, small scripts were 11–13% slower.
+RTS -A64mwas measured and is 10–45% slower, so it is not used.Reviewer notes
SharedArrayBufferunder cross-origin isolation, which the web follow-up will have to deal with.readDirectoryis covered only by bridge and preopen tests; this ShellCheck build never lists directories.