diff --git a/.eslintignore b/.eslintignore index a639cfb4..509b1144 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,6 +3,7 @@ node_modules /lib/typings .eslintrc.js +.eslintrc.cjs /example /test/.build /test/CMakeFiles @@ -13,4 +14,4 @@ node_modules /packages/**/test/**/actual /packages/**/test/**/expected /packages/**/test/**/rollup/src -/out +out diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 98% rename from .eslintrc.js rename to .eslintrc.cjs index e79553e6..1513e601 100644 --- a/.eslintrc.js +++ b/.eslintrc.cjs @@ -4,7 +4,7 @@ const sharedRules = { 'no-new-func': 'off', 'no-implied-eval': 'off', 'no-var': 'off', - 'camelcase': 'off' + camelcase: 'off' } module.exports = { @@ -72,7 +72,7 @@ module.exports = { project: './tsconfig.json', tsconfigRootDir: __dirname, createDefaultProgram: true - }, + } } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index d19295ae..c211ebf6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "files.associations": { - "**/api-extractor.json": "jsonc" + "**/api-extractor*.json": "jsonc" }, "typescript.tsdk": "./node_modules/typescript/lib", "cmake.sourceDirectory": "${workspaceFolder}/packages/test", @@ -17,5 +17,9 @@ "launch": { "statusBarVisibility": "hidden" } + }, + "livePreview.httpHeaders": { + "Cross-Origin-Embedder-Policy": "require-corp", + "Cross-Origin-Opener-Policy": "same-origin" } } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 663569c7..21fbdecc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,7 @@ This doc will explain the structure of this project and some points need to note Macros are powered by `packages/ts-transform-macro` -- `packages/core` (`dependencies`) +- `packages/core` (`dependencies`, follow emnapi version) This package is designed for using emnapi on non-Emscripten platform, it is a trasformed output of `packages/emnapi` by using `packages/ts-transform-emscripten-parse-tools`. @@ -64,15 +64,25 @@ This doc will explain the structure of this project and some points need to note We need to manually provide imported symbols to the second parameter of `WebAssembly.instantiate`, so this is the use case of this package. -- `packages/runtime` (`dependencies`) +- `packages/runtime` (`dependencies`, follow emnapi version) Provide runtime implementation of `napi_value` / `napi_handle_scope` / `napi_ref` etc. -- `packages/node` (`dependencies`) +- `packages/node` (`dependencies`, follow emnapi version) Some APIs make sense on Node.js only, such as `napi_async_init`, `napi_async_destroy` and `napi_make_callback`. This package provide native Node.js bindings for JavaScript implementation as bridge. +- `packages/rollup-plugin-emscripten-esm-library` (`devDependencies`, independent) + +- `packages/ts-transform-emscripten-esm-library` (`devDependencies`, independent) + +- `packages/ts-transform-emscripten-parse-tools` (`devDependencies`, follow emnapi version, private) + +- `packages/ts-transform-emscripten-parse-tools` (`devDependencies`, independent) + +- `packages/wasi-threads` (`dependencies`, independent) + ## Environment Requirements - Node.js `>= 16` diff --git a/package.json b/package.json index 353d8fda..268e586e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "version": "0.0.0", + "type": "module", "scripts": { "prepare": "npm run build", "build": "npm run build --workspaces --if-present", @@ -53,6 +54,7 @@ "typescript": "~5.7.2" }, "workspaces": [ + "packages/shared", "packages/ts-transform-macro", "packages/ts-transform-emscripten-esm-library", "packages/ts-transform-emscripten-parse-tools", diff --git a/packages/core/.npmignore b/packages/core/.npmignore index f944142a..f6ce3c8f 100644 --- a/packages/core/.npmignore +++ b/packages/core/.npmignore @@ -7,3 +7,4 @@ node_modules .npmignore api-extractor.json tsconfig.json +/dist/types diff --git a/packages/core/api-extractor.json b/packages/core/api-extractor.json index 9f068bb7..0769ff33 100644 --- a/packages/core/api-extractor.json +++ b/packages/core/api-extractor.json @@ -1,115 +1,3 @@ { - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - - // "extends": "./shared/api-extractor-base.json" - // "extends": "my-package/include/api-extractor-base.json" - - "projectFolder": ".", - - "mainEntryPointFilePath": "/lib/typings/index.d.ts", - - "bundledPackages": [], - - "compiler": { - "tsconfigFilePath": "/tsconfig.json" - - // "overrideTsconfig": { - // . . . - // } - - // "skipLibCheck": true, - }, - - "apiReport": { - "enabled": false - - // "reportFileName": ".api.md", - - // "reportFolder": "/etc/", - - // "reportTempFolder": "/api/temp/" - }, - - "docModel": { - "enabled": false - - // "apiJsonFilePath": "/temp/.api.json" - }, - - "dtsRollup": { - "enabled": true, - - "untrimmedFilePath": "", - - // "betaTrimmedFilePath": "/dist/-beta.d.ts", - - "publicTrimmedFilePath": "/dist/emnapi-core.d.ts" - - // "omitTrimmingComments": true - }, - - "tsdocMetadata": { - "enabled": false, - - "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" - }, - - // "newlineKind": "crlf", - - "messages": { - /** - * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing - * the input .d.ts files. - * - * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" - * - * DEFAULT VALUE: A single "default" entry with logLevel=warning. - */ - "compilerMessageReporting": { - "default": { - "logLevel": "warning" - - // "addToApiReportFile": false - } - - // "TS2551": { - // "logLevel": "warning", - // "addToApiReportFile": true - // }, - // - // . . . - }, - - "extractorMessageReporting": { - "default": { - "logLevel": "warning" - // "addToApiReportFile": false - }, - "ae-missing-release-tag": { - "logLevel": "none", - "addToApiReportFile": false - } - - // "ae-extra-release-tag": { - // "logLevel": "warning", - // "addToApiReportFile": true - // }, - // - // . . . - }, - - "tsdocMessageReporting": { - "default": { - "logLevel": "warning" - // "addToApiReportFile": false - } - - // "tsdoc-link-tag-unescaped-text": { - // "logLevel": "warning", - // "addToApiReportFile": true - // }, - // - // . . . - } - } + "extends": "../shared/api-extractor-base.json" } diff --git a/packages/core/index.js b/packages/core/index.js deleted file mode 100644 index 11764ede..00000000 --- a/packages/core/index.js +++ /dev/null @@ -1,5 +0,0 @@ -if (typeof process !== 'undefined' && process.env.NODE_ENV === 'production') { - module.exports = require('./dist/emnapi-core.cjs.min.js') -} else { - module.exports = require('./dist/emnapi-core.cjs.js') -} diff --git a/packages/core/package.json b/packages/core/package.json index 079640c0..24e6fab4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -2,36 +2,32 @@ "name": "@emnapi/core", "version": "1.3.1", "description": "emnapi core", - "main": "index.js", - "module": "./dist/emnapi-core.esm-bundler.js", - "types": "./dist/emnapi-core.d.ts", + "type": "module", + "main": "./dist/emnapi-core.cjs", + "module": "./dist/emnapi-core.js", + "types": "./dist/emnapi-core.d.cts", "sideEffects": false, "exports": { ".": { "types": { "module": "./dist/emnapi-core.d.ts", - "import": "./dist/emnapi-core.d.mts", - "default": "./dist/emnapi-core.d.ts" + "module-sync": "./dist/emnapi-core.d.ts", + "import": "./dist/emnapi-core.d.cts", + "require": "./dist/emnapi-core.d.cts", + "default": "./dist/emnapi-core.d.cts" }, - "module": "./dist/emnapi-core.esm-bundler.js", - "import": "./dist/emnapi-core.mjs", - "default": "./index.js" + "module": "./dist/emnapi-core.js", + "module-sync": "./dist/emnapi-core.js", + "default": "./dist/emnapi-core.cjs" }, - "./dist/emnapi-core.cjs.min": { - "types": "./dist/emnapi-core.d.ts", - "default": "./dist/emnapi-core.cjs.min.js" - }, - "./dist/emnapi-core.min.mjs": { - "types": "./dist/emnapi-core.d.mts", - "default": "./dist/emnapi-core.min.mjs" - } + "./package.json": "./package.json" }, "dependencies": { "@emnapi/wasi-threads": "1.0.1", "tslib": "^2.4.0" }, "scripts": { - "build": "node ./script/build.js" + "build": "node ./script/build.js && rollup -c" }, "repository": { "type": "git", diff --git a/packages/core/rollup.config.js b/packages/core/rollup.config.js new file mode 100644 index 00000000..30306bc3 --- /dev/null +++ b/packages/core/rollup.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from '@emnapi/shared' +import pkg from './package.json' with { type: "json" } + +export default defineConfig({ + outputName: 'emnapiCore', + outputFile: 'emnapi-core', + defines: { + __VERSION__: JSON.stringify(pkg.version) + } +}) diff --git a/packages/core/script/build.js b/packages/core/script/build.js index 9d3a01b3..248e3468 100644 --- a/packages/core/script/build.js +++ b/packages/core/script/build.js @@ -1,200 +1,10 @@ -const path = require('path') -const fs = require('fs-extra') -const rollup = require('rollup') -const ts = require('typescript') -const rollupTypescript = require('@rollup/plugin-typescript').default -const rollupNodeResolve = require('@rollup/plugin-node-resolve').default -const rollupReplace = require('@rollup/plugin-replace').default -const rollupTerser = require('@rollup/plugin-terser').default -const rollupAlias = require('@rollup/plugin-alias').default -const { compile } = require('@tybys/tsapi') -const { globSync } = require('glob') -const dist = path.join(__dirname, '../dist') - -function build () { - compile(path.join(__dirname, '../tsconfig.json'), { - optionsToExtend: { - target: ts.ScriptTarget.ES2021, - outDir: path.join(__dirname, '../lib'), - emitDeclarationOnly: true, - declaration: true, - declarationDir: path.join(__dirname, '../lib/typings') - } - }) - - globSync('**/*.d.ts', { cwd: path.join(__dirname, '../src/emnapi') }).forEach(file => { - const from = path.join(path.join(__dirname, '../src/emnapi', file)) - const to = path.join(path.join(__dirname, '../lib/typings/emnapi', file)) - fs.mkdirSync(path.dirname(to), { recursive: true }) - fs.copyFileSync(from, to) - }) - - /** - * @param {ts.ScriptTarget} esversion - * @param {boolean=} minify - * @returns {rollup.RollupOptions} - */ - function createInput (esversion, minify, external) { - return { - input: path.join(__dirname, '../src/index.ts'), - external, - plugins: [ - rollupTypescript({ - tsconfig: path.join(__dirname, '../tsconfig.json'), - tslib: path.join( - path.dirname(require.resolve('tslib')), - JSON.parse(fs.readFileSync(path.join(path.dirname(require.resolve('tslib')), 'package.json'))).module - ), - compilerOptions: { - target: esversion, - ...(esversion !== ts.ScriptTarget.ES5 ? { removeComments: true, downlevelIteration: false } : {}) - }, - include: [ - './src/**/*' - ], - transformers: { - after: [ - require('@tybys/ts-transform-pure-class').default - ] - } - }), - rollupAlias({ - entries: [ - { find: '@', replacement: path.join(__dirname, '../src') } - ] - }), - rollupNodeResolve({ - mainFields: ['module', 'main'] - }), - rollupReplace({ - preventAssignment: true, - values: { - __VERSION__: JSON.stringify(require('../package.json').version) - } - }), - ...(minify - ? [ - rollupTerser({ - compress: true, - mangle: true, - format: { - comments: false - } - }) - ] - : []) - ] - } - } - - return Promise.all(([ - { - input: createInput(ts.ScriptTarget.ES2021, false), - output: { - file: path.join(dist, 'emnapi-core.js'), - format: 'umd', - name: 'emnapiCore', - exports: 'named', - strict: false - } - }, - { - input: createInput(ts.ScriptTarget.ES2021, true), - output: { - file: path.join(dist, 'emnapi-core.min.js'), - format: 'umd', - name: 'emnapiCore', - exports: 'named', - strict: false - } - }, - { - input: createInput(ts.ScriptTarget.ES2021, false, ['tslib', '@emnapi/wasi-threads']), - output: { - file: path.join(dist, 'emnapi-core.cjs.js'), - format: 'cjs', - name: 'emnapiCore', - exports: 'named', - strict: false - } - }, - { - input: createInput(ts.ScriptTarget.ES2021, true, ['tslib', '@emnapi/wasi-threads']), - output: { - file: path.join(dist, 'emnapi-core.cjs.min.js'), - format: 'cjs', - name: 'emnapiCore', - exports: 'named', - strict: false - } - }, - { - input: createInput(ts.ScriptTarget.ES2021, false, ['tslib', '@emnapi/wasi-threads']), - output: { - file: path.join(dist, 'emnapi-core.mjs'), - format: 'esm', - name: 'emnapiCore', - exports: 'named', - strict: false - } - }, - { - input: createInput(ts.ScriptTarget.ES2021, true, ['tslib', '@emnapi/wasi-threads']), - output: { - file: path.join(dist, 'emnapi-core.min.mjs'), - format: 'esm', - name: 'emnapiCore', - exports: 'named', - strict: false - } - }, - { - input: createInput(ts.ScriptTarget.ES2021, false, ['tslib', '@emnapi/wasi-threads']), - output: { - file: path.join(dist, 'emnapi-core.esm-bundler.js'), - format: 'esm', - name: 'emnapiCore', - exports: 'named', - strict: false - } - } - ]).map(conf => { - return rollup.rollup(conf.input).then(bundle => bundle.write(conf.output)) - })).then(() => { - const { - Extractor, - ExtractorConfig - } = require('@microsoft/api-extractor') - const apiExtractorJsonPath = path.join(__dirname, '../api-extractor.json') - const extractorConfig = ExtractorConfig.loadFileAndPrepare(apiExtractorJsonPath) - const extractorResult = Extractor.invoke(extractorConfig, { - localBuild: true, - showVerboseMessages: true - }) - if (extractorResult.succeeded) { - console.log('API Extractor completed successfully') - } else { - const errmsg = `API Extractor completed with ${extractorResult.errorCount} errors and ${extractorResult.warningCount} warnings` - return Promise.reject(new Error(errmsg)) - } - - const dts = extractorConfig.publicTrimmedFilePath - - const mDts = path.join(__dirname, '../dist/emnapi-core.d.mts') - const cjsMinDts = path.join(__dirname, '../dist/emnapi-core.cjs.min.d.ts') - const mjsMinDts = path.join(__dirname, '../dist/emnapi-core.min.d.mts') - fs.copyFileSync(dts, mDts) - fs.copyFileSync(dts, cjsMinDts) - fs.copyFileSync(dts, mjsMinDts) - fs.appendFileSync(dts, '\nexport as namespace emnapiCore;\n', 'utf8') - }) -} - -exports.build = build - -if (module === require.main) { - build().catch(err => { - console.error(err) - process.exit(1) - }) -} +import path from 'path' +import fs from 'fs' +import { globSync } from 'glob' + +globSync('**/*.d.ts', { cwd: path.join(import.meta.dirname, '../src/emnapi') }).forEach(file => { + const from = path.join(path.join(import.meta.dirname, '../src/emnapi', file)) + const to = path.join(path.join(import.meta.dirname, '../dist/types/emnapi', file)) + fs.mkdirSync(path.dirname(to), { recursive: true }) + fs.copyFileSync(from, to) +}) diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index a34a2a46..cf64b210 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -10,7 +10,7 @@ "outDir": "./dist", "paths": { "tslib" : ["../../node_modules/tslib/tslib.d.ts"], - "@emnapi/wasi-threads": ["../wasi-threads/lib/typings/index.d.ts"], + "@emnapi/wasi-threads": ["../wasi-threads/dist/types/index.d.ts"], "@/*": ["./src/*"], } }, diff --git a/packages/emnapi/README.md b/packages/emnapi/README.md index 64a48d2e..53dd62db 100644 --- a/packages/emnapi/README.md +++ b/packages/emnapi/README.md @@ -43,7 +43,7 @@ If you want to deep dive into WebAssembly, highly recommend you to visit [learn- You will need to install: -- Node.js `>= v16.15.0` +- Node.js `>= v22.12.0` for developing this repository on local machine, `>= v16.15.0` for user runtime. - npm `>= v8` - Emscripten `>= v3.1.9` / wasi-sdk / LLVM clang with wasm support - (Optional) CMake `>= v3.13` @@ -303,7 +303,7 @@ declare namespace Module { ``` ```html - + - + + - - - diff --git a/packages/wasi-threads/test/index.js b/packages/wasi-threads/test/index.js index 511a04f4..f34b6469 100644 --- a/packages/wasi-threads/test/index.js +++ b/packages/wasi-threads/test/index.js @@ -1,113 +1,6 @@ -(function (main) { - const ENVIRONMENT_IS_NODE = - typeof process === 'object' && process !== null && - typeof process.versions === 'object' && process.versions !== null && - typeof process.versions.node === 'string' +import { WASI } from 'wasi' +import { Worker } from 'worker_threads' +import { WASIThreads } from '@emnapi/wasi-threads' +import { main } from './run.js' - if (ENVIRONMENT_IS_NODE) { - const _require = function (request) { - if (request === '@emnapi/wasi-threads') return require('..') - return require(request) - } - main(_require, process, __dirname) - } else { - if (typeof importScripts === 'function') { - // eslint-disable-next-line no-undef - importScripts('../../../node_modules/@tybys/wasm-util/dist/wasm-util.min.js') - // eslint-disable-next-line no-undef - importScripts('../dist/wasi-threads.js') - // eslint-disable-next-line no-undef - importScripts('./proxy.js') - } - - const nodeWasi = { WASI: globalThis.wasmUtil.WASI } - const nodePath = { - join: function () { - return Array.prototype.join.call(arguments, '/') - } - } - const nodeWorkerThreads = { - Worker: globalThis.proxyWorker.Worker - } - const _require = function (request) { - if (request === '@emnapi/wasi-threads') return globalThis.wasiThreads - if (request === 'node:worker_threads' || request === 'worker_threads') return nodeWorkerThreads - if (request === 'node:wasi' || request === 'wasi') return nodeWasi - if (request === 'node:path' || request === 'path') return nodePath - throw new Error('Can not find module: ' + request) - } - const _process = { - env: {}, - exit: () => {} - } - main(_require, _process, '.') - } -})(async function (require, process, __dirname) { - const { WASI } = require('node:wasi') - const { WASIThreads } = require('@emnapi/wasi-threads') - const { Worker } = require('node:worker_threads') - const { join } = require('node:path') - - async function run (file) { - const wasi = new WASI({ - version: 'preview1', - args: [file, 'node'], - env: process.env - }) - const wasiThreads = new WASIThreads({ - wasi, - onCreateWorker: ({ name }) => { - return new Worker(join(__dirname, 'worker.js'), { - name, - workerData: { - name - }, - env: process.env, - execArgv: ['--experimental-wasi-unstable-preview1'] - }) - }, - // optional - waitThreadStart: 1000 - }) - const memory = new WebAssembly.Memory({ - initial: 16777216 / 65536, - maximum: 2147483648 / 65536, - shared: true - }) - let input - try { - input = require('node:fs').readFileSync(require('node:path').join(__dirname, file)) - } catch (err) { - console.warn(err) - const response = await fetch(file) - input = await response.arrayBuffer() - } - let { module, instance } = await WebAssembly.instantiate(input, { - env: { - memory, - print_string: function (ptr) { - const HEAPU8 = new Uint8Array(memory.buffer) - let len = 0 - while (HEAPU8[ptr + len] !== 0) len++ - const string = new TextDecoder().decode(HEAPU8.slice(ptr, ptr + len)) - console.log(string) - } - }, - ...wasi.getImportObject(), - ...wasiThreads.getImportObject() - }) - - if (typeof instance.exports._start === 'function') { - const { exitCode } = wasiThreads.start(instance, module, memory) - return exitCode - } else { - instance = wasiThreads.initialize(instance, module, memory) - return instance.exports.fn(1) - } - } - - console.log('-------- command --------') - await run('main.wasm') - console.log('-------- reactor --------') - await run('lib.wasm') -}) +main(WASI, WASIThreads, Worker, process, './worker.js') diff --git a/packages/wasi-threads/test/proxy.js b/packages/wasi-threads/test/proxy.js index e4377d03..8ea40025 100644 --- a/packages/wasi-threads/test/proxy.js +++ b/packages/wasi-threads/test/proxy.js @@ -1,93 +1,86 @@ -(function (exports) { - function addProxyListener (worker) { - const map = new Map() - worker.onmessage = (e) => { - const { type, payload } = e.data - if (type === 'new') { - const { id, url, options } = payload - const w = new globalThis.Worker(url, options) - map.set(id, w) - w.onmessage = (e) => { - worker.postMessage({ type: 'onmessage', payload: { id, data: e.data } }) - } - w.onmessageerror = (e) => { - worker.postMessage({ type: 'onmessageerror', payload: { id, data: e.data } }) +export class Worker { + constructor (url, options) { + if (typeof window !== 'undefined') { + return new window.Worker(url, options) + } + this.id = String(Math.random()) + globalThis.addEventListener('message', ({ data }) => { + if (data.payload.id === this.id) { + if (data.type === 'onmessage' || data.type === 'onmessageerror') { + this[data.type]?.({ data: data.payload.data }) } - w.onerror = (e) => { - worker.postMessage({ - type: 'onerror', - payload: { - id, - data: { - message: e.message, - filename: e.filename, - lineno: e.lineno, - colno: e.colno, - error: e.error - } - } - }) + if (data.type === 'error') { + this.onerror?.(data.payload.data) } - } else if (type === 'postMessage') { - const { id, args } = payload - const w = map.get(id) - w.postMessage.apply(w, args) - } else if (type === 'terminate') { - const { id } = payload - map.get(id).terminate() - map.delete(id) } - } + }) + postMessage({ + type: 'new', + payload: { + id: this.id, + url: typeof url === 'string' ? url : url.href, + options + } + }) } - class Worker { - constructor (url, options) { - if (typeof window !== 'undefined') { - throw new Error('Can not use ProxyWorker in browser main thread') + postMessage () { + postMessage({ + type: 'postMessage', + payload: { + id: this.id, + args: Array.prototype.slice.call(arguments) } - this.id = String(Math.random()) - globalThis.addEventListener('message', ({ data }) => { - if (data.payload.id === this.id) { - if (data.type === 'onmessage' || data.type === 'onmessageerror') { - this[data.type]?.({ data: data.payload.data }) - } - if (data.type === 'error') { - this.onerror?.(data.payload.data) - } - } - }) - postMessage({ - type: 'new', - payload: { - id: this.id, - url, - options - } - }) - } - - postMessage () { - postMessage({ - type: 'postMessage', - payload: { - id: this.id, - args: Array.prototype.slice.call(arguments) - } - }) - } + }) + } - terminate () { - postMessage({ - type: 'terminate', - payload: { - id: this.id - } - }) - } + terminate () { + postMessage({ + type: 'terminate', + payload: { + id: this.id + } + }) } +} - exports.proxyWorker = { - Worker, - addProxyListener +export function addProxyListener (worker) { + const map = new Map() + worker.onmessage = (e) => { + const { type, payload } = e.data + if (type === 'new') { + const { id, url, options } = payload + const w = new globalThis.Worker(url, options) + map.set(id, w) + w.onmessage = (e) => { + worker.postMessage({ type: 'onmessage', payload: { id, data: e.data } }) + } + w.onmessageerror = (e) => { + worker.postMessage({ type: 'onmessageerror', payload: { id, data: e.data } }) + } + w.onerror = (e) => { + worker.postMessage({ + type: 'onerror', + payload: { + id, + data: { + message: e.message, + filename: e.filename, + lineno: e.lineno, + colno: e.colno, + error: e.error + } + } + }) + } + } else if (type === 'postMessage') { + const { id, args } = payload + const w = map.get(id) + w.postMessage.apply(w, args) + } else if (type === 'terminate') { + const { id } = payload + map.get(id).terminate() + map.delete(id) + } } -})(globalThis) +} diff --git a/packages/wasi-threads/test/run.js b/packages/wasi-threads/test/run.js new file mode 100644 index 00000000..db5bf959 --- /dev/null +++ b/packages/wasi-threads/test/run.js @@ -0,0 +1,105 @@ +export async function main (WASI, WASIThreads, Worker, process, workerSource) { + async function run (file) { + const wasi = new WASI({ + version: 'preview1', + args: [file, 'node'], + env: process.env + }) + const wasiThreads = new WASIThreads({ + wasi, + onCreateWorker: ({ name }) => { + return new Worker(new URL(workerSource, import.meta.url), { + type: 'module', + name, + workerData: { + name + }, + env: process.env, + execArgv: ['--experimental-wasi-unstable-preview1'] + }) + }, + // optional + waitThreadStart: 1000 + }) + const memory = new WebAssembly.Memory({ + initial: 16777216 / 65536, + maximum: 2147483648 / 65536, + shared: true + }) + let input + try { + const { readFileSync } = await import('fs') + input = readFileSync(new URL(file, import.meta.url)) + } catch (err) { + console.warn(err) + const response = await fetch(file) + input = await response.arrayBuffer() + } + let { module, instance } = await WebAssembly.instantiate(input, { + env: { + memory, + print_string: function (ptr) { + const HEAPU8 = new Uint8Array(memory.buffer) + let len = 0 + while (HEAPU8[ptr + len] !== 0) len++ + const string = new TextDecoder().decode(HEAPU8.slice(ptr, ptr + len)) + console.log(string) + } + }, + ...wasi.getImportObject(), + ...wasiThreads.getImportObject() + }) + + if (typeof instance.exports._start === 'function') { + const { exitCode } = wasiThreads.start(instance, module, memory) + return exitCode + } else { + instance = wasiThreads.initialize(instance, module, memory) + return instance.exports.fn(1) + } + } + + console.log('-------- command --------') + await run('main.wasm') + console.log('-------- reactor --------') + await run('lib.wasm') +} + +export function child (WASI, ThreadMessageHandler, WASIThreads) { + const handler = new ThreadMessageHandler({ + async onLoad ({ wasmModule, wasmMemory }) { + const wasi = new WASI({ + version: 'preview1' + }) + + const wasiThreads = new WASIThreads({ + wasi, + childThread: true + }) + + const originalInstance = await WebAssembly.instantiate(wasmModule, { + env: { + memory: wasmMemory, + print_string: function (ptr) { + const HEAPU8 = new Uint8Array(wasmMemory.buffer) + let len = 0 + while (HEAPU8[ptr + len] !== 0) len++ + const string = new TextDecoder().decode(HEAPU8.slice(ptr, ptr + len)) + console.log(string) + } + }, + ...wasi.getImportObject(), + ...wasiThreads.getImportObject() + }) + + const instance = wasiThreads.initialize(originalInstance, wasmModule, wasmMemory) + + return { module: wasmModule, instance } + } + }) + + globalThis.onmessage = function (e) { + handler.handle(e) + // handle other messages + } +} diff --git a/packages/wasi-threads/test/worker.js b/packages/wasi-threads/test/worker.js index f69e7f42..f6d6c6b1 100644 --- a/packages/wasi-threads/test/worker.js +++ b/packages/wasi-threads/test/worker.js @@ -1,103 +1,16 @@ -/* eslint-disable no-eval */ +import { WASI } from 'wasi' +import { ThreadMessageHandler, WASIThreads } from '@emnapi/wasi-threads' +import { child } from './run.js' +import { workerData, parentPort } from 'worker_threads' -(function (main) { - const ENVIRONMENT_IS_NODE = - typeof process === 'object' && process !== null && - typeof process.versions === 'object' && process.versions !== null && - typeof process.versions.node === 'string' - - if (ENVIRONMENT_IS_NODE) { - const _require = function (request) { - if (request === '@emnapi/wasi-threads') return require('..') - return require(request) - } - - const _init = function () { - const nodeWorkerThreads = require('node:worker_threads') - const parentPort = nodeWorkerThreads.parentPort - - parentPort.on('message', (data) => { - globalThis.onmessage({ data }) - }) - - Object.assign(globalThis, { - self: globalThis, - require, - Worker: nodeWorkerThreads.Worker, - importScripts: function (f) { - (0, eval)(require('node:fs').readFileSync(f, 'utf8') + '//# sourceURL=' + f) - }, - postMessage: function (msg) { - parentPort.postMessage(msg) - } - }) - } - - main(_require, _init) - } else { - // eslint-disable-next-line no-undef - importScripts('../../../node_modules/@tybys/wasm-util/dist/wasm-util.min.js') - // eslint-disable-next-line no-undef - importScripts('../dist/wasi-threads.js') - - const nodeWasi = { WASI: globalThis.wasmUtil.WASI } - const nodeWorkerThreads = { - workerData: { - name: globalThis.name - } - } - const _require = function (request) { - if (request === '@emnapi/wasi-threads') return globalThis.wasiThreads - if (request === 'node:worker_threads' || request === 'worker_threads') return nodeWorkerThreads - if (request === 'node:wasi' || request === 'wasi') return nodeWasi - throw new Error('Can not find module: ' + request) - } - const _init = function () {} - main(_require, _init) - } -})(function main (require, init) { - init() - - const { WASI } = require('node:wasi') - const { workerData } = require('node:worker_threads') - const { ThreadMessageHandler, WASIThreads } = require('@emnapi/wasi-threads') - - console.log(`name: ${workerData.name}`) - - const handler = new ThreadMessageHandler({ - async onLoad ({ wasmModule, wasmMemory }) { - const wasi = new WASI({ - version: 'preview1' - }) - - const wasiThreads = new WASIThreads({ - wasi, - childThread: true - }) - - const originalInstance = await WebAssembly.instantiate(wasmModule, { - env: { - memory: wasmMemory, - print_string: function (ptr) { - const HEAPU8 = new Uint8Array(wasmMemory.buffer) - let len = 0 - while (HEAPU8[ptr + len] !== 0) len++ - const string = new TextDecoder().decode(HEAPU8.slice(ptr, ptr + len)) - console.log(string) - } - }, - ...wasi.getImportObject(), - ...wasiThreads.getImportObject() - }) +parentPort.on('message', (data) => { + globalThis.onmessage({ data }) +}) - const instance = wasiThreads.initialize(originalInstance, wasmModule, wasmMemory) +globalThis.postMessage = function (...args) { + parentPort.postMessage(...args) +} - return { module: wasmModule, instance } - } - }) +console.log(`name: ${workerData.name}`) - globalThis.onmessage = function (e) { - handler.handle(e) - // handle other messages - } -}) +child(WASI, ThreadMessageHandler, WASIThreads) diff --git a/packages/wasi-threads/tsconfig.json b/packages/wasi-threads/tsconfig.json index 5fbf9ca4..0fa6259a 100644 --- a/packages/wasi-threads/tsconfig.json +++ b/packages/wasi-threads/tsconfig.json @@ -9,7 +9,7 @@ "importHelpers": true, "paths": { "tslib" : ["../../node_modules/tslib/tslib.d.ts"], - "@/*": ["./src/*"], + "@/*": ["./src/*"] } }, "include": [ diff --git a/script/release.js b/script/release.js index 2622fa17..dc716c15 100644 --- a/script/release.js +++ b/script/release.js @@ -1,8 +1,10 @@ -const fs = require('fs-extra') -const path = require('path') -const crossZip = require('@tybys/cross-zip') -const { which } = require('./which.js') -const { spawn, spawnSync, ChildProcessError } = require('./spawn.js') +import fs from 'fs-extra' +import path from 'path' +import crossZip from '@tybys/cross-zip' +import { which } from './which.js' +import { spawn, spawnSync, ChildProcessError } from './spawn.js' + +const __dirname = import.meta.dirname async function main () { await Promise.resolve() @@ -41,7 +43,7 @@ async function main () { let runtimeNapiVersion try { - runtimeNapiVersion = require('@emnapi/runtime').NAPI_VERSION_EXPERIMENTAL + runtimeNapiVersion = (await import('@emnapi/runtime')).NAPI_VERSION_EXPERIMENTAL } catch (_) { runtimeNapiVersion = 0x7fffffff } diff --git a/script/spawn.js b/script/spawn.js index 6140f3a9..003e6309 100644 --- a/script/spawn.js +++ b/script/spawn.js @@ -1,4 +1,6 @@ -class ChildProcessError extends Error { +import childProcess from 'child_process' + +export class ChildProcessError extends Error { constructor (message, code) { super(message) this.code = code @@ -13,11 +15,11 @@ class ChildProcessError extends Error { * @param {'inherit' | 'pipe' | 'ignore'=} stdio * @returns {Promise & { cp: import('child_process').ChildProcess }} */ -function spawn (command, args, cwdPath, stdio, env) { +export function spawn (command, args, cwdPath, stdio, env) { const argsString = args.map(a => a.indexOf(' ') !== -1 ? ('"' + a + '"') : a).join(' ') const cwd = cwdPath || process.cwd() console.log(`[spawn] ${cwd}${process.platform === 'win32' ? '>' : '$'} ${command} ${argsString}`) - const cp = require('child_process').spawn(command, args, { + const cp = childProcess.spawn(command, args, { env: env || process.env, cwd, stdio: stdio || 'inherit', @@ -39,11 +41,11 @@ function spawn (command, args, cwdPath, stdio, env) { return p } -function spawnSync (command, args, cwdPath) { +export function spawnSync (command, args, cwdPath) { const argsString = args.map(a => a.indexOf(' ') !== -1 ? ('"' + a + '"') : a).join(' ') const cwd = cwdPath || process.cwd() console.log(`[spawn] ${cwd}${process.platform === 'win32' ? '>' : '$'} ${command} ${argsString}`) - const result = require('child_process').spawnSync(command, args, { + const result = childProcess.spawnSync(command, args, { env: process.env, cwd }) @@ -55,7 +57,3 @@ function spawnSync (command, args, cwdPath) { } return result } - -exports.spawn = spawn -exports.spawnSync = spawnSync -exports.ChildProcessError = ChildProcessError diff --git a/script/which.js b/script/which.js index 333fee91..ae6aeaca 100644 --- a/script/which.js +++ b/script/which.js @@ -1,20 +1,19 @@ #!/usr/bin/env node -'use strict' +import { pathToFileURL } from 'url' +import path from 'path' +import fs from 'fs' /** * @param {string} cmd * @param {NodeJS.ProcessEnv} env * @returns {string} */ -function which (cmd, env = process.env) { +export function which (cmd, env = process.env) { const PATH = process.platform === 'win32' ? env.Path : env.PATH const pathList = PATH ? (process.platform === 'win32' ? PATH.split(';') : PATH.split(':')) : undefined if (pathList === undefined || pathList.length === 0) return '' - const path = require('path') - const fs = require('fs') - const PATHEXT = process.platform === 'win32' ? (env.PATHEXT ? env.PATHEXT : '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.RB;.RBW') : '' const extlist = process.platform === 'win32' ? PATHEXT.split(';').map(v => v.toLowerCase()) : ['', '.sh'] for (let i = 0; i < pathList.length; i++) { @@ -49,10 +48,7 @@ function which (cmd, env = process.env) { return '' } -exports.which = which -Object.defineProperty(exports, '__esModule', { value: true }) - -if (require.main === module) { +if (import.meta.url === pathToFileURL(process.argv[1]).href) { const p = which(process.argv[2]) if (p) { console.log(p)