From 28050a8f174fd607a8a6297ea6077ff192342c8c Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Thu, 15 Oct 2026 17:58:19 +0200 Subject: [PATCH 1/2] fix: lock js-component-bindgen version Pin bindgen to v2.11.0 and require --locked in CI --- .github/workflows/main.yml | 2 +- Cargo.lock | 4 +++- Cargo.toml | 2 +- Makefile | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 94b0812a..6ee7e82d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,7 +34,7 @@ jobs: run: cargo fmt -- --check - name: Run clippy - run: cargo clippy --workspace --all-targets --all-features + run: cargo clippy --locked --workspace --all-targets --all-features ######### # Build # diff --git a/Cargo.lock b/Cargo.lock index 26610fe8..78ee121b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -402,7 +402,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-component-bindgen" -version = "2.10.1" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d73b9c04f15dd549239f5302dd6a80be973d055b93c6ad60b7d8d0c9ab07f9b9" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index 26f516a9..df79a05e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,4 +33,4 @@ wit-parser = { version = "0.258.0", features = ["decoding", "serde"] } wit-bindgen = { version = "0.54.0", features = ["macros", "async", "realloc"] } wit-bindgen-core = { version = "0.61.1", default-features = false } -js-component-bindgen = { version = "2.11.0" } +js-component-bindgen = { version = "=2.11.0" } diff --git a/Makefile b/Makefile index f51852a4..abd4168e 100644 --- a/Makefile +++ b/Makefile @@ -21,8 +21,8 @@ lib/spidermonkey-embedding-splicer.js: target/wasm32-wasip1/release/splicer_comp @$(JCO) new target/wasm32-wasip1/release/splicer_component.wasm -o obj/spidermonkey-embedding-splicer.wasm --wasi-reactor @$(JCO) transpile -q --name spidermonkey-embedding-splicer obj/spidermonkey-embedding-splicer.wasm -o lib -- -O1 -target/wasm32-wasip1/release/splicer_component.wasm: Cargo.toml crates/spidermonkey-embedding-splicer/Cargo.toml crates/spidermonkey-embedding-splicer/src/*.rs crates/splicer-component/src/*.rs - cargo build --lib --release --target wasm32-wasip1 +target/wasm32-wasip1/release/splicer_component.wasm: Cargo.toml Cargo.lock crates/spidermonkey-embedding-splicer/Cargo.toml crates/spidermonkey-embedding-splicer/src/*.rs crates/splicer-component/src/*.rs + cargo build --locked --lib --release --target wasm32-wasip1 lib/starlingmonkey_embedding.wasm: $(STARLINGMONKEY_DEPS) | lib cmake -B build-release -DCMAKE_BUILD_TYPE=Release From 7a4718bed52f349719fec512d905782d7557eebc Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Fri, 16 Oct 2026 10:04:54 +0200 Subject: [PATCH 2/2] fix: explicitly configure splicer filesystem access Newer preview2-shim releases no longer expose the host filesystem by default, preventing the splicer from reading wit paths. --- .github/workflows/main.yml | 8 +- Makefile | 4 +- README.md | 3 +- examples/hello-world/test.sh | 2 +- package-lock.json | 170 ++++++++++++++++++++++++++++++++-- package.json | 2 +- src/componentize.js | 12 ++- src/splicer.js | 34 +++++++ test/api.js | 13 +++ test/import-return-buffers.js | 3 +- test/splicer.js | 115 +++++++++++++++++++++++ types.d.ts | 3 +- 12 files changed, 347 insertions(+), 22 deletions(-) create mode 100644 src/splicer.js create mode 100644 test/splicer.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6ee7e82d..cdeae89d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -59,7 +59,7 @@ jobs: uses: actions/cache@v4 id: splicer-build with: - key: splicer-${{ matrix.os }}-${{ hashFiles('Cargo.lock', 'crates/spidermonkey-embedding-splicer/src/**/*.rs') }} + key: splicer-${{ matrix.os }}-${{ hashFiles('Makefile', 'Cargo.lock', 'crates/spidermonkey-embedding-splicer/src/**/*.rs') }} path: | lib target @@ -134,7 +134,7 @@ jobs: uses: actions/cache/restore@v4 id: splicer-build with: - key: splicer-${{ matrix.os }}-${{ hashFiles('Cargo.lock', 'crates/spidermonkey-embedding-splicer/src/**/*.rs') }} + key: splicer-${{ matrix.os }}-${{ hashFiles('Makefile', 'Cargo.lock', 'crates/spidermonkey-embedding-splicer/src/**/*.rs') }} path: | lib target @@ -195,7 +195,7 @@ jobs: uses: actions/cache/restore@v4 id: splicer-build with: - key: splicer-${{ matrix.os }}-${{ hashFiles('Cargo.lock', 'crates/spidermonkey-embedding-splicer/src/**/*.rs') }} + key: splicer-${{ matrix.os }}-${{ hashFiles('Makefile', 'Cargo.lock', 'crates/spidermonkey-embedding-splicer/src/**/*.rs') }} path: | lib target @@ -239,7 +239,7 @@ jobs: uses: actions/cache/restore@v4 id: splicer-build with: - key: splicer-${{ matrix.os }}-${{ hashFiles('Cargo.lock', 'crates/spidermonkey-embedding-splicer/src/**/*.rs') }} + key: splicer-${{ matrix.os }}-${{ hashFiles('Makefile', 'Cargo.lock', 'crates/spidermonkey-embedding-splicer/src/**/*.rs') }} path: | lib target diff --git a/Makefile b/Makefile index abd4168e..62194334 100644 --- a/Makefile +++ b/Makefile @@ -17,9 +17,9 @@ debug: lib/starlingmonkey_embedding.debug.wasm lib/spidermonkey-embedding-splice release: lib/starlingmonkey_embedding.wasm lib/spidermonkey-embedding-splicer.js release-weval: lib/starlingmonkey_ics.wevalcache lib/spidermonkey-embedding-splicer.js -lib/spidermonkey-embedding-splicer.js: target/wasm32-wasip1/release/splicer_component.wasm crates/spidermonkey-embedding-splicer/wit/spidermonkey-embedding-splicer.wit | obj lib +lib/spidermonkey-embedding-splicer.js: Makefile target/wasm32-wasip1/release/splicer_component.wasm crates/spidermonkey-embedding-splicer/wit/spidermonkey-embedding-splicer.wit | obj lib @$(JCO) new target/wasm32-wasip1/release/splicer_component.wasm -o obj/spidermonkey-embedding-splicer.wasm --wasi-reactor - @$(JCO) transpile -q --name spidermonkey-embedding-splicer obj/spidermonkey-embedding-splicer.wasm -o lib -- -O1 + @$(JCO) transpile -q --instantiation async --no-wasi-shim --name spidermonkey-embedding-splicer obj/spidermonkey-embedding-splicer.wasm -o lib -- -O1 target/wasm32-wasip1/release/splicer_component.wasm: Cargo.toml Cargo.lock crates/spidermonkey-embedding-splicer/Cargo.toml crates/spidermonkey-embedding-splicer/src/*.rs crates/splicer-component/src/*.rs cargo build --locked --lib --release --target wasm32-wasip1 diff --git a/README.md b/README.md index 7e04bdd9..256ef727 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,8 @@ export function componentize(opts: { */ sourcePath: string; /** - * Path to WIT file or folder + * Path to a WIT file or folder. Dependencies must be inside the given folder + * or, for a file, its parent folder. Subfolders are included. */ witPath?: string; /** diff --git a/examples/hello-world/test.sh b/examples/hello-world/test.sh index 7d2d4f9a..7a57f90c 100755 --- a/examples/hello-world/test.sh +++ b/examples/hello-world/test.sh @@ -7,7 +7,7 @@ export TEST_BINARY_PATH=$(realpath host)/target/release/wasmtime-test # Build the JS component if not present echo -e "[info] expecting component WASM at [$COMPONENT_WASM_PATH]..."; if [ ! -f "$COMPONENT_WASM_PATH" ]; then - cd guest && npm install && npm run build && cd .. + cd guest && npm install --no-save ../../.. && npm run build && cd .. fi # Build the Rust embedding test binary if not present diff --git a/package-lock.json b/package-lock.json index 396bc254..5e64c606 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "@bytecodealliance/componentize-js", - "version": "0.22.0", + "version": "0.23.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bytecodealliance/componentize-js", - "version": "0.22.0", + "version": "0.23.0", "workspaces": [ "." ], "dependencies": { "@bytecodealliance/jco": "^1.15.1", + "@bytecodealliance/preview2-shim": "^0.29.0", "@bytecodealliance/weval": "^0.5.0", "@bytecodealliance/wizer": "^10.0.0", "es-module-lexer": "^1.6.0", @@ -21,7 +22,6 @@ "componentize-js": "src/cli.js" }, "devDependencies": { - "@bytecodealliance/preview2-shim": "^0.17.4", "cross-env": "^7.0.3", "semver": "^7.7.2", "vitest": "^3.2.4" @@ -49,6 +49,153 @@ "jco": "src/jco.js" } }, + "node_modules/@bytecodealliance/jco-node-fs": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs/-/jco-node-fs-0.3.2.tgz", + "integrity": "sha512-yxvqQreNcOQdMsEKZomkSqThtkxrAz3f1Cmy8BsV0UHi9saa2/D1KDB14IAXPYwDq/maS6lCzS+MuMQx6QkUgA==", + "license": "(Apache-2.0 WITH LLVM-exception)", + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "@bytecodealliance/jco-node-fs-darwin-arm64": "0.3.2", + "@bytecodealliance/jco-node-fs-darwin-x64": "0.3.2", + "@bytecodealliance/jco-node-fs-linux-arm64-gnu": "0.3.2", + "@bytecodealliance/jco-node-fs-linux-arm64-musl": "0.3.2", + "@bytecodealliance/jco-node-fs-linux-x64-gnu": "0.3.2", + "@bytecodealliance/jco-node-fs-linux-x64-musl": "0.3.2", + "@bytecodealliance/jco-node-fs-win32-arm64-msvc": "0.3.2", + "@bytecodealliance/jco-node-fs-win32-x64-msvc": "0.3.2" + } + }, + "node_modules/@bytecodealliance/jco-node-fs-darwin-arm64": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-darwin-arm64/-/jco-node-fs-darwin-arm64-0.3.2.tgz", + "integrity": "sha512-WXQq1tRVUWhbK+ImH0G8Kni2AR0sdokLf/KyXRw5P6n4A9ZbgMHOuPfN/PEsYgw4eosuTAonutK0xILk/hA81A==", + "cpu": [ + "arm64" + ], + "license": "(Apache-2.0 WITH LLVM-exception)", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20" + } + }, + "node_modules/@bytecodealliance/jco-node-fs-darwin-x64": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-darwin-x64/-/jco-node-fs-darwin-x64-0.3.2.tgz", + "integrity": "sha512-Fy/OjHnto7hNE5ksewVkmAMXG/uXyj+RAKFZWQFTzFkp73UyT5vuPjKq3nvx9KolUGWBG37BudEMb4ZAoD9TbQ==", + "cpu": [ + "x64" + ], + "license": "(Apache-2.0 WITH LLVM-exception)", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20" + } + }, + "node_modules/@bytecodealliance/jco-node-fs-linux-arm64-gnu": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-linux-arm64-gnu/-/jco-node-fs-linux-arm64-gnu-0.3.2.tgz", + "integrity": "sha512-OafRtRu3tfOFQARfkia/LDEqSo2R3yAL1xPwzf4RcRW6JqkOY7qKWsUS5+LPVo/aFaSkMX7F7bdFGpFPmcoZJg==", + "cpu": [ + "arm64" + ], + "license": "(Apache-2.0 WITH LLVM-exception)", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20" + } + }, + "node_modules/@bytecodealliance/jco-node-fs-linux-arm64-musl": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-linux-arm64-musl/-/jco-node-fs-linux-arm64-musl-0.3.2.tgz", + "integrity": "sha512-JblDiwD7+Y+w0d/tjaNDX4QdGvh5lyuDc+S/jDeT7sYA1caA9yiwIjLa8PC/lm2xudqSxD+Cuy4AosXvl+qI0g==", + "cpu": [ + "arm64" + ], + "license": "(Apache-2.0 WITH LLVM-exception)", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20" + } + }, + "node_modules/@bytecodealliance/jco-node-fs-linux-x64-gnu": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-linux-x64-gnu/-/jco-node-fs-linux-x64-gnu-0.3.2.tgz", + "integrity": "sha512-6FzmHoLOsa6b4X9AuFLsvlpNF66Dt4sRRftZHaT1I1pYzCWrJkkpgNJhRXxf67ZPmOwBZJT4dan/g752Rx4xRw==", + "cpu": [ + "x64" + ], + "license": "(Apache-2.0 WITH LLVM-exception)", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20" + } + }, + "node_modules/@bytecodealliance/jco-node-fs-linux-x64-musl": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-linux-x64-musl/-/jco-node-fs-linux-x64-musl-0.3.2.tgz", + "integrity": "sha512-SbAJG2PDV8YAHBESHU9sVSckOitJyvlezdF4CGP5/mOp01JnijVkLVXzGK62yVSfa5TpAybbSd1rk+v36NAW/Q==", + "cpu": [ + "x64" + ], + "license": "(Apache-2.0 WITH LLVM-exception)", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20" + } + }, + "node_modules/@bytecodealliance/jco-node-fs-win32-arm64-msvc": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-win32-arm64-msvc/-/jco-node-fs-win32-arm64-msvc-0.3.2.tgz", + "integrity": "sha512-nUWYdFysRQJGsIOCk0KAYN2smkNYiHG7rZ7m+0ZXqP+5wqWxKEjkDsORwXiBjdaWFLVww0anZ3SBxrju6P7hXQ==", + "cpu": [ + "arm64" + ], + "license": "(Apache-2.0 WITH LLVM-exception)", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20" + } + }, + "node_modules/@bytecodealliance/jco-node-fs-win32-x64-msvc": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@bytecodealliance/jco-node-fs-win32-x64-msvc/-/jco-node-fs-win32-x64-msvc-0.3.2.tgz", + "integrity": "sha512-RsPj2t8TvltsMWK7/Z7JcVC/IWPMi07/AJkNLiNTCuGTdT/sseqTThJJwhuz3sjFw7dJdxA+bSGT+23y/KT0fw==", + "cpu": [ + "x64" + ], + "license": "(Apache-2.0 WITH LLVM-exception)", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20" + } + }, "node_modules/@bytecodealliance/jco/node_modules/@bytecodealliance/componentize-js": { "version": "0.19.3", "resolved": "https://registry.npmjs.org/@bytecodealliance/componentize-js/-/componentize-js-0.19.3.tgz", @@ -66,12 +213,21 @@ "componentize-js": "src/cli.js" } }, - "node_modules/@bytecodealliance/preview2-shim": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@bytecodealliance/preview2-shim/-/preview2-shim-0.17.4.tgz", - "integrity": "sha512-vRfxQ6ob5wCgXlpVNoeIUHrqJ2+JTcnQASNMpoi3OVPRYA2oUfhMJdWFP0u5JVL9FlC4YKe+QKqerr345B3T4A==", + "node_modules/@bytecodealliance/jco/node_modules/@bytecodealliance/preview2-shim": { + "version": "0.17.9", + "resolved": "https://registry.npmjs.org/@bytecodealliance/preview2-shim/-/preview2-shim-0.17.9.tgz", + "integrity": "sha512-i0R3eQBe6PA/o/1EFE3Owe4In2rcccb6QxnjpntM/lPe3/duJ0bRQTVZM2Ufpo99X4eofGeltQUkape1C91FFA==", "license": "(Apache-2.0 WITH LLVM-exception)" }, + "node_modules/@bytecodealliance/preview2-shim": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/preview2-shim/-/preview2-shim-0.29.0.tgz", + "integrity": "sha512-Lhr/LNOx3sxWIo0jimsk64LwmkbbAkmrFbXBUjfu9+A0U4vuZZMm88UoZApLrGKiG8exkTCUXP9ndM4WmDQn6A==", + "license": "(Apache-2.0 WITH LLVM-exception)", + "dependencies": { + "@bytecodealliance/jco-node-fs": "0.3.2" + } + }, "node_modules/@bytecodealliance/weval": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@bytecodealliance/weval/-/weval-0.5.0.tgz", diff --git a/package.json b/package.json index f1d62895..02c09132 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,13 @@ "import": "./src/componentize.js" }, "devDependencies": { - "@bytecodealliance/preview2-shim": "^0.17.4", "cross-env": "^7.0.3", "semver": "^7.7.2", "vitest": "^3.2.4" }, "dependencies": { "@bytecodealliance/jco": "^1.15.1", + "@bytecodealliance/preview2-shim": "^0.29.0", "@bytecodealliance/weval": "^0.5.0", "@bytecodealliance/wizer": "^10.0.0", "es-module-lexer": "^1.6.0", diff --git a/src/componentize.js b/src/componentize.js index 66e5942e..16996e03 100644 --- a/src/componentize.js +++ b/src/componentize.js @@ -19,7 +19,7 @@ import { preview1AdapterReactorPath, } from '@bytecodealliance/jco'; -import { splicer } from '../lib/spidermonkey-embedding-splicer.js'; +import { createSplicer } from './splicer.js'; import { maybeWindowsPath } from './platform.js'; @@ -148,12 +148,16 @@ export async function componentize( features.delete('fetch-event'); } - // Splice the bindigns for the given WIT world into the engine WASM + const { splicer, witPath: splicerWitPath } = await createSplicer( + witWorld == null ? witPath : undefined, + ); + + // Splice the bindings for the given WIT world into the engine WASM let { wasm, jsBindings, exports, imports } = splicer.spliceBindings( await readFile(engine), [...features], witWorld, - maybeWindowsPath(witPath), + splicerWitPath, worldName, false, ); @@ -393,7 +397,7 @@ export async function componentize( bin, [...features], witWorld, - maybeWindowsPath(witPath), + splicerWitPath, worldName, ); diff --git a/src/splicer.js b/src/splicer.js new file mode 100644 index 00000000..21e08fd3 --- /dev/null +++ b/src/splicer.js @@ -0,0 +1,34 @@ +import { readFile, realpath, stat } from 'node:fs/promises'; +import { basename, dirname } from 'node:path'; +import { WASIShim } from '@bytecodealliance/preview2-shim/instantiation'; + +import { instantiate } from '../lib/spidermonkey-embedding-splicer.js'; + +// Compiled modules can be shared, but instances must retain isolated preopens. +const coreModules = new Map(); + +function getCoreModule(name) { + if (!coreModules.has(name)) { + coreModules.set( + name, + readFile(new URL(`../lib/${name}`, import.meta.url)).then(WebAssembly.compile), + ); + } + return coreModules.get(name); +} + +export async function createSplicer(witPath) { + const preopens = {}; + let guestWitPath; + + if (witPath != null) { + const hostPath = await realpath(witPath); + const isDirectory = (await stat(hostPath)).isDirectory(); + preopens['/wit'] = isDirectory ? hostPath : dirname(hostPath); + guestWitPath = isDirectory ? '/wit' : `/wit/${basename(hostPath)}`; + } + + const wasi = new WASIShim({ sandbox: { preopens } }); + const { splicer } = await instantiate(getCoreModule, wasi.getImportObject()); + return { splicer, witPath: guestWitPath }; +} diff --git a/test/api.js b/test/api.js index 66947f94..f3d9c788 100644 --- a/test/api.js +++ b/test/api.js @@ -3,6 +3,7 @@ import { join, resolve } from 'node:path'; import { fileURLToPath, URL } from 'node:url'; import { copyFile, mkdtemp } from 'node:fs/promises'; +import { componentize } from '@bytecodealliance/componentize-js'; import { suite, test, assert } from 'vitest'; import { setupComponent } from "./util.js"; @@ -14,6 +15,18 @@ import { } from './util.js'; suite('API', () => { + test('inline WIT takes precedence over an unreadable WIT path', async () => { + const { component } = await componentize('export function answer() { return 42; }', { + witWorld: 'package test:inline; world inline { export answer: func() -> u32; }', + witPath: join(tmpdir(), 'missing-componentize-inline-wit', 'world.wit'), + disableFeatures: ['stdio', 'random', 'clocks', 'http', 'fetch-event'], + debugBuild: DEBUG_TEST_ENABLED, + enableAot: WEVAL_TEST_ENABLED, + }); + assert.instanceOf(component, Uint8Array); + assert.isAbove(component.length, 8); + }); + // When using a different directory sourcePath as an arg to componentize() // (called via setupComponent() in this test), wizer would fail to initialize the component // due to a missing file -- the path prefix stripping was not correctly being resolved. diff --git a/test/import-return-buffers.js b/test/import-return-buffers.js index 1a4f17c4..f9e3638f 100644 --- a/test/import-return-buffers.js +++ b/test/import-return-buffers.js @@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises'; import { Script } from 'node:vm'; import { assert, test } from 'vitest'; -import { splicer } from '../lib/spidermonkey-embedding-splicer.js'; +import { createSplicer } from '../src/splicer.js'; import { DEBUG_TEST_ENABLED, WEVAL_TEST_ENABLED } from './util.js'; @@ -20,6 +20,7 @@ world test { ? 'starlingmonkey_embedding_weval.wasm' : `starlingmonkey_embedding${DEBUG_TEST_ENABLED ? '.debug' : ''}.wasm`; const engine = await readFile(new URL(`../lib/${engineName}`, import.meta.url)); + const { splicer } = await createSplicer(); const { jsBindings } = splicer.spliceBindings( engine, [], wit, undefined, undefined, false, ); diff --git a/test/splicer.js b/test/splicer.js new file mode 100644 index 00000000..72bad153 --- /dev/null +++ b/test/splicer.js @@ -0,0 +1,115 @@ +import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, relative, toNamespacedPath } from 'node:path'; +import { preopens } from '@bytecodealliance/preview2-shim/filesystem'; +import { afterAll, assert, beforeAll, expect, suite, test } from 'vitest'; + +import { createSplicer } from '../src/splicer.js'; + +const emptyModule = Uint8Array.from([0, 97, 115, 109, 1, 0, 0, 0]); +const features = ['stdio', 'random', 'clocks', 'http', 'fetch-event']; +let tempDir; +let firstDir; +let secondDir; +let witFile; + +function readWorld({ splicer, witPath }, worldName, witWorld) { + const wasm = splicer.stubWasi( + emptyModule, features, witWorld, witPath, worldName, + ); + assert.isTrue(WebAssembly.validate(wasm)); +} + +beforeAll(async () => { + tempDir = await mkdtemp(join(tmpdir(), 'componentize-splicer-')); + firstDir = join(tempDir, 'first world'); + secondDir = join(tempDir, 'second world'); + witFile = join(firstDir, 'world file.wit'); + await mkdir(join(firstDir, 'deps', 'dependency'), { recursive: true }); + await mkdir(secondDir); + await writeFile(witFile, 'package test:first; world first {}'); + await writeFile( + join(secondDir, 'world.wit'), + 'package test:second; world second {}', + ); + await writeFile( + join(firstDir, 'dependent.wit'), + 'package test:first; world dependent { import test:dependency/api; }', + ); + await writeFile( + join(firstDir, 'deps', 'dependency', 'api.wit'), + 'package test:dependency; interface api { ping: func(); }', + ); +}); + +afterAll(async () => { + if (tempDir) await rm(tempDir, { recursive: true }); +}); + +suite('Splicer filesystem', () => { + test('reads absolute and relative WIT files with spaces', async () => { + for (const path of [witFile, relative(process.cwd(), witFile)]) { + const instance = await createSplicer(path); + assert.equal(instance.witPath, '/wit/world file.wit'); + readWorld(instance, 'first'); + } + }); + + test('reads a WIT directory and its dependency tree', async () => { + const instance = await createSplicer(firstDir); + assert.equal(instance.witPath, '/wit'); + readWorld(instance, 'dependent'); + }); + + test('keeps concurrent instances and global preopens isolated', async () => { + const originalPreopens = preopens.getDirectories().slice(); + const [first, second] = await Promise.all([ + createSplicer(firstDir), + createSplicer(secondDir), + ]); + assert.notStrictEqual(first.splicer, second.splicer); + readWorld(first, 'first'); + readWorld(second, 'second'); + assert.throws(() => readWorld(first, 'second'), /World `second` not found/); + assert.throws(() => readWorld(second, 'first'), /World `first` not found/); + assert.deepEqual(preopens.getDirectories(), originalPreopens); + }); + + test('does not expose host paths outside the WIT tree', async () => { + const instance = await createSplicer(firstDir); + assert.throws( + () => readWorld({ ...instance, witPath: join(secondDir, 'world.wit') }, 'second'), + /reading file/, + ); + assert.throws( + () => readWorld({ ...instance, witPath: '/wit/../second world/world.wit' }, 'second'), + /reading file/, + ); + }); + + test('inline WIT works without granting filesystem access', async () => { + const instance = await createSplicer(); + readWorld(instance, 'inline', 'package test:inline; world inline {}'); + assert.throws( + () => readWorld({ ...instance, witPath: witFile }, 'first'), + /reading file/, + ); + }); + + test('resolves a symlinked WIT directory', async () => { + const link = join(tempDir, 'linked world'); + await symlink(await realpath(firstDir), link, 'junction'); + readWorld(await createSplicer(link), 'dependent'); + }); + + test('reports missing and empty WIT paths', async () => { + for (const path of [join(tempDir, 'missing.wit'), '']) { + await expect(createSplicer(path)).rejects.toMatchObject({ code: 'ENOENT' }); + } + }); + + test.runIf(process.platform === 'win32')('reads namespaced Windows paths', async () => { + readWorld(await createSplicer(toNamespacedPath(witFile)), 'first'); + readWorld(await createSplicer(toNamespacedPath(firstDir)), 'dependent'); + }); +}); diff --git a/types.d.ts b/types.d.ts index 8ed60047..0116d414 100644 --- a/types.d.ts +++ b/types.d.ts @@ -6,7 +6,8 @@ interface ComponentizeOptions { */ sourcePath: string; /** - * Path to WIT file or folder + * Path to a WIT file or folder. Dependencies must be inside the given folder + * or, for a file, its parent folder. Subfolders are included. */ witPath?: string; /**