Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): support generate browser compatible codes #1891

Merged
merged 1 commit into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ triples/index.js
rollup.config.js
crates/cli/index.js
examples/napi/index.wasi.mjs
examples/napi/index.wasi-browser.js
examples/napi/wasi-worker.mjs
examples/napi/wasi-worker-browser.js
9 changes: 5 additions & 4 deletions .github/workflows/test-release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -479,16 +479,15 @@ jobs:
run: cargo check -p ${{ matrix.settings.package }} -F ${{ matrix.settings.features }}

test-node-wasi:
runs-on: ubuntu-latest
runs-on: macos-latest
name: Test node wasi target
timeout-minutes: 10
continue-on-error: true
steps:
- uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
cache: 'yarn'
- name: Install
uses: dtolnay/rust-toolchain@stable
Expand All @@ -502,7 +501,7 @@ jobs:
~/.cargo/registry
~/.cargo/git
target
key: stable-wasm32-wasi-preview1-threads-node@18-cargo-cache
key: stable-wasm32-wasi-preview1-threads-node@20-cargo-cache
- name: Install dependencies
run: yarn install --immutable --mode=skip-build
- name: Build
Expand All @@ -514,6 +513,8 @@ jobs:
env:
WASI_TEST: 'true'
NODE_OPTIONS: '--max-old-space-size=8192'
- name: Browser test
run: yarn workspace @examples/napi vitest

