Repository: bytecodealliance/jco
Pull request #599: feat(p2-shim): add easy to use p2 shim object
Author: login: vados-cosmonic
Created: 2025-03-13T17:37:49Z | Merged: 2025-03-20T16:12:05Z

This commit introduces an import for `preview2-shim` that can be used to more easily fulfill requirements for `instantiate` calls that are exported from transpiled modules.

After creating a `WasiP2Shim` object, users should be able to call `instantiate(undefined, shim.importObject())` and make use of all WASI interfaces specified (or overriden) on the shim.


Diff:
diff --git a/packages/preview2-shim/lib/common/instantiation.js b/packages/preview2-shim/lib/common/instantiation.js
new file mode 100644
index 00000000..1f778138
--- /dev/null
+++ b/packages/preview2-shim/lib/common/instantiation.js
@@ -0,0 +1,121 @@
+import * as wasi from "@bytecodealliance/preview2-shim";
+
+/**
+ * A class that holds WASI shims and can be used to configure
+ * an instantiation of a WebAssembly component transpiled with jco
+ * (i.e. via `jco transpile`).
+ *
+ * Normally, transpiled components contain mapping for WASI interfaces
+ * and/or imports that import the relevant packages (ex. `@bytecodealliance/preview2-shim/clocks`)
+ * from the right sources.
+ *
+ * This function makes use of the `WASIShim` object to provide an object that can be easily
+ * fed to the `instantiate` function produced by a transpiled component:
+ *
+ * ```js
+ * import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation"
+ * // ...
+ * import { instantiate } from "path/to/transpiled/component.js"
+ * // ...
+ * const component = await instantiate(null, new WASIShim().getImportObject())
+ * ```
+ *
+ * You can also replace imports that you'd like to override with custom implementations,
+ * by using the `WASIShim` object directly:
+ *
+ * ```js
+ * import { random } from "@bytecodealliance/preview2-shim"
+ * import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation"
+ * // ...
+ * import { instantiate } from "path/to/transpiled/component.js"
+ * // ...
+ * const customWASIShim = new WASIShim({
+ *     random: {
+ *         // For these two interfaces we re-use the default provided shim
+ *         random: random.random,
+ *         insecure-seed: random.insecureSeed,
+ *         // For insecure, we can supply our own custom implementation
+ *         insecure: {
+ *             ...
+ *         }
+ *     }
+ * });
+ *
+ * const component = await instantiate(null, customWASIShim.getImportObject())
+ * ```
+ *
+ * Note that this object is similar but not identical to the Node `WASI` object --
+ * it is solely concerned with shimming of preview2 when dealing with a WebAssembly
+ * component transpiled by Jco. While this object *does* work with Node (and the browser)
+ * semantics are not the same as Node's `WASI` object.
+ *
+ * @class WASIShim
+ */
+export class WASIShim {
+  /** Object that confirms to the shim interface for `wasi:cli` */
+  #cli;
+  /** Object that confirms to the shim interface for `wasi:filesystem` */
+  #filesystem;
+  /** Object that confirms to the shim interface for `wasi:io` */
+  #io;
+  /** Object that confirms to the shim interface for `wasi:random` */
+  #random;
+  /** Object that confirms to the shim interface for `wasi:clocks` */
+  #clocks;
+  /** Object that confirms to the shim interface for `wasi:sockets` */
+  #sockets;
+
+  constructor(shims) {
+    this.#cli = shims?.cli ?? wasi.cli;
+    this.#filesystem = shims?.filesystem ?? wasi.filesystem;
+    this.#io = shims?.io ?? wasi.io;
+    this.#random = shims?.random ?? wasi.random;
+    this.#clocks = shims?.clocks ?? wasi.clocks;
+    this.#sockets = shims?.sockets ?? wasi.sockets;
+  }
+
+  /**
+   * Generate an import object for the shim that can be used with
+   * functions like `instantiate` that are exposed from a transpiled
+   * WebAssembly component.
+   *
+   * @returns {object}
+   */
+  getImportObject() {
+    return {
+      "wasi:cli/environment": this.#cli.environment,
+      "wasi:cli/exit": this.#cli.exit,
+      "wasi:cli/stderr": this.#cli.stderr,
+      "wasi:cli/stdin": this.#cli.stdin,
+      "wasi:cli/stdout": this.#cli.stdout,
+      "wasi:cli/terminal-input": this.#cli.terminalInput,
+      "wasi:cli/terminal-output": this.#cli.terminalOutput,
+      "wasi:cli/terminal-stderr": this.#cli.terminalStderr,
+      "wasi:cli/terminal-stdin": this.#cli.terminalStdin,
+      "wasi:cli/terminal-stdout": this.#cli.terminalStdout,
+
+      "wasi:sockets/instance-network": this.#sockets.instanceNetwork,
+      "wasi:sockets/ip-name-lookup": this.#sockets.ipNameLookup,
+      "wasi:sockets/network": this.#sockets.network,
+      "wasi:sockets/tcp": this.#sockets.tcp,
+      "wasi:sockets/tcp-create-socket": this.#sockets.tcpCreateSocket,
+      "wasi:sockets/udp": this.#sockets.udp,
+      "wasi:sockets/udp-create-socket": this.#sockets.udpCreateSocket,
+
+      "wasi:filesystem/preopens": this.#filesystem.preopens,
+      "wasi:filesystem/types": this.#filesystem.types,
+
+      "wasi:io/error": this.#io.error,
+      "wasi:io/poll": this.#io.poll,
+      "wasi:io/streams": this.#io.streams,
+
+      "wasi:random/random": this.#random.random,
+      "wasi:random/insecure": this.#random.insecure,
+      "wasi:random/insecure-seed": this.#random.insecureSeed,
+
+      "wasi:clocks/monotonic-clock": this.#clocks.monotonicClock,
+      "wasi:clocks/timezone": this.#clocks.timezone,
+      "wasi:clocks/wall-clock": this.#clocks.wallClock,
+    };
+  }
+}
diff --git a/packages/preview2-shim/package.json b/packages/preview2-shim/package.json
index ad0f53db..7635b0e4 100644
--- a/packages/preview2-shim/package.json
+++ b/packages/preview2-shim/package.json
@@ -15,6 +15,11 @@
       "types": "./types/*.d.ts",
       "node": "./lib/nodejs/*.js",
       "default": "./lib/browser/*.js"
