diff --git a/examples/wasm-hello/package-lock.json b/examples/wasm-hello/package-lock.json
index 7def2885..8306c072 100644
--- a/examples/wasm-hello/package-lock.json
+++ b/examples/wasm-hello/package-lock.json
@@ -8,7 +8,7 @@
"name": "typephp-wasm-hello",
"version": "0.1.0",
"dependencies": {
- "@bytecodealliance/preview2-shim": "0.18.0"
+ "@bytecodealliance/preview2-shim": "0.20.1"
},
"devDependencies": {
"@bytecodealliance/jco": "^1.27.0",
@@ -225,24 +225,10 @@
"oxc-minify": "^0.136.0"
}
},
- "node_modules/@bytecodealliance/jco-transpile/node_modules/@bytecodealliance/preview2-shim": {
- "version": "0.20.1",
- "resolved": "https://registry.npmmirror.com/@bytecodealliance/preview2-shim/-/preview2-shim-0.20.1.tgz",
- "integrity": "sha512-gvQP6DWfeQIYKHBb+nPykku1DRtfXQjM685+e75YYr8ZF2ZxisE4+nWrpwf84+NaWJRfuL5HvoLKQFbup1iwkQ==",
- "dev": true,
- "license": "(Apache-2.0 WITH LLVM-exception)"
- },
- "node_modules/@bytecodealliance/jco/node_modules/@bytecodealliance/preview2-shim": {
+ "node_modules/@bytecodealliance/preview2-shim": {
"version": "0.20.1",
"resolved": "https://registry.npmmirror.com/@bytecodealliance/preview2-shim/-/preview2-shim-0.20.1.tgz",
"integrity": "sha512-gvQP6DWfeQIYKHBb+nPykku1DRtfXQjM685+e75YYr8ZF2ZxisE4+nWrpwf84+NaWJRfuL5HvoLKQFbup1iwkQ==",
- "dev": true,
- "license": "(Apache-2.0 WITH LLVM-exception)"
- },
- "node_modules/@bytecodealliance/preview2-shim": {
- "version": "0.18.0",
- "resolved": "https://registry.npmjs.org/@bytecodealliance/preview2-shim/-/preview2-shim-0.18.0.tgz",
- "integrity": "sha512-f7+HY98GBrfdKjXb2QT6ycbz+NR+Ocn3Aq87IZ8aVf0lThJocQ4E6dyCC9Y/5l0oOgS9nJ0Z1edeYfmTU87EdA==",
"license": "(Apache-2.0 WITH LLVM-exception)"
},
"node_modules/@bytecodealliance/preview3-shim": {
diff --git a/examples/wasm-hello/package.json b/examples/wasm-hello/package.json
index 74c865a9..6e8038ee 100644
--- a/examples/wasm-hello/package.json
+++ b/examples/wasm-hello/package.json
@@ -9,7 +9,7 @@
"build": "vite build"
},
"dependencies": {
- "@bytecodealliance/preview2-shim": "0.18.0"
+ "@bytecodealliance/preview2-shim": "0.20.1"
},
"devDependencies": {
"@bytecodealliance/jco": "^1.27.0",
diff --git a/examples/wasm-hello/typephp-worker.mjs b/examples/wasm-hello/typephp-worker.mjs
index 9f03e122..1af6381f 100644
--- a/examples/wasm-hello/typephp-worker.mjs
+++ b/examples/wasm-hello/typephp-worker.mjs
@@ -3,7 +3,11 @@ import {
_setStdin,
_setStdout,
} from '@bytecodealliance/preview2-shim/cli';
-import { _setFileData } from '@bytecodealliance/preview2-shim/filesystem';
+import {
+ _getPreopens,
+ _setFileData,
+ types as filesystemTypes,
+} from '@bytecodealliance/preview2-shim/filesystem';
import { WASIShim } from '@bytecodealliance/preview2-shim/instantiation';
const encoder = new TextEncoder();
@@ -14,6 +18,49 @@ let persistent = false;
let storageName = 'typephp-wasi-filesystem.json';
let extensionQueue = Promise.resolve();
+function installMutableFilesystem(data) {
+ const descriptorEntries = new WeakMap();
+ for (const [descriptor] of _getPreopens()) descriptorEntries.set(descriptor, data);
+
+ function resolve(entry, guestPath) {
+ for (const segment of String(guestPath).split('/')) {
+ if (segment === '' || segment === '.') continue;
+ if (segment === '..' || !entry?.dir?.[segment]) throw { tag: 'no-entry' };
+ entry = entry.dir[segment];
+ }
+ return entry;
+ }
+
+ function remove(descriptor, guestPath, directory) {
+ const root = descriptorEntries.get(descriptor);
+ if (!root) throw { tag: 'bad-descriptor' };
+ const segments = String(guestPath).split('/').filter((segment) => segment !== '' && segment !== '.');
+ const name = segments.pop();
+ if (!name || name === '..' || segments.includes('..')) throw { tag: 'no-entry' };
+ const parent = resolve(root, segments.join('/'));
+ const entry = parent?.dir?.[name];
+ if (!entry) throw { tag: 'no-entry' };
+ if (directory ? !entry.dir : entry.dir) throw { tag: directory ? 'not-directory' : 'is-directory' };
+ if (directory && Object.keys(entry.dir).length !== 0) throw { tag: 'not-empty' };
+ delete parent.dir[name];
+ }
+
+ const descriptor = filesystemTypes.Descriptor.prototype;
+ const openAt = descriptor.openAt;
+ descriptor.openAt = function (...args) {
+ const opened = openAt.apply(this, args);
+ const parent = descriptorEntries.get(this);
+ if (parent) descriptorEntries.set(opened, resolve(parent, args[1]));
+ return opened;
+ };
+ descriptor.unlinkFileAt = function (guestPath) {
+ remove(this, guestPath, false);
+ };
+ descriptor.removeDirectoryAt = function (guestPath) {
+ remove(this, guestPath, true);
+ };
+}
+
function outputHandler(stream) {
return {
write(bytes) {
@@ -101,6 +148,7 @@ async function start(data) {
storageName = String(data.storageName || 'typephp-wasi-filesystem.json');
fileData = persistent ? await loadFileData(storageName) : { dir: {} };
_setFileData(fileData);
+ installMutableFilesystem(fileData);
_setStdin(inputHandler(String(data.stdin || '')));
_setStdout(outputHandler('stdout'));
_setStderr(outputHandler('stderr'));
diff --git a/tests/wasm/harness/browser-runner.mjs b/tests/wasm/harness/browser-runner.mjs
index f3fe0708..df14355d 100644
--- a/tests/wasm/harness/browser-runner.mjs
+++ b/tests/wasm/harness/browser-runner.mjs
@@ -39,21 +39,22 @@ const pageOptions = JSON.stringify({ ...options, stdin }).replace(/
`;
const harnessSource = `
-import { _setStderr, _setStdin, _setStdout } from '/deps/lib/browser/cli.js';
-import { _setFileData } from '/deps/lib/browser/filesystem.js';
-import { WASIShim } from '/deps/lib/common/instantiation.js';
+import { _setStderr, _setStdin, _setStdout } from '/deps/dist/browser/cli.js';
+import { _getPreopens, _setFileData, types as filesystemTypes } from '/deps/dist/browser/filesystem.js';
+import { WASIShim } from '/deps/dist/common/instantiation.js';
import { instantiate } from '/artifact/program.js';
const decoder = new TextDecoder();
const encoder = new TextEncoder();
const options = globalThis.TYPEPHP_WASM_TEST_OPTIONS;
let output = '';
+globalThis.TYPEPHP_WASM_TEST_STAGE = 'module-loaded';
function outputHandler() {
return {
@@ -79,11 +80,56 @@ function inputHandler(text) {
};
}
+function installMutableFilesystem(fileData) {
+ const descriptorEntries = new WeakMap();
+ for (const [descriptor] of _getPreopens()) descriptorEntries.set(descriptor, fileData);
+
+ function resolve(entry, guestPath) {
+ for (const segment of String(guestPath).split('/')) {
+ if (segment === '' || segment === '.') continue;
+ if (segment === '..' || !entry?.dir?.[segment]) throw { tag: 'no-entry' };
+ entry = entry.dir[segment];
+ }
+ return entry;
+ }
+
+ function remove(descriptor, guestPath, directory) {
+ const root = descriptorEntries.get(descriptor);
+ if (!root) throw { tag: 'bad-descriptor' };
+ const segments = String(guestPath).split('/').filter((segment) => segment !== '' && segment !== '.');
+ const name = segments.pop();
+ if (!name || name === '..' || segments.includes('..')) throw { tag: 'no-entry' };
+ const parent = resolve(root, segments.join('/'));
+ const entry = parent?.dir?.[name];
+ if (!entry) throw { tag: 'no-entry' };
+ if (directory ? !entry.dir : entry.dir) throw { tag: directory ? 'not-directory' : 'is-directory' };
+ if (directory && Object.keys(entry.dir).length !== 0) throw { tag: 'not-empty' };
+ delete parent.dir[name];
+ }
+
+ const descriptor = filesystemTypes.Descriptor.prototype;
+ const openAt = descriptor.openAt;
+ descriptor.openAt = function (...args) {
+ const opened = openAt.apply(this, args);
+ const parent = descriptorEntries.get(this);
+ if (parent) descriptorEntries.set(opened, resolve(parent, args[1]));
+ return opened;
+ };
+ descriptor.unlinkFileAt = function (guestPath) {
+ remove(this, guestPath, false);
+ };
+ descriptor.removeDirectoryAt = function (guestPath) {
+ remove(this, guestPath, true);
+ };
+}
+
try {
if (typeof WebAssembly.Suspending !== 'function' || typeof WebAssembly.promising !== 'function') {
throw new Error('Chrome does not provide WebAssembly JSPI');
}
- _setFileData({ dir: { sandbox: { dir: {} } } });
+ const fileData = { dir: { sandbox: { dir: {} } } };
+ _setFileData(fileData);
+ installMutableFilesystem(fileData);
_setStdin(inputHandler(String(options.stdin || '')));
_setStdout(outputHandler());
_setStderr(outputHandler());
@@ -92,7 +138,9 @@ try {
env: { ...(options.env || {}) },
enableNetwork: true,
}});
+ globalThis.TYPEPHP_WASM_TEST_STAGE = 'instantiating';
const component = await instantiate(null, wasi.getImportObject());
+ globalThis.TYPEPHP_WASM_TEST_STAGE = 'running';
const result = await component.run.run();
globalThis.TYPEPHP_WASM_TEST_RESULT = { output, result };
} catch (error) {
@@ -126,12 +174,21 @@ const server = http.createServer(async (request, response) => {
response.writeHead(404).end();
return;
}
- const filename = safePath(mapping[0], mapping[1]);
+ let filename = safePath(mapping[0], mapping[1]);
if (!filename) {
response.writeHead(403).end();
return;
}
- const data = await fs.readFile(filename);
+ let data;
+ try {
+ data = await fs.readFile(filename);
+ } catch (error) {
+ // The browser shim publishes Node-style extensionless ESM imports.
+ // Resolve those within the already validated static root only.
+ if (error?.code !== 'ENOENT' || path.extname(filename) !== '') throw error;
+ filename += '.js';
+ data = await fs.readFile(filename);
+ }
response.writeHead(200, { 'content-type': contentTypes.get(path.extname(filename)) || 'application/octet-stream' });
response.end(data);
} catch (error) {
@@ -163,7 +220,8 @@ try {
try {
await page.waitForFunction(() => globalThis.TYPEPHP_WASM_TEST_RESULT !== undefined, { timeout: 120000 });
} catch (error) {
- throw new Error(`${error.message}\n${diagnostics.join('\n')}`);
+ const stage = await page.evaluate(() => globalThis.TYPEPHP_WASM_TEST_STAGE || 'page-loading');
+ throw new Error(`${error.message}\nstage: ${stage}\n${diagnostics.join('\n')}`);
}
const result = await page.evaluate(() => globalThis.TYPEPHP_WASM_TEST_RESULT);
process.stdout.write(result.output || '');
diff --git a/tests/wasm/harness/package-lock.json b/tests/wasm/harness/package-lock.json
index 90948fbf..a4cd30cb 100644
--- a/tests/wasm/harness/package-lock.json
+++ b/tests/wasm/harness/package-lock.json
@@ -7,6 +7,7 @@
"name": "typephp-wasm-test-harness",
"dependencies": {
"@bytecodealliance/jco": "^1.27.0",
+ "@bytecodealliance/preview2-shim": "0.20.1",
"puppeteer-core": "^24.16.0"
}
},
diff --git a/tests/wasm/harness/package.json b/tests/wasm/harness/package.json
index 37f6310c..cf922f74 100644
--- a/tests/wasm/harness/package.json
+++ b/tests/wasm/harness/package.json
@@ -4,6 +4,7 @@
"type": "module",
"dependencies": {
"@bytecodealliance/jco": "^1.27.0",
+ "@bytecodealliance/preview2-shim": "0.20.1",
"puppeteer-core": "^24.16.0"
}
}