test-latest-bun:
runs-on: ubuntu-latest
Expand Down
28 changes: 27 additions & 1 deletion cli/src/api/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,16 @@ export async function collectArtifacts(userOptions: ArtifactsOptions) {
options.buildOutputDir ?? options.cwd,
`wasi-worker.mjs`,
)
const browserEntry = join(
options.buildOutputDir ?? options.cwd,
`${binaryName}.wasi-browser.js`,
)
const browserWorkerFile = join(
options.buildOutputDir ?? options.cwd,
`wasi-worker-browser.js`,
)
debug.info(
`Move wasi cjs file [${colors.yellowBright(
`Move wasi binding file [${colors.yellowBright(
cjsFile,
)}] to [${colors.yellowBright(wasiDir)}]`,
)
Expand All @@ -114,6 +122,24 @@ export async function collectArtifacts(userOptions: ArtifactsOptions) {
join(wasiDir, `wasi-worker.mjs`),
await readFileAsync(workerFile),
)
debug.info(
`Move wasi browser entry file [${colors.yellowBright(
browserEntry,
)}] to [${colors.yellowBright(wasiDir)}]`,
)
await writeFileAsync(
join(wasiDir, `${binaryName}.wasi-browser.js`),
await readFileAsync(browserEntry),
)
debug.info(
`Move wasi browser worker file [${colors.yellowBright(
browserWorkerFile,
)}] to [${colors.yellowBright(wasiDir)}]`,
)
await writeFileAsync(
join(wasiDir, `wasi-worker-browser.js`),
await readFileAsync(browserWorkerFile),
)
}
}

Expand Down
68 changes: 55 additions & 13 deletions cli/src/api/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,14 @@ import {
} from '../utils/index.js'

import { createCjsBinding } from './templates/index.js'
import { createWasiBinding } from './templates/load-wasi-template.js'
import { WASI_WORKER_TEMPLATE } from './templates/wasi-worker-template.js'
import {
createWasiBinding,
createWasiBrowserBinding,
} from './templates/load-wasi-template.js'
import {
WASI_WORKER_BROWSER_TEMPLATE,
WASI_WORKER_TEMPLATE,
} from './templates/wasi-worker-template.js'

const debug = debugFactory('build')
const require = createRequire(import.meta.url)
Expand Down Expand Up @@ -635,16 +641,16 @@ class Builder {
})()
: []
const jsOutput = await this.writeJsBinding(idents)
const wasmOutput = await this.writeWasiBinding(
const wasmBindingsOutput = await this.writeWasiBinding(
wasiRegisterFunctions,
dest ?? 'index.wasm',
idents,
)
if (jsOutput) {
this.outputs.push(jsOutput)
}
if (wasmOutput) {
this.outputs.push(wasmOutput)
if (wasmBindingsOutput) {
this.outputs.push(...wasmBindingsOutput)
}
}

Expand Down Expand Up @@ -790,15 +796,20 @@ class Builder {
) {
if (distFileName && wasiRegisterFunctions.length) {
const { name, dir } = parse(distFileName)
const newPath = join(dir, `${this.config.binaryName}.wasi.cjs`)
const bindingPath = join(dir, `${this.config.binaryName}.wasi.cjs`)
const browserBindingPath = join(
dir,
`${this.config.binaryName}.wasi-browser.js`,
)
const workerPath = join(dir, 'wasi-worker.mjs')
const browserWorkerPath = join(dir, 'wasi-worker-browser.mjs')
const exportsCode = idents
.map(
(ident) => `module.exports.${ident} = __napiModule.exports.${ident}`,
)
.join(',\n')
.join('\n')
await writeFileAsync(
newPath,
bindingPath,
createWasiBinding(
name,
this.config.packageName,
Expand All @@ -808,12 +819,43 @@ class Builder {
'\n',
'utf8',
)
await writeFileAsync(
browserBindingPath,
createWasiBrowserBinding(name, wasiRegisterFunctions) +
idents
.map(
(ident) =>
`export const ${ident} = __napiModule.exports.${ident}`,
)
.join('\n') +
'\n',
'utf8',
)
await writeFileAsync(workerPath, WASI_WORKER_TEMPLATE, 'utf8')
return {
kind: 'wasm',
path: newPath,
} satisfies Output
await writeFileAsync(
browserWorkerPath,
WASI_WORKER_BROWSER_TEMPLATE,
'utf8',
)
return [
{
kind: 'js',
path: bindingPath,
},
{
kind: 'js',
path: browserBindingPath,
},
{
kind: 'js',
path: workerPath,
},
{
kind: 'js',
path: browserWorkerPath,
},
] satisfies Output[]
}
return null
return []
}
}
12 changes: 11 additions & 1 deletion cli/src/api/create-npm-dirs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ export async function createNpmDirs(userOptions: CreateNpmDirsOptions) {
const entry = `${binaryName}.wasi.cjs`
scopedPackageJson.files.push(entry, `wasi-worker.mjs`)
scopedPackageJson.main = entry
// @ts-expect-error
scopedPackageJson.browser = `${binaryName}.wasi-browser.js`
let needRestrictNodeVersion = true
if (scopedPackageJson.engines?.node) {
try {
Expand All @@ -114,10 +116,18 @@ export async function createNpmDirs(userOptions: CreateNpmDirsOptions) {
const emnapiRuntime = await fetch(
`https://registry.npmjs.org/@emnapi/runtime`,
).then((res) => res.json() as Promise<PackageMeta>)
const wasiUtil = await fetch(
`https://registry.npmjs.org/@tybys/wasm-util`,
).then((res) => res.json() as Promise<PackageMeta>)
const memfsBrowser = await fetch(
`https://registry.npmjs.org/memfs-browser`,
).then((res) => res.json() as Promise<PackageMeta>)
// @ts-expect-error
scopedPackageJson.dependencies = {
'@emnapi/core': `^${emnapiCore['dist-tags'].latest}`,
'@emnapi/runtime': `^${emnapiRuntime['dist-tags'].latest}`,
'@tybys/wasm-util': `^${wasiUtil['dist-tags'].latest}`,
'memfs-browser': `^${memfsBrowser['dist-tags'].latest}`,
}
}

Expand All @@ -137,7 +147,7 @@ export async function createNpmDirs(userOptions: CreateNpmDirsOptions) {
const targetReadme = join(targetDir, 'README.md')
await writeFileAsync(targetReadme, readme(packageName, target))

debug.info(`${packageName}-${target.platformArchABI} created`)
debug.info(`${packageName} -${target.platformArchABI} created`)
}
}

