Add wasip1 support, enabling CLI, API, browser, sync/async - #64174
Jake Bailey (jakebailey) wants to merge 43 commits into
Conversation
f6f443f to
cdbe79f
Compare
There was a problem hiding this comment.
🟡 Changes recommended
WASI LSP stdin busy-spins while idle, and process-backed API shutdown can hang during manually batched initialization.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds experimental WASI support for the TypeScript CLI and synchronous/asynchronous APIs across Node.js and browsers.
Changes:
- Adds a dual CLI/reactor
tsc.wasmbuild and minimal WASI host. - Introduces injectable API transports and browser-compatible clients.
- Adds packaging, publishing, CI, and integration-test coverage.
File summaries
| File | Description |
|---|---|
tsc/internal/ipc/timing.go |
Exposes server timing collection. |
tsc/internal/api/wasmreactor/reactor.go |
Implements the in-process API reactor. |
tsc/internal/api/wasmreactor/reactor_test.go |
Tests reactor requests and files. |
tsc/cmd/tsc/wasmapi_wasip1.go |
Defines the WebAssembly reactor ABI. |
tsc/cmd/tsc/sys.go |
Uses platform-aware working-directory lookup. |
tsc/cmd/tsc/notifycontext.go |
Provides native signal handling. |
tsc/cmd/tsc/notifycontext_wasip1.go |
Disables unsupported WASI signals. |
tsc/cmd/tsc/main.go |
Uses platform-specific notification contexts. |
tsc/cmd/tsc/lsp.go |
Makes LSP dependencies WASI-compatible. |
tsc/cmd/tsc/lsp_stdin.go |
Defines native LSP stdin. |
tsc/cmd/tsc/lsp_stdin_wasip1.go |
Adds WASI stdin handling. |
tsc/cmd/tsc/lsp_process.go |
Defines native process integrations. |
tsc/cmd/tsc/lsp_process_wasip1.go |
Disables unavailable WASI process features. |
tsc/cmd/tsc/currentdirectory.go |
Defines native cwd lookup. |
tsc/cmd/tsc/currentdirectory_wasip1.go |
Derives WASI cwd from PWD. |
tsc/cmd/tsc/api.go |
Uses portable cwd and signal helpers. |
tools/pipelines/typescript-publish.yml |
Publishes the WASI npm artifact. |
packages/typescript/test/version.test.ts |
Tests CLI WASI fallback. |
packages/typescript/test/sync/wasm.test.ts |
Tests CLI and API WebAssembly execution. |
packages/typescript/test/sync/transport.test.ts |
Tests synchronous injected transports. |
packages/typescript/test/encoder.test.ts |
Tests portable base64 encoding. |
packages/typescript/test/browser/wasm.test.ts |
Tests WebAssembly APIs in Chromium. |
packages/typescript/test/browser/syncAPI.ts |
Adapts sync API browser tests. |
packages/typescript/test/browser/suite.test.ts |
Runs API suites in Chromium. |
packages/typescript/test/browser/processShim.ts |
Provides browser process/timer shims. |
packages/typescript/test/browser/harness.ts |
Implements the browser test harness. |
packages/typescript/test/browser/bundle.test.ts |
Verifies browser-safe bundles. |
packages/typescript/test/browser/asyncAPI.ts |
Adapts async API browser tests. |
packages/typescript/test/browser/apiWrapper.ts |
Manages browser reactors and filesystem mirroring. |
packages/typescript/test/async/transport.test.ts |
Tests asynchronous transport behavior. |
packages/typescript/src/api/sync/transportClient.ts |
Implements sync protocol-over-transport. |
packages/typescript/src/api/sync/transport.ts |
Defines the sync transport contract. |
packages/typescript/src/api/sync/client.ts |
Supports injected sync transports. |
packages/typescript/src/api/sync/browserClient.ts |
Adds the browser sync client. |
packages/typescript/src/api/sync/api.ts |
Routes generated sync API through browser-aware clients. |
packages/typescript/src/api/options.ts |
Adds transport-based API options. |
packages/typescript/src/api/node/wtf8.ts |
Removes the Node buffer dependency. |
packages/typescript/src/api/node/encoder.ts |
Adds portable base64 encoding. |
packages/typescript/src/api/async/transportClient.ts |
Implements async protocol batching and lifecycle. |
packages/typescript/src/api/async/transport.ts |
Defines the async transport contract. |
packages/typescript/src/api/async/client.ts |
Supports injected async transports. |
packages/typescript/src/api/async/browserClient.ts |
Adds the browser async client. |
packages/typescript/src/api/async/api.ts |
Selects browser-aware clients and adjusts closing. |
packages/typescript/package.json |
Exports transports and browser clients. |
packages/typescript/lib/tsc.js |
Falls back to WASI when native binaries are unavailable. |
packages/typescript/lib/getExePath.js |
Resolves the WASI package artifact. |
packages/typescript/lib/getExePath.d.ts |
Declares WASI path resolution. |
packages/typescript-wasip1-wasm/tsconfig.json |
Configures the WASI helper package. |
packages/typescript-wasip1-wasm/src/wasmURL.ts |
Exposes the packaged module URL. |
packages/typescript-wasip1-wasm/src/wasmURL.source.ts |
Exposes the source-build module URL. |
packages/typescript-wasip1-wasm/src/wasi.ts |
Implements the browser WASI host. |
packages/typescript-wasip1-wasm/src/index.ts |
Implements and exports WasmTransport. |
packages/typescript-wasip1-wasm/README.md |
Documents CLI and API usage. |
packages/typescript-wasip1-wasm/package.json |
Defines the WASI npm package. |
package.json |
Adds Binaryen tooling. |
package-lock.json |
Locks new packages and workspace metadata. |
Herebyfile.mjs |
Builds, patches, tests, and packages WebAssembly. |
.github/workflows/ci.yml |
Runs Chromium API tests in CI. |
Review details
- Files reviewed: 56/58 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
cdbe79f to
06fe820
Compare
|
tried this out in a browser worker (Chrome), running what works:
two things I hit on the way, in case they're useful for the README:
what doesn't work: content mappers. which is the one thing we need for templates 😅 here's a patch that closes that gap with only standard WASI imports -- same idea as
--- a/tsc/cmd/tsc/lsp_process_wasip1.go
+++ b/tsc/cmd/tsc/lsp_process_wasip1.go
@@ -5,6 +5,11 @@ package main
import (
"errors"
+ "fmt"
"io"
+ "runtime"
+ "syscall"
+
+ "github.com/microsoft/TypeScript/tsc/internal/json"
)
func getNpmInstall() func(cwd string, args []string) ([]byte, error) {
@@ -13,6 +18,71 @@ func getNpmInstall() func(cwd string, args []string) ([]byte, error) {
}
}
+// hostProcessPath is a device the WASI host may provide so that content mappers can run.
+// A wasm module cannot start processes, so the host runs the mapper itself: the module opens
+// this path, writes one JSON line naming the command and directory, and then speaks the
+// mapper's stdio protocol over the descriptor. Hosts that do not provide the path get the
+// usual "content mappers are unsupported" behavior, because opening it fails.
+const hostProcessPath = "/.typescript/host-process"
+
func getLSPSpawn() func(command []string, dir string, stderr io.Writer) (io.ReadWriteCloser, error) {
- return nil
+ return spawnHostProcess
+}
+
+type hostProcessHeader struct {
+ Command []string `json:"command"`
+ Dir string `json:"dir"`
+}
+
+func spawnHostProcess(command []string, dir string, stderr io.Writer) (io.ReadWriteCloser, error) {
+ fd, err := syscall.Open(hostProcessPath, syscall.O_RDWR, 0)
+ if err != nil {
+ return nil, fmt.Errorf("the host does not provide %s: %w", hostProcessPath, err)
+ }
+ header, err := json.Marshal(hostProcessHeader{Command: command, Dir: dir})
+ if err != nil {
+ _ = syscall.Close(fd)
+ return nil, err
+ }
+ p := &hostProcess{fd: fd}
+ if _, err := p.Write(append(header, '\n')); err != nil {
+ _ = p.Close()
+ return nil, err
+ }
+ return p, nil
+}
+
+// hostProcess adapts the device descriptor to an io.ReadWriteCloser. Reads poll, like the LSP's
+// stdin does, because a blocking host call would park every goroutine in the module.
+type hostProcess struct {
+ fd int
+}
+
+func (p *hostProcess) Read(b []byte) (int, error) {
+ for {
+ n, err := syscall.Read(p.fd, b)
+ if n == 0 && err == nil {
+ return 0, io.EOF
+ }
+ if err != syscall.EAGAIN {
+ return n, err
+ }
+ runtime.Gosched()
+ }
+}
+
+func (p *hostProcess) Write(b []byte) (int, error) {
+ written := 0
+ for written < len(b) {
+ n, err := syscall.Write(p.fd, b[written:])
+ if err != nil {
+ return written, err
+ }
+ written += n
+ }
+ return written, nil
+}
+
+func (p *hostProcess) Close() error {
+ return syscall.Close(p.fd)
}on the host side that's a device whose with that, the commit: NullVoxPopuli-ai-agent@304e189 the consumer this is for is the REPL at https://limber.glimdown.com -- prototype here: NullVoxPopuli/limber#2255. that one currently runs a happy to open a PR against your branch if the shape works for you. (posted from my agent account; the testing and the patch were done with AI assistance) |
|
apologies if this is way off base. I literally don't know golang :( |
|
Thanks for the feedback; it's alright to not know Go, just trying it in a real project (agent or not) is good! |
|
When I started this work, content mappers didn't exist, so I'm not surprised that it's not factored in yet. I don't know what "execing" looks like, nor how an existing tsconfig would have any hope of working out of the box without node. |
|
one thing that's suprised me so far is how the |
Add an injectable synchronous transport and a browser-conditioned client for the TypeScript API. Provide a Go WebAssembly reactor backed by an in-memory filesystem and exercise it through the compiler and checker APIs. Publish the reactor and its transport separately as @typescript/api-wasm, with release-versioned builds and ordered npm publishing. Keep the main TypeScript package free of the WebAssembly binary and verify its browser module graph with an esbuild regression test.
Reuse the existing async and sync API suites in Playwright with narrow browser shims and explicit host-only exclusions. Add WASI scheduler polling and callback-backed output writes so WasmTransport preserves native filesystem behavior.
Do not clear filesystem callbacks configured directly on a transport when the API is constructed without its own filesystem. Assert that the browser harness accounts for all 669 API tests and cover WASM emit callbacks through the public transport setup.
Keep an active transport's filesystem callback intact when a second transport fails to create a session on the same reactor instance.
Close a newly created reactor session when filesystem callback attachment fails so the instance remains reusable.
06fe820 to
c67875a
Compare
Build the compiler, language server, and in-process API from one WASI module. Add the Node fallback, platform-specific runtime behavior, and release packaging for the unified artifact.
Return io.EOF when WASI stdin closes instead of triggering repeated empty reads. Cover the command lifecycle in the unified module test.
c67875a to
3788e0b
Compare
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Critical callback contract and reentrancy failures, plus unresolved lifecycle, compatibility, path translation, polling, and allocation issues, block approval.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 3
Open (8)
Synchronous callbacks cannot re-enter the transport · New Async transport rejects supported Promise callbacks · New On WASI stdin configured as nonblocking,EAGAINis the normal idle state.runtime.Gosched()… Transport options reject explicit undefined values · New WASI input options reject explicit undefined values · New Windows --option absolute paths are not translated · New Transport input options reject explicit undefined values · New This still hangs when the regular spawned-process client is closed while initialization is held in…
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The web LSP depends on an unavailable nonblocking-stdin implementation and can stall with the pinned WASI dependency.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
Open (2)
Resolved since last review (2)
# Conflicts: # package-lock.json # packages/typescript/src/api/options.ts # packages/vscode-typescript/package.json
Upstream API tests now use the default node:test export and exercise filesystem behavior specific to spawned process transports. Keep the browser suite building while continuing to account explicitly for behavior the injected WASM transport cannot model.
Keep the JavaScript API host versioned with the main TypeScript package while leaving the WASI package focused on compiler artifacts. This gives browser consumers an explicit host entry and a discoverable list of bundled library files without coupling the artifact package back to the API package.
Browser consumers need guidance on avoiding synchronous instantiation limits and selecting compatible API and artifact packages. Document the preferred browser path and require both packages to use the same exact version.
# Conflicts: # tsc/cmd/tsc/lsp.go
# Conflicts: # packages/vscode-typescript/package.json # tsc/cmd/tsc/lsp.go





Fixes #63858
Fixes #63862
For #63813
This is still a bit experimental, but this creates a wasip1
tsc.wasmbuild. This build works as a CLI, so one can run it anywhere and get the CLI, LSP server, etc. But, one can also instantiate the module and give it to the new TypeScript IPC-based API (including in the browser!) and get sync/async API access. This works similarly (though not the same) to David Sherret's ts-ast-viewer tsgo prototype. The dual-nature of this is enabled by some Wasm export +binaryentrickery to make one blob that can do both things.In a perfect world, we'd be able to do wasip3 or something, component model, yadda yadda, but the main Go compiler cannot do that yet.
I think for now, this works alright. I think I'd also want to slap a "no warranty" label on this for now until we can get Wasm a bit more settled.
Some interesting tidbits:
node:test, I had copilot write some shims for those APIs to make it work. Smarter would be to use vitest in browser mode, I suspect. But this works and runs all but 20 tests succesfully.TODO: