Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
- [Detecting traps](./advanced/detecting-traps.md)
- [Preview 3 streams and backpressure](./advanced/preview3-streams-and-backpressure.md)
- [Preview 3 host value disposal](./advanced/preview3-host-value-disposal.md)
- [Preview 2 shim helpers]()
- [CLI helpers](./advanced/preview2-shim-helpers/cli.md)
- [JavaScript ecosystem interoperability]()
- [`jco-std`](./interop/jco-std.md)
- [Node.js built-in compatibility](./interop/nodejs-builtins.md)
Expand Down
72 changes: 72 additions & 0 deletions docs/src/advanced/preview2-shim-helpers/cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Preview 2 shim CLI helpers

[`@bytecodealliance/preview2-shim`][npm-p2-shim] ships host-side helper functions next to its WASI
implementations. Helpers build only on the shim's public surface and are exported from `helpers/`
subpaths, separate from the underscore-prefixed functions (`_setStdin`, `_setPreopens`, ...) that
exist to configure the shim itself.

Helpers that only exist for one build are exported under that build's prefix. The CLI helper on
this page is browser-only, because it builds on the browser `wasi:io` pollables:

```js
import { bufferedInputStream } from "@bytecodealliance/preview2-shim/browser/helpers/cli";
```

[npm-p2-shim]: https://www.npmjs.com/package/@bytecodealliance/preview2-shim

## `bufferedInputStream(input)`

Builds an `InputStreamHandler` that serves a fixed buffer to the guest and then reports the stream
as closed. It replaces the bounded handler that hosts otherwise have to hand-roll whenever a
guest's stdin is known up front, such as piping a document into a CLI component.

`input` is a `Uint8Array`, or a string that is encoded as UTF-8. Each `blockingRead(len)` returns
up to `len` bytes from what remains of the buffer. Once the buffer is exhausted, further reads raise
the WASI `closed` stream error, which the guest observes as end of input. `subscribe()` returns an
always-ready pollable, so a guest that polls stdin before reading never blocks.

### Feeding an isolated CLI

`createCli` builds CLI interfaces that do not touch the shim's global streams:

```js
import { cli } from "@bytecodealliance/preview2-shim";
import { bufferedInputStream } from "@bytecodealliance/preview2-shim/browser/helpers/cli";

const isolated = cli.createCli({
stdin: bufferedInputStream("first line\nsecond line\n"),
});
```

### Feeding a `WASIShim`

`WASIShim` accepts the same handler through its `stdin` option and builds an isolated CLI for the
instance:

```js
import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation";
import { bufferedInputStream } from "@bytecodealliance/preview2-shim/browser/helpers/cli";

const shim = new WASIShim({
stdin: bufferedInputStream("input for the guest\n"),
});
const component = await instantiate(undefined, shim.getImportObject());
```

See [Manual Wasm instantiation with WASI overrides](../manual-wasm-instantiation-with-wasi-overrides.md)
for how `instantiate` and `WASIShim` fit together.

### Replacing the global stdin

The compatibility setter installs the handler on the stream shared by every component that does
not configure its own CLI:

```js
import { cli } from "@bytecodealliance/preview2-shim";
import { bufferedInputStream } from "@bytecodealliance/preview2-shim/browser/helpers/cli";

cli._setStdin(bufferedInputStream(new Uint8Array([1, 2, 3])));
```

Prefer `createCli` or `WASIShim` when more than one component runs in the same page, since the
global stream is consumed once and then stays closed.
7 changes: 7 additions & 0 deletions packages/preview2-shim/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ http._setRequestStreaming(true);
The setting affects subsequent requests made through the browser HTTP shim. Call
`http._setRequestStreaming(false)` to restore portable completion buffering.

Host-side helper functions that build on the public shim surface are exported from `helpers/`
subpaths, separate from the underscore-prefixed configuration setters. The browser-only
`bufferedInputStream` helper (`@bytecodealliance/preview2-shim/browser/helpers/cli`) feeds a guest a
fixed stdin buffer. See [Preview 2 shim CLI helpers][book-cli-helpers] in the Jco book.

[book-cli-helpers]: https://bytecodealliance.github.io/jco/advanced/preview2-shim-helpers/cli.html

