feat(wasm): upgrade preview2-shim and add mutable filesystem support

- Upgrade @bytecodealliance/preview2-shim from 0.18.0 to 0.20.1
- Update import paths from /deps/lib/ to /deps/dist/
- Add _getPreopens and filesystemTypes imports for new functionality
- Implement installMutableFilesystem function with directory operations
- Add unlinkFileAt and removeDirectoryAt methods to Descriptor prototype
- Install mutable filesystem in both browser runner and worker
- Add ENOENT error handling for extensionless ESM imports
- Introduce TYPEPHP_WASM_TEST_STAGE global for debugging
- Enhance error reporting with stage information in test harness
master
韩天峰 2 weeks ago
parent 343084c4ba
commit 6c9adbccad
  1. 18
      examples/wasm-hello/package-lock.json
  2. 2
      examples/wasm-hello/package.json
  3. 50
      examples/wasm-hello/typephp-worker.mjs
  4. 74
      tests/wasm/harness/browser-runner.mjs
  5. 1
      tests/wasm/harness/package-lock.json
  6. 1
      tests/wasm/harness/package.json

@ -8,7 +8,7 @@
"name": "typephp-wasm-hello", "name": "typephp-wasm-hello",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@bytecodealliance/preview2-shim": "0.18.0" "@bytecodealliance/preview2-shim": "0.20.1"
}, },
"devDependencies": { "devDependencies": {
"@bytecodealliance/jco": "^1.27.0", "@bytecodealliance/jco": "^1.27.0",
@ -225,24 +225,10 @@
"oxc-minify": "^0.136.0" "oxc-minify": "^0.136.0"
} }
}, },
"node_modules/@bytecodealliance/jco-transpile/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/jco/node_modules/@bytecodealliance/preview2-shim": {
"version": "0.20.1", "version": "0.20.1",
"resolved": "https://registry.npmmirror.com/@bytecodealliance/preview2-shim/-/preview2-shim-0.20.1.tgz", "resolved": "https://registry.npmmirror.com/@bytecodealliance/preview2-shim/-/preview2-shim-0.20.1.tgz",
"integrity": "sha512-gvQP6DWfeQIYKHBb+nPykku1DRtfXQjM685+e75YYr8ZF2ZxisE4+nWrpwf84+NaWJRfuL5HvoLKQFbup1iwkQ==", "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)" "license": "(Apache-2.0 WITH LLVM-exception)"
}, },
"node_modules/@bytecodealliance/preview3-shim": { "node_modules/@bytecodealliance/preview3-shim": {

@ -9,7 +9,7 @@
"build": "vite build" "build": "vite build"
}, },
"dependencies": { "dependencies": {
"@bytecodealliance/preview2-shim": "0.18.0" "@bytecodealliance/preview2-shim": "0.20.1"
}, },
"devDependencies": { "devDependencies": {
"@bytecodealliance/jco": "^1.27.0", "@bytecodealliance/jco": "^1.27.0",

@ -3,7 +3,11 @@ import {
_setStdin, _setStdin,
_setStdout, _setStdout,
} from '@bytecodealliance/preview2-shim/cli'; } 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'; import { WASIShim } from '@bytecodealliance/preview2-shim/instantiation';
const encoder = new TextEncoder(); const encoder = new TextEncoder();
@ -14,6 +18,49 @@ let persistent = false;
let storageName = 'typephp-wasi-filesystem.json'; let storageName = 'typephp-wasi-filesystem.json';
let extensionQueue = Promise.resolve(); 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) { function outputHandler(stream) {
return { return {
write(bytes) { write(bytes) {
@ -101,6 +148,7 @@ async function start(data) {
storageName = String(data.storageName || 'typephp-wasi-filesystem.json'); storageName = String(data.storageName || 'typephp-wasi-filesystem.json');
fileData = persistent ? await loadFileData(storageName) : { dir: {} }; fileData = persistent ? await loadFileData(storageName) : { dir: {} };
_setFileData(fileData); _setFileData(fileData);
installMutableFilesystem(fileData);
_setStdin(inputHandler(String(data.stdin || ''))); _setStdin(inputHandler(String(data.stdin || '')));
_setStdout(outputHandler('stdout')); _setStdout(outputHandler('stdout'));
_setStderr(outputHandler('stderr')); _setStderr(outputHandler('stderr'));

@ -39,21 +39,22 @@ const pageOptions = JSON.stringify({ ...options, stdin }).replace(/</g, '\\u003c
const html = `<!doctype html> const html = `<!doctype html>
<meta charset="utf-8"> <meta charset="utf-8">
<script type="importmap"> <script type="importmap">
{"imports":{"@bytecodealliance/preview2-shim":"/deps/lib/browser/index.js","@bytecodealliance/preview2-shim/":"/deps/lib/browser/"}} {"imports":{"@bytecodealliance/preview2-shim":"/deps/dist/browser/index.js","@bytecodealliance/preview2-shim/":"/deps/dist/browser/"}}
</script> </script>
<script>globalThis.TYPEPHP_WASM_TEST_OPTIONS = ${pageOptions};</script> <script>globalThis.TYPEPHP_WASM_TEST_OPTIONS = ${pageOptions};</script>
<script type="module" src="/harness.js"></script>`; <script type="module" src="/harness.js"></script>`;
const harnessSource = ` const harnessSource = `
import { _setStderr, _setStdin, _setStdout } from '/deps/lib/browser/cli.js'; import { _setStderr, _setStdin, _setStdout } from '/deps/dist/browser/cli.js';
import { _setFileData } from '/deps/lib/browser/filesystem.js'; import { _getPreopens, _setFileData, types as filesystemTypes } from '/deps/dist/browser/filesystem.js';
import { WASIShim } from '/deps/lib/common/instantiation.js'; import { WASIShim } from '/deps/dist/common/instantiation.js';
import { instantiate } from '/artifact/program.js'; import { instantiate } from '/artifact/program.js';
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const options = globalThis.TYPEPHP_WASM_TEST_OPTIONS; const options = globalThis.TYPEPHP_WASM_TEST_OPTIONS;
let output = ''; let output = '';
globalThis.TYPEPHP_WASM_TEST_STAGE = 'module-loaded';
function outputHandler() { function outputHandler() {
return { 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 { try {
if (typeof WebAssembly.Suspending !== 'function' || typeof WebAssembly.promising !== 'function') { if (typeof WebAssembly.Suspending !== 'function' || typeof WebAssembly.promising !== 'function') {
throw new Error('Chrome does not provide WebAssembly JSPI'); 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 || ''))); _setStdin(inputHandler(String(options.stdin || '')));
_setStdout(outputHandler()); _setStdout(outputHandler());
_setStderr(outputHandler()); _setStderr(outputHandler());
@ -92,7 +138,9 @@ try {
env: { ...(options.env || {}) }, env: { ...(options.env || {}) },
enableNetwork: true, enableNetwork: true,
}}); }});
globalThis.TYPEPHP_WASM_TEST_STAGE = 'instantiating';
const component = await instantiate(null, wasi.getImportObject()); const component = await instantiate(null, wasi.getImportObject());
globalThis.TYPEPHP_WASM_TEST_STAGE = 'running';
const result = await component.run.run(); const result = await component.run.run();
globalThis.TYPEPHP_WASM_TEST_RESULT = { output, result }; globalThis.TYPEPHP_WASM_TEST_RESULT = { output, result };
} catch (error) { } catch (error) {
@ -126,12 +174,21 @@ const server = http.createServer(async (request, response) => {
response.writeHead(404).end(); response.writeHead(404).end();
return; return;
} }
const filename = safePath(mapping[0], mapping[1]); let filename = safePath(mapping[0], mapping[1]);
if (!filename) { if (!filename) {
response.writeHead(403).end(); response.writeHead(403).end();
return; 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.writeHead(200, { 'content-type': contentTypes.get(path.extname(filename)) || 'application/octet-stream' });
response.end(data); response.end(data);
} catch (error) { } catch (error) {
@ -163,7 +220,8 @@ try {
try { try {
await page.waitForFunction(() => globalThis.TYPEPHP_WASM_TEST_RESULT !== undefined, { timeout: 120000 }); await page.waitForFunction(() => globalThis.TYPEPHP_WASM_TEST_RESULT !== undefined, { timeout: 120000 });
} catch (error) { } 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); const result = await page.evaluate(() => globalThis.TYPEPHP_WASM_TEST_RESULT);
process.stdout.write(result.output || ''); process.stdout.write(result.output || '');

@ -7,6 +7,7 @@
"name": "typephp-wasm-test-harness", "name": "typephp-wasm-test-harness",
"dependencies": { "dependencies": {
"@bytecodealliance/jco": "^1.27.0", "@bytecodealliance/jco": "^1.27.0",
"@bytecodealliance/preview2-shim": "0.20.1",
"puppeteer-core": "^24.16.0" "puppeteer-core": "^24.16.0"
} }
}, },

@ -4,6 +4,7 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@bytecodealliance/jco": "^1.27.0", "@bytecodealliance/jco": "^1.27.0",
"@bytecodealliance/preview2-shim": "0.20.1",
"puppeteer-core": "^24.16.0" "puppeteer-core": "^24.16.0"
} }
} }

Loading…
Cancel
Save