Expand Down
2 changes: 1 addition & 1 deletion cli/src/api/templates/ci-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ jobs:
with:
operating_system: freebsd
version: '13.2'
memory: 13G
memory: 8G
cpu_count: 3
environment_variables: 'DEBUG RUSTUP_IO_THREADS'
shell: bash
Expand Down
65 changes: 65 additions & 0 deletions cli/src/api/templates/load-wasi-template.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,68 @@
export const createWasiBrowserBinding = (
wasiFilename: string,
wasiRegisterFunctions: string[],
) => `import { instantiateNapiModuleSync as __emnapiInstantiateNapiModuleSync } from '@emnapi/core'
import { getDefaultContext as __emnapiGetDefaultContext } from '@emnapi/runtime'
import { WASI as __WASI } from '@tybys/wasm-util'
import { Volume as __Volume, createFsFromVolume as __createFsFromVolume } from 'memfs-browser'

import __wasmUrl from './${wasiFilename}.wasm?url'

const __fs = __createFsFromVolume(
__Volume.fromJSON({
'/': null,
}),
)

const __wasi = new __WASI({
version: 'preview1',
fs: __fs,
})

const __emnapiContext = __emnapiGetDefaultContext()

const __sharedMemory = new WebAssembly.Memory({
initial: 1024,
maximum: 10240,
shared: true,
})

let __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer())

const {
instance: __napiInstance,
module: __wasiModule,
napiModule: __napiModule,
} = __emnapiInstantiateNapiModuleSync(__wasmFile, {
context: __emnapiContext,
asyncWorkPoolSize: 4,
wasi: __wasi,
onCreateWorker() {
return new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {
type: 'module',
})
},
overwriteImports(importObject) {
importObject.env = {
...importObject.env,
...importObject.napi,
...importObject.emnapi,
memory: __sharedMemory,
}
return importObject
},
beforeInit({ instance }) {
__napi_rs_initialize_modules(instance)
},
})

function __napi_rs_initialize_modules(__napiInstance) {
${wasiRegisterFunctions
.map((name) => ` __napiInstance.exports['${name}']?.()`)
.join('\n')}
}
`

export const createWasiBinding = (
wasmFileName: string,
packageName: string,
Expand Down
34 changes: 34 additions & 0 deletions cli/src/api/templates/wasi-worker-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,37 @@ globalThis.onmessage = function (e) {
handler.handle(e);
};
`

export const WASI_WORKER_BROWSER_TEMPLATE = `import { instantiateNapiModuleSync, MessageHandler } from '@emnapi/core'
import { WASI } from '@tybys/wasm-util'
import { Volume, createFsFromVolume } from 'memfs-browser'

const fs = createFsFromVolume(Volume.fromJSON({
'/': null
}))

const handler = new MessageHandler({
onLoad({ wasmModule, wasmMemory }) {
const wasi = new WASI({
fs,
print: function() { console.log.apply(console, arguments) }
})
return instantiateNapiModuleSync(wasmModule, {
childThread: true,
wasi,
overwriteImports(importObject) {
importObject.env = {
...importObject.env,
...importObject.napi,
...importObject.emnapi,
memory: wasmMemory
}
}
})
}
})

globalThis.onmessage = function(e) {
handler.handle(e)
}
`
Binary file modified examples/napi/__tests__/__snapshots__/typegen.spec.ts.snap
Binary file not shown.
7 changes: 7 additions & 0 deletions examples/napi/__tests__/__snapshots__/values.spec.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@ Generated by [AVA](https://avajs.dev).
[
'@napi-rs/cli',
'@types/lodash',
'@vitest/browser',
'@vitest/ui',
'ava',
'cross-env',
'electron',
'lodash',
'memfs-browser',
'sinon',
'vite',
'vite-plugin-node-polyfills',
'vitest',
'webdriverio',
]
Binary file modified examples/napi/__tests__/__snapshots__/values.spec.ts.snap
Binary file not shown.