Browser applications select storage explicitly. The bundled file-data adapter is ephemeral and must
be opted into:

Expand Down
16 changes: 12 additions & 4 deletions packages/preview2-shim/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,24 @@
"type": "module",
"exports": {
".": {
"types": "./dist/browser/index.d.ts",
"node": {
"types": "./dist/nodejs/index.d.ts",
"default": "./dist/nodejs/index.js"
},
"default": "./dist/browser/index.js"
"default": {
"types": "./dist/browser/index.d.ts",
"default": "./dist/browser/index.js"
}
},
"./*": {
"types": "./dist/browser/*.d.ts",
"node": {
"types": "./dist/nodejs/*.d.ts",
"default": "./dist/nodejs/*.js"
},
"default": "./dist/browser/*.js"
"default": {
"types": "./dist/browser/*.d.ts",
"default": "./dist/browser/*.js"
}
},
"./instantiation": {
"types": "./dist/common/instantiation.d.ts",
Expand All @@ -55,6 +59,10 @@
"./interfaces/*": {
"types": "./types/interfaces/*.d.ts"
},
"./browser/helpers/cli": {
"types": "./dist/browser/helpers/cli.d.ts",
"default": "./dist/browser/helpers/cli.js"
},
"./io-worker": {
"types": "./dist/nodejs/io-worker.d.ts",
"node": "./dist/nodejs/io-worker.js"
Expand Down
27 changes: 27 additions & 0 deletions packages/preview2-shim/src/browser/helpers/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { pollableCreate, type InputStreamHandler } from "../io.js";

/**
* Build an `InputStreamHandler` that serves a fixed buffer, then reports `closed`.
*
* Pass the result to `createCli({ stdin })` or `_setStdin` to feed a guest a fixed stdin.
*/
export function bufferedInputStream(input: Uint8Array | string): InputStreamHandler {
const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
let offset = 0;

return {
blockingRead(len: bigint): Uint8Array {
if (offset >= bytes.length) {
throw { tag: "closed" };
}
const remaining = bytes.length - offset;
const toRead = len < BigInt(remaining) ? Number(len) : remaining;
const chunk = bytes.slice(offset, offset + toRead);
offset += toRead;
return chunk;
},
subscribe() {
return pollableCreate();
},
};
}
42 changes: 42 additions & 0 deletions packages/preview2-shim/test/browser/helpers/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { suite, test, assert } from "vitest";

suite("Browser CLI helpers", () => {
test("bufferedInputStream serves bytes across reads then reports closed", async () => {
const { bufferedInputStream } = await import("../../../src/browser/helpers/cli.js");
const handler = bufferedInputStream(new Uint8Array([1, 2, 3, 4, 5]));

assert.deepStrictEqual(handler.blockingRead(3n), new Uint8Array([1, 2, 3]));
assert.deepStrictEqual(handler.blockingRead(10n), new Uint8Array([4, 5]));
let caught: unknown;
try {
handler.blockingRead(1n);
} catch (err) {
caught = err;
}
assert.deepStrictEqual(caught, { tag: "closed" });
});

test("bufferedInputStream accepts a string and encodes it as UTF-8", async () => {
const { bufferedInputStream } = await import("../../../src/browser/helpers/cli.js");
const handler = bufferedInputStream("hi");

assert.deepStrictEqual(handler.blockingRead(10n), new TextEncoder().encode("hi"));
assert.throws(() => handler.blockingRead(1n));
});

test("bufferedInputStream reports closed immediately for empty input", async () => {
const { bufferedInputStream } = await import("../../../src/browser/helpers/cli.js");
const handler = bufferedInputStream(new Uint8Array(0));

assert.throws(() => handler.blockingRead(1n));
});

test("bufferedInputStream wires into createCli's stdin", async () => {
const { createCli } = await import("../../../src/browser/cli.js");
const { bufferedInputStream } = await import("../../../src/browser/helpers/cli.js");
const cli = createCli({ stdin: bufferedInputStream("hello") });
const stdin = cli.stdin.getStdin();

assert.deepStrictEqual(stdin.blockingRead(5n), new TextEncoder().encode("hello"));
});
});
Loading