+    },
+    "./instantiation": {
+      "types": "./types/instantiation.d.ts",
+      "node": "./lib/common/instantiation.js",
+      "default": "./lib/common/instantiation.js"
     }
   },
   "scripts": {
diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js
index 2b07964c..f49f15f8 100644
--- a/packages/preview2-shim/test/test.js
+++ b/packages/preview2-shim/test/test.js
@@ -10,12 +10,12 @@ import { fileURLToPath } from "node:url";
 
 const symbolDispose = Symbol.dispose || Symbol.for("dispose");
 
-function testWithGCWrap (asyncTestFn) {
+function testWithGCWrap(asyncTestFn) {
   return async () => {
     await asyncTestFn();
     // Force the JS GC to run finalizers
     gc();
-    await new Promise(resolve => setTimeout(resolve, 200));
+    await new Promise((resolve) => setTimeout(resolve, 200));
   };
 }
 
@@ -117,7 +117,7 @@ suite("Node.js Preview2", () => {
         {},
         fileURLToPath(import.meta.url).slice(1),
         {},
-        {}
+        {},
       );
       const stream = childDescriptor.readViaStream(0);
       const poll = stream.subscribe();
@@ -129,72 +129,74 @@ suite("Node.js Preview2", () => {
       toDispose.push(stream);
       toDispose.push(childDescriptor);
     })();
-    
 
     // Force the Poll to GC so the next dispose doesn't trap
     gc();
-    await new Promise(resolve => setTimeout(resolve, 200));
+    await new Promise((resolve) => setTimeout(resolve, 200));
 
     for (const item of toDispose) {
       item[symbolDispose]();
     }
   });
 
-  test("WASI HTTP", testWithGCWrap(async () => {
-    const { http } = await import("@bytecodealliance/preview2-shim");
-    const { handle } = http.outgoingHandler;
-    const { OutgoingRequest, OutgoingBody, Fields } = http.types;
-    const encoder = new TextEncoder();
-    const request = new OutgoingRequest(
-      new Fields([
-        ["User-agent", encoder.encode("WASI-HTTP/0.0.1")],
-        ["Content-type", encoder.encode("application/json")],
-      ])
-    );
-    request.setPathWithQuery("/");
-    request.setAuthority("webassembly.org");
-    request.setScheme({ tag: "HTTPS" });
+  test(
+    "WASI HTTP",
+    testWithGCWrap(async () => {
+      const { http } = await import("@bytecodealliance/preview2-shim");
+      const { handle } = http.outgoingHandler;
+      const { OutgoingRequest, OutgoingBody, Fields } = http.types;
+      const encoder = new TextEncoder();
+      const request = new OutgoingRequest(
+        new Fields([
+          ["User-agent", encoder.encode("WASI-HTTP/0.0.1")],
+          ["Content-type", encoder.encode("application/json")],
+        ]),
+      );
+      request.setPathWithQuery("/");
+      request.setAuthority("webassembly.org");
+      request.setScheme({ tag: "HTTPS" });
 
-    const outgoingBody = request.body();
-    OutgoingBody.finish(outgoingBody);
+      const outgoingBody = request.body();
+      OutgoingBody.finish(outgoingBody);
 
-    const futureIncomingResponse = handle(request);
-    futureIncomingResponse.subscribe().block();
-    const incomingResponseResult = futureIncomingResponse.get().val;
+      const futureIncomingResponse = handle(request);
+      futureIncomingResponse.subscribe().block();
+      const incomingResponseResult = futureIncomingResponse.get().val;
 
-    if (incomingResponseResult.tag !== 'ok') {
-      throw incomingResponseResult.val;
-    }
+      if (incomingResponseResult.tag !== "ok") {
+        throw incomingResponseResult.val;
+      }
 
-    const status = incomingResponseResult.val.status();
-    const responseHeaders = incomingResponseResult.val.headers().entries();
+      const status = incomingResponseResult.val.status();
+      const responseHeaders = incomingResponseResult.val.headers().entries();
 
-    const decoder = new TextDecoder();
-    const headers = Object.fromEntries(
-      responseHeaders.map(([k, v]) => [k, decoder.decode(v)])
-    );
+      const decoder = new TextDecoder();
+      const headers = Object.fromEntries(
+        responseHeaders.map(([k, v]) => [k, decoder.decode(v)]),
+      );
 
-    let responseBody;
-    const incomingBody = incomingResponseResult.val.consume();
-    {
-      const bodyStream = incomingBody.stream();
-      bodyStream.subscribe().block();
-      let buf = bodyStream.read(5000n);
-      while (buf.byteLength === 0) {
-        try {
-          buf = bodyStream.read(5000n);
-        } catch (e) {
-          if (e.tag === "closed") break;
-          throw e.val || e;
+      let responseBody;
+      const incomingBody = incomingResponseResult.val.consume();
+      {
+        const bodyStream = incomingBody.stream();
+        bodyStream.subscribe().block();
+        let buf = bodyStream.read(5000n);
+        while (buf.byteLength === 0) {
+          try {
+            buf = bodyStream.read(5000n);
+          } catch (e) {
+            if (e.tag === "closed") break;
+            throw e.val || e;
+          }
         }
+        responseBody = new TextDecoder().decode(buf);
       }
-      responseBody = new TextDecoder().decode(buf);
-    }
 
-    strictEqual(status, 200);
-    ok(headers["content-type"].startsWith("text/html"));
-    ok(responseBody.includes("WebAssembly"));
-  }));
+      strictEqual(status, 200);
+      ok(headers["content-type"].startsWith("text/html"));
+      ok(responseBody.includes("WebAssembly"));
+    }),
+  );
 
   suite("WASI Sockets (TCP)", async () => {
     test("sockets.instanceNetwork() should be a singleton", async () => {
@@ -213,7 +215,7 @@ suite("Node.js Preview2", () => {
         () => {
           sockets.tcpCreateSocket.createTcpSocket("abc");
         },
-        (err) => err === "not-supported"
+        (err) => err === "not-supported",
       );
     });
     test("tcp.bind(): should bind to a valid ipv4 address", async () => {
@@ -288,7 +290,7 @@ suite("Node.js Preview2", () => {
         () => {
           tcpSocket.startBind(network, localAddress);
         },
-        (err) => err === "invalid-argument"
+        (err) => err === "invalid-argument",
       );
     });
 
@@ -311,7 +313,7 @@ suite("Node.js Preview2", () => {
           // already bound
           tcpSocket.startBind(network, localAddress);
         },
-        (err) => err === "invalid-state"
+        (err) => err === "invalid-state",
       );
     });
 
@@ -335,59 +337,64 @@ suite("Node.js Preview2", () => {
       // const [socket, input, output] = tcpSocket.accept();
     });
 
-    test("tcp.connect(): should connect to a valid ipv4 address and port=0", testWithGCWrap(async () => {
-      const { lookup } = await import("node:dns");
-      const { sockets } = await import("@bytecodealliance/preview2-shim");
-      const network = sockets.instanceNetwork.instanceNetwork();
-      const tcpSocket = sockets.tcpCreateSocket.createTcpSocket("ipv4");
-
-      const pollable = tcpSocket.subscribe();
-
-      const googleIp = await new Promise((resolve, reject) =>
-        lookup("google.com", (err, result) => err ? reject(err) : resolve(result))
-      );
-
-      tcpSocket.startConnect(network, {
-        tag: "ipv4",
-        val: {
-          address: googleIp.split("."),
-          port: 80,
-        },
-      });
-
-      ok(!pollable.ready());
-      pollable.block();
-      ok(pollable.ready());
-
-      const [input, output] = tcpSocket.finishConnect();
-
-      strictEqual(tcpSocket.addressFamily(), "ipv4");
-
-      ok(pollable.ready());
-
-      output.blockingWriteAndFlush(
-        new TextEncoder().encode("GET http://www.google.com/ HTTP/1.1\n\n")
-      );
-
-      {
-        input.subscribe().block();
-        let buf = input.read(5000n);
-        while (buf.byteLength === 0) {
-          try {
-            buf = input.read(5000n);
-          } catch (e) {
-            if (e.tag === "closed") break;
-            throw e.val || e;
+    test(
+      "tcp.connect(): should connect to a valid ipv4 address and port=0",
+      testWithGCWrap(async () => {
+        const { lookup } = await import("node:dns");
+        const { sockets } = await import("@bytecodealliance/preview2-shim");
+        const network = sockets.instanceNetwork.instanceNetwork();
+        const tcpSocket = sockets.tcpCreateSocket.createTcpSocket("ipv4");
+
+        const pollable = tcpSocket.subscribe();
+
+        const googleIp = await new Promise((resolve, reject) =>
+          lookup("google.com", (err, result) =>
+            err ? reject(err) : resolve(result),
+          ),
+        );
+
+        tcpSocket.startConnect(network, {
+          tag: "ipv4",
+          val: {
+            address: googleIp.split("."),
+            port: 80,
+          },
+        });
+
+        ok(!pollable.ready());
+        pollable.block();
+        ok(pollable.ready());
+
+        const [input, output] = tcpSocket.finishConnect();
+
+        strictEqual(tcpSocket.addressFamily(), "ipv4");
+
+        ok(pollable.ready());
+
+        output.blockingWriteAndFlush(
+          new TextEncoder().encode("GET http://www.google.com/ HTTP/1.1\n\n"),
+        );
+
+        {
+          input.subscribe().block();
+          let buf = input.read(5000n);
+          while (buf.byteLength === 0) {
+            try {
+              buf = input.read(5000n);
+            } catch (e) {
+              if (e.tag === "closed") break;
+              throw e.val || e;
+            }
           }
+          const responseBody = new TextDecoder().decode(buf);
+          ok(responseBody.includes("<title>Google"));
+          ok(responseBody.includes("<!doctype"));
+          ok(responseBody.includes("<script"));
         }
-        const responseBody = new TextDecoder().decode(buf);
-        ok(responseBody.includes("<title>Google"));
-        ok(responseBody.includes("<!doctype"));
-        ok(responseBody.includes("<script"));
-      }
 
-      tcpSocket.shutdown("both");
-    }));
+        tcpSocket.shutdown("both");
+      }),
+    );
   });
 
   suite("WASI Sockets (UDP)", async () => {
@@ -417,7 +424,7 @@ suite("Node.js Preview2", () => {
       socket.finishBind();
 
       const boundAddress = socket.localAddress();
-      strictEqual(boundAddress.tag, 'ipv4');
+      strictEqual(boundAddress.tag, "ipv4");
       deepStrictEqual(boundAddress.val.address, [0, 0, 0, 0]);
       strictEqual(boundAddress.val.port > 0, true);
       strictEqual(socket.addressFamily(), "ipv4");
@@ -426,9 +433,9 @@ suite("Node.js Preview2", () => {
     test("udp.bind(): should bind to a valid ipv6 address and port=0", async () => {
       const { sockets } = await import("@bytecodealliance/preview2-shim");
       const network = sockets.instanceNetwork.instanceNetwork();
-      const socket = sockets.udpCreateSocket.createUdpSocket('ipv6');
+      const socket = sockets.udpCreateSocket.createUdpSocket("ipv6");
       const localAddress = {
-        tag: 'ipv6',
+        tag: "ipv6",
         val: {
           address: [0, 0, 0, 0, 0, 0, 0, 0],
           port: 0,
@@ -439,7 +446,7 @@ suite("Node.js Preview2", () => {
       socket.finishBind();
 
       const boundAddress = socket.localAddress();
-      strictEqual(boundAddress.tag, 'ipv6');
+      strictEqual(boundAddress.tag, "ipv6");
       deepStrictEqual(boundAddress.val.address, [0, 0, 0, 0, 0, 0, 0, 0]);
       strictEqual(boundAddress.val.port > 0, true);
       strictEqual(socket.addressFamily(), "ipv6");
@@ -472,32 +479,84 @@ suite("Node.js Preview2", () => {
 
       const boundAddress = socket.localAddress();
 
-      strictEqual(boundAddress.tag, 'ipv4');
+      strictEqual(boundAddress.tag, "ipv4");
       notDeepStrictEqual(boundAddress.val.address, [0, 0, 0, 0]);
       strictEqual(boundAddress.val.port > 0, true);
     });
 
-    test("udp.stream(): should connect to a valid ipv6 address", testWithGCWrap(async () => {
-      const { sockets } = await import("@bytecodealliance/preview2-shim");
-      const network = sockets.instanceNetwork.instanceNetwork();
-      const socket = sockets.udpCreateSocket.createUdpSocket('ipv6');
-      const localAddress = {
-        tag: 'ipv6',
-        val: {
-          address: [0, 0, 0, 0, 0, 0, 0, 0],
-          port: 1337,
-        },
-      };
-
-      socket.startBind(network, localAddress);
-      socket.finishBind();
-      socket.stream();
+    test(
+      "udp.stream(): should connect to a valid ipv6 address",
+      testWithGCWrap(async () => {
+        const { sockets } = await import("@bytecodealliance/preview2-shim");
+        const network = sockets.instanceNetwork.instanceNetwork();
+        const socket = sockets.udpCreateSocket.createUdpSocket("ipv6");
+        const localAddress = {
+          tag: "ipv6",
+          val: {
+            address: [0, 0, 0, 0, 0, 0, 0, 0],
+            port: 1337,
+          },
+        };
+
+        socket.startBind(network, localAddress);
+        socket.finishBind();
+        socket.stream();
+
+        strictEqual(socket.addressFamily(), "ipv6");
+
+        const boundAddress = socket.localAddress();
+        deepStrictEqual(boundAddress.val.address, [0, 0, 0, 0, 0, 0, 0, 0]);
+        strictEqual(boundAddress.val.port, 1337);
+      }),
+    );
+  });
+});
 
-      strictEqual(socket.addressFamily(), "ipv6");
+suite("Instantiation", () => {
+  test("WASIShim export (random)", async () => {
+    const { random } = await import("@bytecodealliance/preview2-shim");
+    const { WASIShim } = await import(
+      "@bytecodealliance/preview2-shim/instantiation"
+    );
+    const shim = new WASIShim();
+    ok(shim);
+    deepStrictEqual(
+      Object.keys(shim.getImportObject()["wasi:random/random"]).sort(),
+      Object.keys(random.random).sort(),
+    );
+    deepStrictEqual(
+      Object.keys(shim.getImportObject()["wasi:random/insecure-seed"]).sort(),
+      Object.keys(random.insecureSeed).sort(),
+    );
+    deepStrictEqual(
+      Object.keys(shim.getImportObject()["wasi:random/insecure"]).sort(),
+      Object.keys(random.insecure).sort(),
+    );
+  });
 
-      const boundAddress = socket.localAddress();
-      deepStrictEqual(boundAddress.val.address, [0, 0, 0, 0, 0, 0, 0, 0]);
-      strictEqual(boundAddress.val.port, 1337);
-    }));
+  test("WASIShim export override", async () => {
+    const { random } = await import("@bytecodealliance/preview2-shim");
+    const { WASIShim } = await import(
+      "@bytecodealliance/preview2-shim/instantiation"
+    );
+    const invalidWASIShim = {
+      random: {
+        random: {
+          invalid: function setup() {},
+        },
+      },
+    };
+    const shim = new WASIShim(invalidWASIShim);
+    ok(shim);
+    notDeepStrictEqual(
+      Object.keys(shim.getImportObject()["wasi:random/random"]).sort(),
+      Object.keys(random.random).sort(),
+    );
+    strictEqual(shim.getImportObject()["wasi:random/insecure-seed"], undefined);
+    strictEqual(shim.getImportObject()["wasi:random/insecure"], undefined);
+    deepStrictEqual(
+      Object.keys(shim.getImportObject()["wasi:random/random"]).sort(),
+      Object.keys(invalidWASIShim.random.random).sort(),
+    );
   });
 });
diff --git a/packages/preview2-shim/types/instantiation.d.ts b/packages/preview2-shim/types/instantiation.d.ts
new file mode 100644
index 00000000..2108b9a2
--- /dev/null
+++ b/packages/preview2-shim/types/instantiation.d.ts
@@ -0,0 +1,41 @@
+type WASIImportObject = {
+  "wasi:cli/environment": typeof import("./interfaces/wasi-cli-environment.d.ts");
+  "wasi:cli/exit":  typeof import("./interfaces/wasi-cli-exit.d.ts");
+  "wasi:cli/stderr":  typeof import("./interfaces/wasi-cli-stderr.d.ts");
+  "wasi:cli/stdin":  typeof import("./interfaces/wasi-cli-stdin.d.ts");
+  "wasi:cli/stdout":  typeof import("./interfaces/wasi-cli-stdout.d.ts");
+  "wasi:cli/terminal-input":  typeof import("./interfaces/wasi-cli-terminal-input.d.ts");
+  "wasi:cli/terminal-output":  typeof import("./interfaces/wasi-cli-terminal-output.d.ts");
+  "wasi:cli/terminal-stderr":  typeof import("./interfaces/wasi-cli-terminal-stderr.d.ts");
+  "wasi:cli/terminal-stdin":  typeof import("./interfaces/wasi-cli-terminal-stdin.d.ts");
+  "wasi:cli/terminal-stdout":  typeof import("./interfaces/wasi-cli-terminal-stdout.d.ts");
+
+  "wasi:sockets/instance-network": typeof import("./interfaces/wasi-sockets-instance-network.d.ts");
+  "wasi:sockets/ip-name-lookup": typeof import("./interfaces/wasi-sockets-ip-name-lookup.d.ts");
+  "wasi:sockets/network": typeof import("./interfaces/wasi-sockets-network.d.ts");
+  "wasi:sockets/tcp": typeof import("./interfaces/wasi-sockets-tcp.d.ts");
+  "wasi:sockets/tcp-create-socket": typeof import("./interfaces/wasi-sockets-tcp-create-socket.d.ts");
+  "wasi:sockets/udp": typeof import("./interfaces/wasi-sockets-udp.d.ts");
+  "wasi:sockets/udp-create-socket": typeof import("./interfaces/wasi-sockets-udp-create-socket.d.ts");
+
+  "wasi:filesystem/preopens": typeof import("./interfaces/wasi-filesystem-preopens.d.ts");
+  "wasi:filesystem/types": typeof import("./interfaces/wasi-filesystem-types.d.ts");
+
+  "wasi:io/error": typeof import("./interfaces/wasi-io-error.d.ts");
+  "wasi:io/poll": typeof import("./interfaces/wasi-io-poll.d.ts");
+  "wasi:io/streams": typeof import("./interfaces/wasi-io-streams.d.ts");
+
+  "wasi:random/random": typeof import("./interfaces/wasi-random-random.d.ts");
+  "wasi:random/insecure": typeof import("./interfaces/wasi-random-insecure.d.ts");
+  "wasi:random/insecure-seed": typeof import("./interfaces/wasi-random-insecure-seed.d.ts");
+
+  "wasi:clocks/monotonic-clock": typeof import("./interfaces/wasi-clocks-monotonic-clock.d.ts");
+  "wasi:clocks/wall-clock": typeof import("./interfaces/wasi-clocks-wall-clock.d.ts");
+}
+
+export class WasiP2Shim {
+  constructor(shims?: Partial<WASIImportObject>);
+  importObject(): WASIImportObject;
+}
+
+export function imports(): WASIImportObject;


Reviews:
user: login: guybedford
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T17:45:28Z
user: login: vados-cosmonic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T18:34:26Z
user: login: vados-cosmonic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T18:35:00Z
user: login: guybedford
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T18:42:37Z
user: login: vados-cosmonic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T18:59:32Z
user: login: guybedford
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T19:48:16Z
user: login: vados-cosmonic
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-20T11:27:51Z
user: login: guybedford
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-20T16:11:23Z

Review comments:
diff_hunk: @@ -0,0 +1,49 @@
+import { WasiP2Shim } from "./wasi-shim.js";
+
+export { WasiP2Shim } from "./wasi-shim.js";
+
+/**
+* Generate a WASI imports object that can be used for
+* manual component instantiation of a transpiled component.
+*
+* Normally, transpiled components contain mapping for WASI interfaces
+* and/or imports that import the relevant packages (ex. `@bytecodealliance/preview2-shim/clocks`)
+* from the right sources.
+*
+* This function makes use of the `WasiShim` object to provide an object that can be easily
+* fed to the `instantiate` function produced by a transpiled component:
+*
+* ```js
+* import { imports } from "@bytecodealliance/preview2-shim.js"
+* // ...
+* import { instantiate } from "path/to/transpiled/component.js"
+* // ...
+* const component = instantiate(null, imports())
+* ```
+*
+* You can also replace imports that you'd like to override with custom implementations,
+* by using the `WasiShim` object directly:
+*
+* ```js
+* import { imports, WasiShim } from "@bytecodealliance/preview2-shim.js"
+* // ...
+* import { instantiate } from "path/to/transpiled/component.js"
+* // ...
+* const defaultImports = imports();
+* const customImports = new WasiShim({
+*     random: {
+*         random: defaultImports["wasi:random/random"],
+*         insecure: {
+*             // Your custom implementation for the `wasi:random/insecure` interface goes here
+*         }
+*         insecure-seed: defaultImports["wasi:random/insecure-seed"],
+*     }
+* });
+*
+* const component = instantiate(null, customImports.importObject())
+* ```
+*/
+export function imports()  {
+  const shim = new WasiP2Shim();
+  return shim.importObject();
+}
path: packages/preview2-shim/lib/common/instantiation.js
user: login: guybedford
body: Is this really worth the extra definition over documenting using `new WasiP2Shim().importObject()` directly?
created_at: 2025-03-19T17:36:40Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,49 @@
+import { WasiP2Shim } from "./wasi-shim.js";
+
+export { WasiP2Shim } from "./wasi-shim.js";
path: packages/preview2-shim/lib/common/instantiation.js
user: login: guybedford
body: Note this can just be directly written `export { WasiP2Shim } from './wasi-shim.js'`.

But if that's all this module does, it seems an unnecessary redirection?
created_at: 2025-03-19T17:37:40Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,49 @@
+import { WasiP2Shim } from "./wasi-shim.js";
+
+export { WasiP2Shim } from "./wasi-shim.js";
path: packages/preview2-shim/lib/common/instantiation.js
user: login: vados-cosmonic
body: > Note this can just be directly written export { WasiP2Shim } from './wasi-shim.js'.

So it *seems* like this would work but it actually doesn't -- it's bit me in the past. Something that is only `export`ed actually isn't available to the code in the current module.

That said, we can certainly move `WasiP2Shim` (and maybe just rename it `WasiShim`) to this file as well, I'm fine with that.
created_at: 2025-03-19T18:34:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,49 @@
+import { WasiP2Shim } from "./wasi-shim.js";
+
+export { WasiP2Shim } from "./wasi-shim.js";
+
+/**
+* Generate a WASI imports object that can be used for
+* manual component instantiation of a transpiled component.
+*
+* Normally, transpiled components contain mapping for WASI interfaces
+* and/or imports that import the relevant packages (ex. `@bytecodealliance/preview2-shim/clocks`)
+* from the right sources.
+*
+* This function makes use of the `WasiShim` object to provide an object that can be easily
+* fed to the `instantiate` function produced by a transpiled component:
+*
+* ```js
+* import { imports } from "@bytecodealliance/preview2-shim.js"
+* // ...
+* import { instantiate } from "path/to/transpiled/component.js"
+* // ...
+* const component = instantiate(null, imports())
+* ```
+*
+* You can also replace imports that you'd like to override with custom implementations,
+* by using the `WasiShim` object directly:
+*
+* ```js
+* import { imports, WasiShim } from "@bytecodealliance/preview2-shim.js"
+* // ...
+* import { instantiate } from "path/to/transpiled/component.js"
+* // ...
+* const defaultImports = imports();
+* const customImports = new WasiShim({
+*     random: {
+*         random: defaultImports["wasi:random/random"],
+*         insecure: {
+*             // Your custom implementation for the `wasi:random/insecure` interface goes here
+*         }
+*         insecure-seed: defaultImports["wasi:random/insecure-seed"],
+*     }
+* });
+*
+* const component = instantiate(null, customImports.importObject())
+* ```
+*/
+export function imports()  {
+  const shim = new WasiP2Shim();
+  return shim.importObject();
+}
path: packages/preview2-shim/lib/common/instantiation.js
user: login: vados-cosmonic
body: Nope, happy to change this!
created_at: 2025-03-19T18:34:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,49 @@
+import { WasiP2Shim } from "./wasi-shim.js";
+
+export { WasiP2Shim } from "./wasi-shim.js";
+
+/**
+* Generate a WASI imports object that can be used for
+* manual component instantiation of a transpiled component.
+*
+* Normally, transpiled components contain mapping for WASI interfaces
+* and/or imports that import the relevant packages (ex. `@bytecodealliance/preview2-shim/clocks`)
+* from the right sources.
+*
+* This function makes use of the `WasiShim` object to provide an object that can be easily
+* fed to the `instantiate` function produced by a transpiled component:
+*
+* ```js
+* import { imports } from "@bytecodealliance/preview2-shim.js"
+* // ...
+* import { instantiate } from "path/to/transpiled/component.js"
+* // ...
+* const component = instantiate(null, imports())
+* ```
+*
+* You can also replace imports that you'd like to override with custom implementations,
+* by using the `WasiShim` object directly:
+*
+* ```js
+* import { imports, WasiShim } from "@bytecodealliance/preview2-shim.js"
+* // ...
+* import { instantiate } from "path/to/transpiled/component.js"
+* // ...
+* const defaultImports = imports();
+* const customImports = new WasiShim({
+*     random: {
+*         random: defaultImports["wasi:random/random"],
+*         insecure: {
+*             // Your custom implementation for the `wasi:random/insecure` interface goes here
+*         }
+*         insecure-seed: defaultImports["wasi:random/insecure-seed"],
+*     }
+* });
+*
+* const component = instantiate(null, customImports.importObject())
+* ```
+*/
+export function imports()  {
+  const shim = new WasiP2Shim();
+  return shim.importObject();
+}
path: packages/preview2-shim/lib/common/instantiation.js
user: login: guybedford
body: Ah, I meant rather than separately defining this function to encourage users to use that expression instead.
created_at: 2025-03-19T18:42:37Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,49 @@
+import { WasiP2Shim } from "./wasi-shim.js";
+
+export { WasiP2Shim } from "./wasi-shim.js";
+
+/**
+* Generate a WASI imports object that can be used for
+* manual component instantiation of a transpiled component.
+*
+* Normally, transpiled components contain mapping for WASI interfaces
+* and/or imports that import the relevant packages (ex. `@bytecodealliance/preview2-shim/clocks`)
+* from the right sources.
+*
+* This function makes use of the `WasiShim` object to provide an object that can be easily
+* fed to the `instantiate` function produced by a transpiled component:
+*
+* ```js
+* import { imports } from "@bytecodealliance/preview2-shim.js"
+* // ...
+* import { instantiate } from "path/to/transpiled/component.js"
+* // ...
+* const component = instantiate(null, imports())
+* ```
+*
+* You can also replace imports that you'd like to override with custom implementations,
+* by using the `WasiShim` object directly:
+*
+* ```js
+* import { imports, WasiShim } from "@bytecodealliance/preview2-shim.js"
+* // ...
+* import { instantiate } from "path/to/transpiled/component.js"
+* // ...
+* const defaultImports = imports();
+* const customImports = new WasiShim({
+*     random: {
+*         random: defaultImports["wasi:random/random"],
+*         insecure: {
+*             // Your custom implementation for the `wasi:random/insecure` interface goes here
+*         }
+*         insecure-seed: defaultImports["wasi:random/insecure-seed"],
+*     }
+* });
+*
+* const component = instantiate(null, customImports.importObject())
+* ```
+*/
+export function imports()  {
+  const shim = new WasiP2Shim();
+  return shim.importObject();
+}
path: packages/preview2-shim/lib/common/instantiation.js
user: login: vados-cosmonic
body: Ah gotcha! sure
created_at: 2025-03-19T18:59:32Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,116 @@
+import * as wasi from "@bytecodealliance/preview2-shim";
+
+/**
+ * A class that holds WASI shims and can be used to configure
+ * an instantiation of a WebAssembly component transpiled with jco
+ * (i.e. via `jco transpile`).
+ *
+ * Normally, transpiled components contain mapping for WASI interfaces
+ * and/or imports that import the relevant packages (ex. `@bytecodealliance/preview2-shim/clocks`)
+ * from the right sources.
+ *
+ * This function makes use of the `Shim` object to provide an object that can be easily
+ * fed to the `instantiate` function produced by a transpiled component:
+ *
+ * ```js
+ * import { Shim } from "@bytecodealliance/preview2-shim/instantiation"
+ * // ...
+ * import { instantiate } from "path/to/transpiled/component.js"
+ * // ...
+ * const component = await instantiate(null, new Shim().importObject())
+ * ```
+ *
+ * You can also replace imports that you'd like to override with custom implementations,
+ * by using the `Shim` object directly:
+ *
+ * ```js
+ * import { random } from "@bytecodealliance/preview2-shim"
+ * import { Shim } from "@bytecodealliance/preview2-shim/instantiation"
+ * // ...
+ * import { instantiate } from "path/to/transpiled/component.js"
+ * // ...
+ * const customShim = new Shim({
+ *     random: {
+ *         // For these two interfaces we re-use the default provided shim
+ *         random: random.random,
+ *         insecure-seed: random.insecureSeed,
+ *         // For insecure, we can supply our own custom implementation
+ *         insecure: {
+ *             ...
+ *         }
+ *     }
+ * });
+ *
+ * const component = await instantiate(null, customShim.importObject())
+ * ```
+ *
+ * @class Shim
+ */
+export class Shim {
path: packages/preview2-shim/lib/common/instantiation.js
user: login: guybedford
body: I think having `Wasi` in the name still makes sense, so `WasiShim` perhaps?
created_at: 2025-03-19T19:48:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,116 @@
+import * as wasi from "@bytecodealliance/preview2-shim";
+
+/**
+ * A class that holds WASI shims and can be used to configure
+ * an instantiation of a WebAssembly component transpiled with jco
+ * (i.e. via `jco transpile`).
+ *
+ * Normally, transpiled components contain mapping for WASI interfaces
+ * and/or imports that import the relevant packages (ex. `@bytecodealliance/preview2-shim/clocks`)
+ * from the right sources.
+ *
+ * This function makes use of the `Shim` object to provide an object that can be easily
+ * fed to the `instantiate` function produced by a transpiled component:
+ *
+ * ```js
+ * import { Shim } from "@bytecodealliance/preview2-shim/instantiation"
+ * // ...
+ * import { instantiate } from "path/to/transpiled/component.js"
+ * // ...
+ * const component = await instantiate(null, new Shim().importObject())
+ * ```
+ *
+ * You can also replace imports that you'd like to override with custom implementations,
+ * by using the `Shim` object directly:
+ *
+ * ```js
+ * import { random } from "@bytecodealliance/preview2-shim"
+ * import { Shim } from "@bytecodealliance/preview2-shim/instantiation"
+ * // ...
+ * import { instantiate } from "path/to/transpiled/component.js"
+ * // ...
+ * const customShim = new Shim({
+ *     random: {
+ *         // For these two interfaces we re-use the default provided shim
+ *         random: random.random,
+ *         insecure-seed: random.insecureSeed,
+ *         // For insecure, we can supply our own custom implementation
+ *         insecure: {
+ *             ...
+ *         }
+ *     }
+ * });
+ *
+ * const component = await instantiate(null, customShim.importObject())
+ * ```
+ *
+ * @class Shim
+ */
+export class Shim {
path: packages/preview2-shim/lib/common/instantiation.js
user: login: vados-cosmonic
body: absolutely -- can do that, and one thing I noticed is that we're *really* close to having an API similar to the [Node WASI construct](https://nodejs.org/api/wasi.html) so I'll rename `importObject` to `getImportObject` while I'm here.

Also, given that most ecosystem JS code uses caps when the thing is caps (ex. `URL`, etc) -- I'm going to go with `WASIShim`, what do you think?
created_at: 2025-03-20T11:27:51Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,116 @@
+import * as wasi from "@bytecodealliance/preview2-shim";
+
+/**
+ * A class that holds WASI shims and can be used to configure
+ * an instantiation of a WebAssembly component transpiled with jco
+ * (i.e. via `jco transpile`).
+ *
+ * Normally, transpiled components contain mapping for WASI interfaces
+ * and/or imports that import the relevant packages (ex. `@bytecodealliance/preview2-shim/clocks`)
+ * from the right sources.
+ *
+ * This function makes use of the `Shim` object to provide an object that can be easily
+ * fed to the `instantiate` function produced by a transpiled component:
+ *
+ * ```js
+ * import { Shim } from "@bytecodealliance/preview2-shim/instantiation"
+ * // ...
+ * import { instantiate } from "path/to/transpiled/component.js"
+ * // ...
+ * const component = await instantiate(null, new Shim().importObject())
+ * ```
+ *
+ * You can also replace imports that you'd like to override with custom implementations,
+ * by using the `Shim` object directly:
+ *
+ * ```js
+ * import { random } from "@bytecodealliance/preview2-shim"
+ * import { Shim } from "@bytecodealliance/preview2-shim/instantiation"
+ * // ...
+ * import { instantiate } from "path/to/transpiled/component.js"
+ * // ...
+ * const customShim = new Shim({
+ *     random: {
+ *         // For these two interfaces we re-use the default provided shim
+ *         random: random.random,
+ *         insecure-seed: random.insecureSeed,
+ *         // For insecure, we can supply our own custom implementation
+ *         insecure: {
+ *             ...
+ *         }
+ *     }
+ * });
+ *
+ * const component = await instantiate(null, customShim.importObject())
+ * ```
+ *
+ * @class Shim
+ */
+export class Shim {
path: packages/preview2-shim/lib/common/instantiation.js
user: login: guybedford
body: Sounds good!
created_at: 2025-03-20T16:11:23Z
author_association: COLLABORATOR

PR comments:
user: login: guybedford
created_at: 2025-03-18T22:36:39Z
body: This is exactly the right approach, thanks.

Make sure to add this into the package.json file `"exports"` for the preview2-shims as a new `"./instantiation"` export or otherwise.
author_association: COLLABORATOR
user: login: vados-cosmonic
created_at: 2025-03-19T00:51:59Z
body: Thanks for taking a look -- will finish out the PR and make sure to update the exports!
author_association: COLLABORATOR
user: login: guybedford
created_at: 2025-03-19T16:52:36Z
body: Sure, just let me know when it is ready for review.
author_association: COLLABORATOR
user: login: vados-cosmonic
created_at: 2025-03-19T17:15:54Z
body: Hey @guybedford yeah this is ready for review now!
author_association: COLLABORATOR
user: login: guybedford
created_at: 2025-03-19T18:43:00Z
body: Looks good, can we add a test for this using one of the instantiation outputs?
author_association: COLLABORATOR
user: login: vados-cosmonic
created_at: 2025-03-19T19:19:08Z
body: Added some tests for the shim usage and a basic overriding case!

(also noticed that we probably should move p2-shims to vitest as well for consistency at some point)
author_association: COLLABORATOR
user: login: guybedford
created_at: 2025-03-20T16:12:01Z
body: STILL getting the Mac timeout unfortunately!
author_association: COLLABORATOR