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..354e88f0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,5 +17,9 @@ "launch": { "statusBarVisibility": "hidden" } + }, + "livePreview.httpHeaders": { + "Cross-Origin-Embedder-Policy": "require-corp", + "Cross-Origin-Opener-Policy": "same-origin" } } 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/shared/package.json b/packages/shared/package.json new file mode 100644 index 00000000..a668518a --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,24 @@ +{ + "name": "@emnapi/shared", + "version": "1.3.1", + "private": true, + "description": "", + "type": "module", + "scripts": { + "build": "tsc -p ./tsconfig.json" + }, + "main": "./out/index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/toyobayashi/emnapi.git" + }, + "author": "toyobayashi", + "license": "MIT", + "bugs": { + "url": "https://github.com/toyobayashi/emnapi/issues" + }, + "homepage": "https://github.com/toyobayashi/emnapi/tree/main/packages/shared", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/shared/src/build.ts b/packages/shared/src/build.ts new file mode 100644 index 00000000..a2fbec52 --- /dev/null +++ b/packages/shared/src/build.ts @@ -0,0 +1,91 @@ +#!/usr/bin/env node + +import path from 'path' +import fs from 'fs' +import ts from 'typescript' +import type { RollupOptions, RollupBuild, OutputOptions } from 'rollup' +import { loadConfigFile } from 'rollup/loadConfigFile' +import { pathToFileURL } from 'url' + +export async function build (workspace: string): Promise { + const { compile } = await import('@tybys/tsapi') + const { rollup } = await import('rollup') + + compile(path.resolve(workspace, 'tsconfig.json'), { + optionsToExtend: { + target: ts.ScriptTarget.ES2021, + outDir: path.resolve(workspace, 'lib'), + emitDeclarationOnly: true, + declaration: true, + declarationMap: true, + declarationDir: path.resolve(workspace, 'lib/typings') + } + }) + + const { + Extractor, + ExtractorConfig + } = await import('@microsoft/api-extractor') + const apiExtractorJsonPath = path.resolve(workspace, '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` + throw new Error(errmsg) + } + + const dts = extractorConfig.publicTrimmedFilePath + + const { options } = await loadConfigFile(path.resolve(workspace, 'rollup.config.js'), {}) + + const runRollup = async (conf: RollupOptions): Promise => { + const bundle = await rollup(conf) + + const writeBundle = async (bundle: RollupBuild, output: OutputOptions): Promise => { + const writeOutput = await bundle.write(output ?? {}) + + for (const file of writeOutput.output) { + if (file.type === 'asset') { + console.log(`[Asset] ${output.file ?? file.fileName}`) + } else if (file.type === 'chunk') { + console.log(`[Chunk] ${output.file ?? file.fileName}`) + } + } + + if (output?.file) { + if (output.file.endsWith('.umd.cjs')) { + const umdDts = output.file.replace(/\.cjs$/, '.d.cts') + fs.copyFileSync(dts, umdDts) + if (output.name) { + fs.appendFileSync(umdDts, `\nexport as namespace ${output.name};\n`, 'utf8') + } + } else if (output.file.endsWith('.umd.min.cjs')) { + // ignore + } else if (output.file.endsWith('.cjs')) { + const cjsDts = output.file.replace(/\.cjs$/, '.d.cts') + fs.copyFileSync(dts, cjsDts) + } + } + } + + if (Array.isArray(conf.output)) { + await Promise.all(conf.output.map(o => writeBundle(bundle, o ?? {}))) + } else { + await writeBundle(bundle, conf.output ?? {}) + } + } + + await Promise.all(options.map(runRollup)) +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) { + build(process.argv[2] ?? process.cwd()).catch(err => { + console.error(err) + process.exit(1) + }) +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 00000000..22d85a12 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,2 @@ +export * from './rollup.ts' +export * from './build.ts' diff --git a/packages/shared/src/rollup.ts b/packages/shared/src/rollup.ts new file mode 100644 index 00000000..ae9de58b --- /dev/null +++ b/packages/shared/src/rollup.ts @@ -0,0 +1,159 @@ +import type { InputPluginOption, InputOptions, RollupOptions, NullValue, ExternalOption } from 'rollup' +import ts from 'typescript' + +import { createRequire } from 'module' +const require = createRequire(import.meta.url) + +const rollupTypescript = require('@rollup/plugin-typescript').default as typeof import('@rollup/plugin-typescript').default +const rollupNodeResolve = require('@rollup/plugin-node-resolve').default as typeof import('@rollup/plugin-node-resolve').default +const rollupReplace = require('@rollup/plugin-replace').default as typeof import('@rollup/plugin-replace').default +const transformPureClass = require('@tybys/ts-transform-pure-class').default as typeof import('@tybys/ts-transform-pure-class').default +const rollupTerser = require('@rollup/plugin-terser').default + +export type Format = 'esm' | 'cjs' | 'umd' | 'esm-browser' | 'iife' + +export interface MakeConfigOptions extends Omit { + outputName: string + outputFile: string + format: Format + minify?: boolean + compilerOptions?: ts.CompilerOptions + defines?: Record + external?: ExternalOption | ((source: string, importer: string | undefined, isResolved: boolean, format: Format) => boolean | NullValue) +} + +export interface Options extends Omit { + browser?: boolean +} + +export function makeConfig (options: MakeConfigOptions): RollupOptions { + const { + input, + outputName, + outputFile, + compilerOptions, + plugins, + minify, + format, + defines, + external, + ...restInput + } = options ?? {} + const target = compilerOptions?.target ?? ts.ScriptTarget.ES2021 + + const defaultPlugins: InputPluginOption[] = [ + rollupTypescript({ + compilerOptions: { + ...(target !== ts.ScriptTarget.ES5 ? { removeComments: true, downlevelIteration: false } : {}), + ...options?.compilerOptions + }, + transformers: { + after: [ + transformPureClass + ] + } + }), + rollupNodeResolve({ + mainFields: ['module', 'module-sync', 'import', 'main'] + }), + rollupReplace({ + preventAssignment: true, + values: { + __FORMAT__: JSON.stringify(format), + __DEV__: format === 'umd' || format === 'esm-browser' ? (minify ? 'false' : 'true') : '(process.env.NODE_ENV !== "production")', + ...(format === 'umd' || format === 'esm-browser' ? { 'process.env.NODE_ENV': minify ? '"production"' : '"development"' } : {}), + ...defines + } + }), + ...(minify + ? [ + rollupTerser.default({ + compress: true, + mangle: true, + format: { + comments: false + } + }) + ] + : []) + ] + + const outputDir = 'dist' + const outputs = { + esm: { + file: `${outputDir}/${outputFile}${minify ? '.min' : ''}.js`, + format: 'esm', + exports: 'named' + } satisfies RollupOptions['output'], + 'esm-browser': { + file: `${outputDir}/${outputFile}.browser${minify ? '.min' : ''}.js`, + format: 'esm', + exports: 'named' + } satisfies RollupOptions['output'], + cjs: { + file: `${outputDir}/${outputFile}${minify ? '.min' : ''}.cjs`, + format: 'cjs', + exports: 'named' + } satisfies RollupOptions['output'], + umd: { + file: `${outputDir}/${outputFile}.umd${minify ? '.min' : ''}.cjs`, + format: 'umd', + name: outputName, + exports: 'named' + } satisfies RollupOptions['output'], + iife: { + file: `${outputDir}/${outputFile}.iife${minify ? '.min' : ''}.cjs`, + format: 'iife', + name: outputName, + exports: 'named' + } satisfies RollupOptions['output'] + } + + const defaultExternals: Record = { + esm: ['tslib'], + 'esm-browser': [], + cjs: ['tslib'], + umd: [], + iife: [] + } + + return { + input: input ?? 'src/index.ts', + external: typeof external === 'function' + ? function (this: any, id: string, parent, isResolved) { + const result = external.call(this, id, parent, isResolved, format) + if (result) return result + return defaultExternals[format]?.includes(id) + } + : Array.isArray(external) + ? [...new Set([...(defaultExternals[format] ?? []), ...external])] + : defaultExternals[format], + plugins: plugins instanceof Promise + ? plugins.then(p => { + return [...defaultPlugins, ...(Array.isArray(p) ? p : [p])] + }) + : [...defaultPlugins, ...(Array.isArray(plugins) ? plugins : [plugins])], + ...restInput, + output: outputs[format] + } +} + +export function defineConfig (options: Options): RollupOptions[] { + const { browser = true } = options + return ([ + ['esm', false], + ['cjs', false], + ...(browser + ? [ + ['umd', false], + ['umd', true], + ['esm-browser', false], + ['esm-browser', true] + ] as const + : []) + ] as const).map(([format, minify]) => makeConfig({ + ...options, + format, + minify + })) +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 00000000..5834c0cc --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "target": "ES2021", + "module": "NodeNext", + "moduleResolution": "nodenext", + "strict": true, + "declaration": true, + "sourceMap": true, + "declarationMap": true, + "outDir": "out", + "rewriteRelativeImportExtensions": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/packages/test/package.json b/packages/test/package.json index faab0b2c..638414ff 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -14,6 +14,7 @@ "node-addon-api": "8.1.0", "why-is-node-running": "^2.3.0" }, + "type": "commonjs", "scripts": { "rebuild": "cross-env UV_THREADPOOL_SIZE=2 node ./script/build-emscripten.js Debug", "rebuild:r": "cross-env UV_THREADPOOL_SIZE=2 node ./script/build-emscripten.js Release", diff --git a/packages/wasi-threads/package.json b/packages/wasi-threads/package.json index 0a11daa4..4cfd4196 100644 --- a/packages/wasi-threads/package.json +++ b/packages/wasi-threads/package.json @@ -2,35 +2,31 @@ "name": "@emnapi/wasi-threads", "version": "1.0.1", "description": "WASI threads proposal implementation in JavaScript", - "main": "index.js", - "module": "./dist/wasi-threads.esm-bundler.js", + "type": "module", + "main": "./dist/wasi-threads.cjs", + "module": "./dist/wasi-threads.js", "types": "./dist/wasi-threads.d.ts", "sideEffects": false, "exports": { ".": { "types": { "module": "./dist/wasi-threads.d.ts", - "import": "./dist/wasi-threads.d.mts", - "default": "./dist/wasi-threads.d.ts" + "module-sync": "./dist/wasi-threads.d.ts", + "import": "./dist/wasi-threads.d.cts", + "require": "./dist/wasi-threads.d.cts", + "default": "./dist/wasi-threads.d.cts" }, - "module": "./dist/wasi-threads.esm-bundler.js", - "import": "./dist/wasi-threads.mjs", - "default": "./index.js" + "module": "./dist/wasi-threads.js", + "module-sync": "./dist/wasi-threads.js", + "default": "./dist/wasi-threads.cjs" }, - "./dist/wasi-threads.cjs.min": { - "types": "./dist/wasi-threads.d.ts", - "default": "./dist/wasi-threads.cjs.min.js" - }, - "./dist/wasi-threads.min.mjs": { - "types": "./dist/wasi-threads.d.mts", - "default": "./dist/wasi-threads.min.mjs" - } + "./package.json": "./package.json" }, "dependencies": { "tslib": "^2.4.0" }, "scripts": { - "build": "node ./script/build.js", + "build": "node ../shared/out/build.js .", "build:test": "node ./test/build.js", "test": "node ./test/index.js" }, diff --git a/packages/wasi-threads/rollup.config.js b/packages/wasi-threads/rollup.config.js new file mode 100644 index 00000000..a9fed3ce --- /dev/null +++ b/packages/wasi-threads/rollup.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from '@emnapi/shared' + +export default defineConfig({ + outputName: 'wasiThreads', + outputFile: 'wasi-threads' +}) diff --git a/packages/wasi-threads/script/build.js b/packages/wasi-threads/script/build.js deleted file mode 100644 index 5556f7d2..00000000 --- a/packages/wasi-threads/script/build.js +++ /dev/null @@ -1,195 +0,0 @@ -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 dist = path.join(__dirname, '../dist') - -function build () { - compile(path.join(__dirname, '../tsconfig.json'), { - optionsToExtend: { - target: require('typescript').ScriptTarget.ES2021, - outDir: path.join(__dirname, '../lib'), - emitDeclarationOnly: true, - declaration: true, - declarationMap: true, - declarationDir: path.join(__dirname, '../lib/typings') - } - }) - - /** - * @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 - } - }) - ] - : []) - ] - } - } - - const globalName = 'wasiThreads' - - return Promise.all(([ - { - input: createInput(ts.ScriptTarget.ES2021, false), - output: { - file: path.join(dist, 'wasi-threads.js'), - format: 'umd', - name: globalName, - exports: 'named', - strict: false - } - }, - { - input: createInput(ts.ScriptTarget.ES2021, true), - output: { - file: path.join(dist, 'wasi-threads.min.js'), - format: 'umd', - name: globalName, - exports: 'named', - strict: false - } - }, - { - input: createInput(ts.ScriptTarget.ES2021, false, ['tslib']), - output: { - file: path.join(dist, 'wasi-threads.cjs.js'), - format: 'cjs', - name: globalName, - exports: 'named', - strict: false - } - }, - { - input: createInput(ts.ScriptTarget.ES2021, true, ['tslib']), - output: { - file: path.join(dist, 'wasi-threads.cjs.min.js'), - format: 'cjs', - name: globalName, - exports: 'named', - strict: false - } - }, - { - input: createInput(ts.ScriptTarget.ES2021, false, ['tslib']), - output: { - file: path.join(dist, 'wasi-threads.mjs'), - format: 'esm', - name: globalName, - exports: 'named', - strict: false - } - }, - { - input: createInput(ts.ScriptTarget.ES2021, true, ['tslib']), - output: { - file: path.join(dist, 'wasi-threads.min.mjs'), - format: 'esm', - name: globalName, - exports: 'named', - strict: false - } - }, - { - input: createInput(ts.ScriptTarget.ES2021, false, ['tslib']), - output: { - file: path.join(dist, 'wasi-threads.esm-bundler.js'), - format: 'esm', - name: globalName, - 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/wasi-threads.d.mts') - const cjsMinDts = path.join(__dirname, '../dist/wasi-threads.cjs.min.d.ts') - const mjsMinDts = path.join(__dirname, '../dist/wasi-threads.min.d.mts') - fs.copyFileSync(dts, mDts) - fs.copyFileSync(dts, cjsMinDts) - fs.copyFileSync(dts, mjsMinDts) - fs.appendFileSync(dts, `\nexport as namespace ${globalName};\n`, 'utf8') - }) -} - -exports.build = build - -if (module === require.main) { - build().catch(err => { - console.error(err) - process.exit(1) - }) -} diff --git a/packages/wasi-threads/test/browser-worker.js b/packages/wasi-threads/test/browser-worker.js new file mode 100644 index 00000000..28eb354e --- /dev/null +++ b/packages/wasi-threads/test/browser-worker.js @@ -0,0 +1,6 @@ +import { WASI } from '../../../node_modules/@tybys/wasm-util/dist/wasm-util.esm.js' +import { ThreadMessageHandler, WASIThreads } from '../dist/wasi-threads.js' +import { child } from './run.js' + +console.log(`name: ${globalThis.name}`) +child(WASI, ThreadMessageHandler, WASIThreads) diff --git a/packages/wasi-threads/test/browser.js b/packages/wasi-threads/test/browser.js new file mode 100644 index 00000000..8306ce41 --- /dev/null +++ b/packages/wasi-threads/test/browser.js @@ -0,0 +1,6 @@ +import { WASI } from '../../../node_modules/@tybys/wasm-util/dist/wasm-util.esm.js' +import { Worker } from './proxy.js' +import { WASIThreads } from '../dist/wasi-threads.js' +import { main } from './run.js' + +main(WASI, WASIThreads, Worker, { env: {} }, './browser-worker.js') diff --git a/packages/wasi-threads/test/build.js b/packages/wasi-threads/test/build.js index 9623e2b1..b8131825 100644 --- a/packages/wasi-threads/test/build.js +++ b/packages/wasi-threads/test/build.js @@ -1,5 +1,5 @@ -const { join, resolve } = require('node:path') -const { spawnSync } = require('node:child_process') +import { join, resolve } from 'node:path' +import { spawnSync } from 'node:child_process' const ExecutionModel = { Command: 'command', @@ -9,7 +9,7 @@ const ExecutionModel = { function build (model) { const bin = resolve(process.env.WASI_SDK_PATH, 'bin', 'clang') + (process.platform === 'win32' ? '.exe' : '') const args = [ - '-o', join(__dirname, model === ExecutionModel.Command ? 'main.wasm' : 'lib.wasm'), + '-o', join(import.meta.dirname, model === ExecutionModel.Command ? 'main.wasm' : 'lib.wasm'), '-mbulk-memory', '-matomics', `-mexec-model=${model}`, @@ -31,7 +31,7 @@ function build (model) { '-Wl,--export-dynamic', '-Wl,--max-memory=2147483648', '-Wl,--export=malloc,--export=free', - join(__dirname, 'main.c') + join(import.meta.dirname, 'main.c') ] const quote = s => s.includes(' ') ? `"${s}"` : s console.log(`> ${quote(bin)} ${args.map(quote).join(' ')}`) diff --git a/packages/wasi-threads/test/index.html b/packages/wasi-threads/test/index.html index 0a0fda57..946ceacb 100644 --- a/packages/wasi-threads/test/index.html +++ b/packages/wasi-threads/test/index.html @@ -6,12 +6,11 @@ test - - - - 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/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)