From c42f00ff43587ebe99b0cf5784ae1e05013ef57a Mon Sep 17 00:00:00 2001 From: LongYinan Date: Tue, 26 Dec 2023 23:16:42 +0800 Subject: [PATCH] feat(cli): support wasi target test & release workflow (#1867) --- cli/codegen/commands.ts | 22 +- cli/docs/new.md | 4 +- cli/src/api/build.ts | 6 +- cli/src/api/create-npm-dirs.ts | 14 +- cli/src/api/new.ts | 55 +++-- cli/src/api/pre-publish.ts | 10 +- cli/src/api/rename.ts | 2 +- cli/src/api/templates/ci-template.ts | 214 +++++++++--------- cli/src/api/templates/ci.yml.ts | 16 +- cli/src/api/templates/js-binding.ts | 15 ++ .../api/{ => templates}/load-wasi-template.ts | 0 cli/src/api/templates/package.json.ts | 56 ++++- .../{ => templates}/wasi-worker-template.ts | 0 cli/src/api/universalize.ts | 4 +- cli/src/api/version.ts | 2 +- cli/src/commands/build.ts | 2 +- cli/src/commands/new.ts | 16 +- cli/src/def/new.ts | 29 ++- .../__tests__/__snapshots__/target.spec.ts.md | 7 + .../__snapshots__/target.spec.ts.snap | Bin 1042 -> 1112 bytes cli/src/utils/cargo.ts | 2 +- cli/src/utils/config.ts | 16 ++ cli/src/utils/metadata.ts | 4 +- cli/src/utils/misc.ts | 14 +- cli/src/utils/target.ts | 11 +- examples/napi/index.wasi.cjs | 4 +- 26 files changed, 346 insertions(+), 179 deletions(-) rename cli/src/api/{ => templates}/load-wasi-template.ts (100%) rename cli/src/api/{ => templates}/wasi-worker-template.ts (100%) diff --git a/cli/codegen/commands.ts b/cli/codegen/commands.ts index 53d8b93b..1f0d1a20 100644 --- a/cli/codegen/commands.ts +++ b/cli/codegen/commands.ts @@ -34,7 +34,7 @@ const NEW_OPTIONS: CommandSchema = { name: 'path', type: 'string', description: 'The path where the NAPI-RS project will be created.', - required: true, + required: false, }, ], options: [ @@ -53,13 +53,12 @@ const NEW_OPTIONS: CommandSchema = { short: ['v'], long: 'min-node-api', }, - // will support it later - // { - // name: 'packageManager', - // type: 'string', - // description: 'The package manager to use', - // default: "'yarn'", - // }, + { + name: 'packageManager', + type: 'string', + description: 'The package manager to use. Only support yarn 4.x for now.', + default: "'yarn'", + }, { name: 'license', type: 'string', @@ -99,6 +98,13 @@ const NEW_OPTIONS: CommandSchema = { description: 'Whether generate preconfigured GitHub Actions workflow', default: true, }, + { + name: 'testFramework', + type: 'string', + description: + 'The JavaScript test framework to use, only support `ava` for now', + default: "'ava'", + }, { name: 'dryRun', type: 'boolean', diff --git a/cli/docs/new.md b/cli/docs/new.md index a58b9e3b..39ba6526 100644 --- a/cli/docs/new.md +++ b/cli/docs/new.md @@ -25,13 +25,15 @@ new NapiCli().new({ | Options | CLI Options | type | required | default | description | | -------------------- | ------------------------ | -------- | -------- | ------- | -------------------------------------------------------------------------------- | | | --help,-h | | | | get help | -| path | | true | string | | The path where the NAPI-RS project will be created. | +| path | | false | string | | The path where the NAPI-RS project will be created. | | name | --name,-n | string | false | | The name of the project, default to the name of the directory if not provided | | minNodeApiVersion | --min-node-api,-v | number | false | 4 | The minimum Node-API version to support | +| packageManager | --package-manager | string | false | 'yarn' | The package manager to use. Only support yarn 4.x for now. | | license | --license,-l | string | false | 'MIT' | License for open-sourced project | | targets | --targets,-t | string[] | false | [] | All targets the crate will be compiled for. | | enableDefaultTargets | --enable-default-targets | boolean | false | true | Whether enable default targets | | enableAllTargets | --enable-all-targets | boolean | false | false | Whether enable all targets | | enableTypeDef | --enable-type-def | boolean | false | true | Whether enable the `type-def` feature for typescript definitions auto-generation | | enableGithubActions | --enable-github-actions | boolean | false | true | Whether generate preconfigured GitHub Actions workflow | +| testFramework | --test-framework | string | false | 'ava' | The JavaScript test framework to use, only support `ava` for now | | dryRun | --dry-run | boolean | false | false | Whether to run the command in dry-run mode | diff --git a/cli/src/api/build.ts b/cli/src/api/build.ts index 97f5505c..234ed178 100644 --- a/cli/src/api/build.ts +++ b/cli/src/api/build.ts @@ -31,9 +31,9 @@ import { writeFileAsync, } from '../utils/index.js' -import { createWasiBinding } from './load-wasi-template.js' import { createCjsBinding } from './templates/index.js' -import { WASI_WORKER_TEMPLATE } from './wasi-worker-template.js' +import { createWasiBinding } from './templates/load-wasi-template.js' +import { WASI_WORKER_TEMPLATE } from './templates/wasi-worker-template.js' const debug = debugFactory('build') const require = createRequire(import.meta.url) @@ -704,7 +704,7 @@ class Builder { destName += `.${this.target.platformArchABI}` } if (srcName.endsWith('.wasm')) { - destName += '.wasi-wasm32.wasm' + destName += '.wasm' } else { destName += '.node' } diff --git a/cli/src/api/create-npm-dirs.ts b/cli/src/api/create-npm-dirs.ts index d2065552..d535ea78 100644 --- a/cli/src/api/create-npm-dirs.ts +++ b/cli/src/api/create-npm-dirs.ts @@ -1,4 +1,4 @@ -import { join, resolve } from 'path' +import { join, resolve } from 'node:path' import { applyDefaultCreateNpmDirsOptions, @@ -55,11 +55,13 @@ export async function createNpmDirs(userOptions: CreateNpmDirsOptions) { const targetDir = join(npmPath, `${target.platformArchABI}`) await mkdirAsync(targetDir) - const binaryFileName = `${binaryName}.${target.platformArchABI}.node` + const binaryFileName = + target.arch === 'wasm32' + ? `${binaryName}.${target.platformArchABI}.wasm` + : `${binaryName}.${target.platformArchABI}.node` const scopedPackageJson = { name: `${packageName}-${target.platformArchABI}`, version: packageJson.version, - os: [target.platform], cpu: target.arch !== 'universal' ? [target.arch] : undefined, main: binaryFileName, files: [binaryFileName], @@ -77,9 +79,11 @@ export async function createNpmDirs(userOptions: CreateNpmDirsOptions) { 'bugs', ), } + if (target.arch !== 'wasm32') { + // @ts-expect-error + scopedPackageJson.os = [target.platform] + } - // Only works with yarn 3.1+ - // https://github.com/yarnpkg/berry/pull/3981 if (target.abi === 'gnu') { // @ts-expect-error scopedPackageJson.libc = ['glibc'] diff --git a/cli/src/api/new.ts b/cli/src/api/new.ts index afe50062..78abc61d 100644 --- a/cli/src/api/new.ts +++ b/cli/src/api/new.ts @@ -1,4 +1,4 @@ -import path from 'path' +import path from 'node:path' import { applyDefaultNewOptions, @@ -12,7 +12,9 @@ import { mkdirAsync, readdirAsync, statAsync, + type SupportedTestFramework, writeFileAsync, + SupportedPackageManager, } from '../utils/index.js' import { napiEngineRequirement } from '../utils/version.js' @@ -37,6 +39,9 @@ type NewOptions = Required function processOptions(options: RawNewOptions) { debug('Processing options...') + if (!options.path) { + throw new Error('Please provide the path as the argument') + } options.path = path.resolve(process.cwd(), options.path) debug(`Resolved target path to: ${options.path}`) @@ -69,7 +74,7 @@ export async function newProject(userOptions: RawNewOptions) { debug('Targets to be enabled:') debug(options.targets) - const outputs = generateFiles(options) + const outputs = await generateFiles(options) await ensurePath(options.path, options.dryRun) @@ -110,30 +115,34 @@ async function ensurePath(path: string, dryRun = false) { } } -function generateFiles(options: NewOptions): Output[] { +async function generateFiles(options: NewOptions): Promise { + const packageJson = await generatePackageJson(options) return [ generateCargoToml, generateLibRs, generateBuildRs, - generatePackageJson, generateGithubWorkflow, generateIgnoreFiles, - ].flatMap((generator) => { - const output = generator(options) + ] + .flatMap((generator) => { + const output = generator(options) - if (!output) { - return [] - } + if (!output) { + return [] + } - if (Array.isArray(output)) { - return output.map((o) => ({ - ...o, - target: path.join(options.path, o.target), - })) - } else { - return [{ ...output, target: path.join(options.path, output.target) }] - } - }) + if (Array.isArray(output)) { + return output.map((o) => ({ + ...o, + target: path.join(options.path, o.target), + })) + } else { + return [{ ...output, target: path.join(options.path, output.target) }] + } + }) + .concat([ + { ...packageJson, target: path.join(options.path, packageJson.target) }, + ]) } function generateCargoToml(options: NewOptions): Output { @@ -162,16 +171,17 @@ function generateBuildRs(_options: NewOptions): Output { } } -function generatePackageJson(options: NewOptions): Output { +async function generatePackageJson(options: NewOptions): Promise { return { target: './package.json', - content: createPackageJson({ + content: await createPackageJson({ name: options.name, binaryName: getBinaryName(options.name), targets: options.targets, license: options.license, engineRequirement: napiEngineRequirement(options.minNodeApiVersion), cliVersion: CLI_VERSION, + testFramework: options.testFramework as SupportedTestFramework, }), } } @@ -183,7 +193,10 @@ function generateGithubWorkflow(options: NewOptions): Output | null { return { target: './.github/workflows/ci.yml', - content: createGithubActionsCIYml(options.targets), + content: createGithubActionsCIYml( + options.targets, + options.packageManager as SupportedPackageManager, + ), } } diff --git a/cli/src/api/pre-publish.ts b/cli/src/api/pre-publish.ts index 1deaccce..b2e27afa 100644 --- a/cli/src/api/pre-publish.ts +++ b/cli/src/api/pre-publish.ts @@ -1,6 +1,6 @@ -import { execSync } from 'child_process' -import { existsSync, statSync } from 'fs' -import { join, resolve } from 'path' +import { execSync } from 'node:child_process' +import { existsSync, statSync } from 'node:fs' +import { join, resolve } from 'node:path' import { Octokit } from '@octokit/rest' @@ -153,7 +153,9 @@ export async function prePublish(userOptions: PrePublishOptions) { options.npmDir, `${target.platformArchABI}`, ) - const filename = `${binaryName}.${target.platformArchABI}.node` + const ext = + target.platform === 'wasi' || target.platform === 'wasm' ? 'wasm' : 'node' + const filename = `${binaryName}.${target.platformArchABI}.${ext}` const dstPath = join(pkgDir, filename) if (!options.dryRun) { diff --git a/cli/src/api/rename.ts b/cli/src/api/rename.ts index e4da7541..68cfca88 100644 --- a/cli/src/api/rename.ts +++ b/cli/src/api/rename.ts @@ -1,4 +1,4 @@ -import { resolve } from 'path' +import { resolve } from 'node:path' import { isNil, merge, omitBy, pick } from 'lodash-es' diff --git a/cli/src/api/templates/ci-template.ts b/cli/src/api/templates/ci-template.ts index 1b7c2317..6412e610 100644 --- a/cli/src/api/templates/ci-template.ts +++ b/cli/src/api/templates/ci-template.ts @@ -1,4 +1,6 @@ -export const YAML = () => ` +import type { SupportedPackageManager } from '../../utils/config.js' + +export const YAML = (packageManager: SupportedPackageManager) => ` name: CI env: @@ -32,82 +34,53 @@ jobs: settings: - host: macos-latest target: 'x86_64-apple-darwin' - build: | - yarn build --platform - strip -x *.node + build: ${packageManager} build --platform - host: windows-latest - build: yarn build --platform + build: ${packageManager} build --platform target: 'x86_64-pc-windows-msvc' - host: windows-latest build: | - yarn build --platform --target i686-pc-windows-msvc - yarn test + ${packageManager} build --platform --target i686-pc-windows-msvc + ${packageManager} test target: 'i686-pc-windows-msvc' - host: ubuntu-latest target: 'x86_64-unknown-linux-gnu' - docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian - build: >- - set -e &&\n - yarn build --platform --target x86_64-unknown-linux-gnu &&\n - strip *.node + build: ${packageManager} build --platform --target x86_64-unknown-linux-gnu --use-napi-cross - host: ubuntu-latest target: 'x86_64-unknown-linux-musl' - docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine - build: >- - set -e && - yarn build --platform && - strip *.node + build: ${packageManager} build --platform --target x86_64-unknown-linux-musl -x - host: macos-latest target: 'aarch64-apple-darwin' - build: | - yarn build --platform --target aarch64-apple-darwin - strip -x *.node + build: ${packageManager} build --platform --target aarch64-apple-darwin - host: ubuntu-latest target: 'aarch64-unknown-linux-gnu' - docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 - build: >- - set -e &&\n - yarn build --platform --target aarch64-unknown-linux-gnu &&\n - aarch64-unknown-linux-gnu-strip *.node + build: ${packageManager} build --platform --target aarch64-unknown-linux-gnu --use-napi-cross - host: ubuntu-latest target: 'armv7-unknown-linux-gnueabihf' - setup: | - sudo apt-get update - sudo apt-get install gcc-arm-linux-gnueabihf -y - build: | - yarn build --platform --target armv7-unknown-linux-gnueabihf --cross-compile - arm-linux-gnueabihf-strip *.node + build: ${packageManager} build --platform --target armv7-unknown-linux-gnueabihf --use-napi-cross - host: ubuntu-latest target: 'aarch64-linux-android' - build: | - yarn build --platform --target aarch64-linux-android - \${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip *.node + build: ${packageManager} build --platform --target aarch64-linux-android - host: ubuntu-latest target: 'armv7-linux-androideabi' - build: | - yarn build --platform --target armv7-linux-androideabi - \${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip *.node + build: ${packageManager} build --platform --target armv7-linux-androideabi - host: ubuntu-latest target: 'aarch64-unknown-linux-musl' - docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine - build: >- - set -e &&\n - rustup target add aarch64-unknown-linux-musl &&\n - yarn build --platform --target aarch64-unknown-linux-musl &&\n - /aarch64-linux-musl-cross/bin/aarch64-linux-musl-strip *.node + build: ${packageManager} build --platform --target aarch64-unknown-linux-musl -x - host: windows-latest target: 'aarch64-pc-windows-msvc' - build: yarn build --platform --target aarch64-pc-windows-msvc + build: ${packageManager} build --platform --target aarch64-pc-windows-msvc - host: ubuntu-latest target: 'riscv64gc-unknown-linux-gnu' setup: | sudo apt-get update sudo apt-get install gcc-riscv64-linux-gnu -y - build: | - yarn build --platform --target riscv64gc-unknown-linux-gnu - riscv64-linux-gnu-strip *.node + build: ${packageManager} build --platform --target riscv64gc-unknown-linux-gnu + - host: ubuntu-latest + target: 'wasm32-wasi-preview1-threads' + build: ${packageManager} build --platform --target wasm32-wasi-preview1-threads - name: stable - \${{ matrix.settings.target }} - node@18 + name: stable - \${{ matrix.settings.target }} - node@20 runs-on: \${{ matrix.settings.host }} steps: @@ -115,14 +88,12 @@ jobs: - name: Setup node uses: actions/setup-node@v4 - if: \${{ !matrix.settings.docker }} with: - node-version: 18 - cache: yarn + node-version: 20 + cache: ${packageManager} - name: Install uses: dtolnay/rust-toolchain@stable - if: \${{ !matrix.settings.docker }} with: toolchain: stable targets: \${{ matrix.settings.target }} @@ -134,12 +105,13 @@ jobs: ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ + ~/.napi-rs .cargo-cache target/ key: \${{ matrix.settings.target }}-cargo-\${{ matrix.settings.host }} - uses: goto-bus-stop/setup-zig@v2 - if: \${{ matrix.settings.target == 'armv7-unknown-linux-gnueabihf' }} + if: \${{ contains(matrix.settings.target, 'musl') }} with: version: 0.11.0 @@ -154,36 +126,35 @@ jobs: shell: bash - name: 'Install dependencies' - run: yarn install + run: ${packageManager} install - name: Setup node x86 uses: actions/setup-node@v4 if: matrix.settings.target == 'i686-pc-windows-msvc' with: - node-version: 18 - cache: yarn + node-version: 20 architecture: x86 - - name: Build in docker - uses: addnab/docker-run-action@v3 - if: \${{ matrix.settings.docker }} - with: - image: \${{ matrix.settings.docker }} - options: --user 0:0 -v \${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v \${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v \${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v \${{ github.workspace }}:/build -w /build - run: \${{ matrix.settings.build }} - - name: 'Build' run: \${{ matrix.settings.build }} - if: \${{ !matrix.settings.docker }} shell: bash - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 + if: matrix.settings.target != 'wasm32-wasi-preview1-threads' with: name: bindings-\${{ matrix.settings.target }} path: "*.node" if-no-files-found: error + - name: Upload artifact + uses: actions/upload-artifact@v4 + if: matrix.settings.target == 'wasm32-wasi-preview1-threads' + with: + name: bindings-\${{ matrix.settings.target }} + path: "*.wasm" + if-no-files-found: error + build-freebsd: runs-on: macos-12 name: Build FreeBSD @@ -191,7 +162,8 @@ jobs: - uses: actions/checkout@v4 - name: Build id: build - uses: cross-platform-actions/action@v0.21.0 + uses: cross-platform-actions/action@v0.21.1 + timeout-minutes: 30 env: DEBUG: 'napi:*' RUSTUP_IO_THREADS: 1 @@ -202,9 +174,9 @@ jobs: cpu_count: 3 environment_variables: 'DEBUG RUSTUP_IO_THREADS' shell: bash - prepare: | + run: | sudo pkg install -y -f curl node libnghttp2 npm - sudo npm install -g yarn --ignore-scripts + sudo npm install -g ${packageManager} --ignore-scripts curl https://sh.rustup.rs -sSf --output rustup.sh sh rustup.sh -y --profile minimal --default-toolchain stable source "$HOME/.cargo/env" @@ -219,15 +191,15 @@ jobs: whoami env freebsd-version - yarn install - yarn build + ${packageManager} install + ${packageManager} build strip -x *.node yarn test rm -rf node_modules rm -rf target rm -rf .yarn/cache - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: bindings-freebsd path: "*.node" @@ -255,13 +227,13 @@ jobs: uses: actions/setup-node@v4 with: node-version: \${{ matrix.node }} - cache: 'yarn' + cache: '${packageManager}' - name: 'Install dependencies' - run: yarn install + run: ${packageManager} install - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: bindings-\${{ matrix.settings.target }} path: . @@ -271,7 +243,7 @@ jobs: shell: bash - name: Test bindings - run: yarn test + run: ${packageManager} test test-linux-x64-gnu-binding: name: Test bindings on Linux-x64-gnu - node@\${{ matrix.node }} @@ -290,13 +262,13 @@ jobs: uses: actions/setup-node@v4 with: node-version: \${{ matrix.node }} - cache: 'yarn' + cache: '${packageManager}' - name: 'Install dependencies' - run: yarn install + run: ${packageManager} install - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: bindings-x86_64-unknown-linux-gnu path: . @@ -330,10 +302,10 @@ jobs: - name: 'Install dependencies' run: | yarn config set supportedArchitectures.libc "musl" - yarn install + ${packageManager} install - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: bindings-x86_64-unknown-linux-musl path: . @@ -352,14 +324,14 @@ jobs: strategy: fail-fast: false matrix: - node: ['18', '20'] + node: ['20'] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: bindings-aarch64-unknown-linux-gnu path: . @@ -372,7 +344,7 @@ jobs: run: | yarn config set supportedArchitectures.cpu "arm64" yarn config set supportedArchitectures.libc "glibc" - yarn install + ${packageManager} install - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -385,10 +357,7 @@ jobs: with: image: node:\${{ matrix.node }}-slim options: --platform linux/arm64 -v \${{ github.workspace }}:/build -w /build - run: | - set -e - yarn test - ls -la + run: yarn test test-linux-aarch64-musl-binding: name: Test bindings on aarch64-unknown-linux-musl - node@\${{ matrix.node }} @@ -405,7 +374,7 @@ jobs: - uses: actions/checkout@v4 - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: bindings-aarch64-unknown-linux-musl path: . @@ -418,7 +387,7 @@ jobs: run: | yarn config set supportedArchitectures.cpu "arm64" yarn config set supportedArchitectures.libc "musl" - yarn install + ${packageManager} install - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -431,9 +400,7 @@ jobs: with: image: node:\${{ matrix.node }}-alpine options: --platform linux/arm64 -v \${{ github.workspace }}:/build -w /build - run: | - set -e - yarn test + run: yarn test test-linux-arm-gnueabihf-binding: name: Test bindings on armv7-unknown-linux-gnueabihf - node@\${{ matrix.node }} @@ -449,7 +416,7 @@ jobs: - uses: actions/checkout@v4 - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: bindings-armv7-unknown-linux-gnueabihf path: . @@ -461,7 +428,7 @@ jobs: - name: Install dependencies run: | yarn config set supportedArchitectures.cpu "arm" - yarn install + ${packageManager} install - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -474,10 +441,7 @@ jobs: with: image: node:\${{ matrix.node }}-bullseye-slim options: --platform linux/arm/v7 -v \${{ github.workspace }}:/build -w /build - run: | - set -e - yarn test - ls -la + run: ${packageManager} test universal-macOS: name: Build universal macOS binary @@ -491,33 +455,62 @@ jobs: - name: Setup node uses: actions/setup-node@v4 with: - node-version: 18 - cache: yarn + node-version: 20 + cache: ${packageManager} - name: 'Install dependencies' - run: yarn install + run: ${packageManager} install - name: Download macOS x64 artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: bindings-x86_64-apple-darwin path: . - name: Download macOS arm64 artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: bindings-aarch64-apple-darwin path: . - name: Combine binaries - run: yarn napi universalize + run: ${packageManager} napi universalize - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: bindings-universal-apple-darwin path: "*.node" if-no-files-found: error + test-wasi-nodejs: + name: Test bindings on wasi - node@\${{ matrix.node }} + needs: + - build + strategy: + fail-fast: false + matrix: + node: ['18', '20'] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: bindings-wasm32-wasi-preview1-threads + path: . + - name: List packages + run: ls -R . + - uses: actions/setup-node@v4 + with: + node-version: \${{ matrix.node }} + cache: ${packageManager} + - name: 'Install dependencies' + run: ${packageManager} install + - name: Test + run: ${packageManager} test + env: + NAPI_RS_FORCE_WASI: true + publish: name: Publish runs-on: ubuntu-latest @@ -528,6 +521,7 @@ jobs: - test-linux-arm-gnueabihf-binding - test-macOS-windows-binding - test-linux-aarch64-musl-binding + - test-wasi-nodejs - build-freebsd steps: @@ -536,15 +530,15 @@ jobs: - name: Setup node uses: actions/setup-node@v4 with: - node-version: 18 - cache: 'yarn' + node-version: 20 + cache: '${packageManager}' registry-url: 'https://registry.npmjs.org' - name: 'Install dependencies' - run: yarn install + run: ${packageManager} install - name: Download all artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: artifacts diff --git a/cli/src/api/templates/ci.yml.ts b/cli/src/api/templates/ci.yml.ts index 01ba4ad7..7346f11d 100644 --- a/cli/src/api/templates/ci.yml.ts +++ b/cli/src/api/templates/ci.yml.ts @@ -2,6 +2,7 @@ import { load, dump } from 'js-yaml' import { NodeArchToCpu, + type SupportedPackageManager, UniArchsByPlatform, parseTriple, } from '../../utils/index.js' @@ -15,9 +16,13 @@ const TEST_LINUX_X64_MUSL = 'test-linux-x64-musl-binding' const TEST_LINUX_AARCH64_GNU = 'test-linux-aarch64-gnu-binding' const TEST_LINUX_AARCH64_MUSL = 'test-linux-aarch64-musl-binding' const TEST_LINUX_ARM_GNUEABIHF = 'test-linux-arm-gnueabihf-binding' +const TEST_WASI = 'test-wasi-nodejs' const UNIVERSAL_MACOS = 'universal-macOS' -export const createGithubActionsCIYml = (targets: string[]) => { +export const createGithubActionsCIYml = ( + targets: string[], + packageManager: SupportedPackageManager, +) => { const allTargets = new Set( targets.flatMap((t) => { const platform = parseTriple(t) @@ -31,7 +36,7 @@ export const createGithubActionsCIYml = (targets: string[]) => { }), ) - const fullTemplate = load(YAML()) as any + const fullTemplate = load(YAML(packageManager)) as any const requiredSteps = [] const enableWindowsX86 = allTargets.has('x86_64-pc-windows-msvc') @@ -43,6 +48,7 @@ export const createGithubActionsCIYml = (targets: string[]) => { const enableLinuxArm7 = allTargets.has('armv7-unknown-linux-gnueabihf') const enableFreeBSD = allTargets.has('x86_64-unknown-freebsd') const enableMacOSUni = allTargets.has('universal-apple-darwin') + const enableWasi = allTargets.has('wasm32-wasi-preview1-threads') fullTemplate.jobs.build.strategy.matrix.settings = fullTemplate.jobs.build.strategy.matrix.settings.filter( ({ target }: { target: string }) => allTargets.has(target), @@ -111,6 +117,12 @@ export const createGithubActionsCIYml = (targets: string[]) => { requiredSteps.push(UNIVERSAL_MACOS) } + if (!enableWasi) { + delete fullTemplate.jobs[TEST_WASI] + } else { + requiredSteps.push(TEST_WASI) + } + fullTemplate.jobs.publish.needs = requiredSteps return dump(fullTemplate, { diff --git a/cli/src/api/templates/js-binding.ts b/cli/src/api/templates/js-binding.ts index 48555172..596c7059 100644 --- a/cli/src/api/templates/js-binding.ts +++ b/cli/src/api/templates/js-binding.ts @@ -311,6 +311,21 @@ switch (platform) { throw new Error(\`Unsupported OS: \${platform}, architecture: \${arch}\`) } +if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { + try { + localFileExisted = existsSync( + join(__dirname, '${localName}.wasm32-wasi.wasm') + ) && existsSync(join(__dirname, '${localName}.wasi.cjs')) + if (localFileExisted) { + nativeBinding = require('./${localName}.wasi.cjs') + } else { + nativeBinding = require('${pkgName}-wasm32-wasi') + } + } catch { + // ignore + } +} + if (!nativeBinding) { if (loadError) { throw loadError diff --git a/cli/src/api/load-wasi-template.ts b/cli/src/api/templates/load-wasi-template.ts similarity index 100% rename from cli/src/api/load-wasi-template.ts rename to cli/src/api/templates/load-wasi-template.ts diff --git a/cli/src/api/templates/package.json.ts b/cli/src/api/templates/package.json.ts index 0d3a9aa6..c67af55d 100644 --- a/cli/src/api/templates/package.json.ts +++ b/cli/src/api/templates/package.json.ts @@ -1,12 +1,21 @@ -import { CommonPackageJsonFields } from '../../utils/config.js' +import type { + CommonPackageJsonFields, + SupportedTestFramework, +} from '../../utils/config.js' +import { UNIVERSAL_TARGETS } from '../../utils/target.js' -export const createPackageJson = ({ +interface PackageMeta { + 'dist-tags': { [index: string]: string } +} + +export const createPackageJson = async ({ name, binaryName, targets, license, engineRequirement, cliVersion, + testFramework, }: { name: string binaryName: string @@ -14,10 +23,24 @@ export const createPackageJson = ({ license: string engineRequirement: string cliVersion: string + testFramework: SupportedTestFramework }) => { + const hasWasmTarget = targets.some((t) => t.includes('wasm')) + const universalTargets = targets.filter( + (t) => t in UNIVERSAL_TARGETS, + ) as (keyof typeof UNIVERSAL_TARGETS)[] + const unifiedtargets = universalTargets.length + ? targets.filter( + (target) => + !universalTargets.some((t) => { + // @ts-expect-error + return UNIVERSAL_TARGETS[t].includes(target) + }), + ) + : targets const content: CommonPackageJsonFields = { name, - version: '1.0.0', + version: '0.0.0', license, engines: { node: engineRequirement, @@ -29,10 +52,10 @@ export const createPackageJson = ({ exports: undefined, napi: { binaryName, - targets, + targets: unifiedtargets, }, scripts: { - test: 'node -e "assert(require(\'.\').sum(1, 2) === 3)"', + test: testFramework, build: 'napi build --release --platform --strip', 'build:debug': 'napi build', prepublishOnly: 'napi prepublish -t npm', @@ -44,5 +67,28 @@ export const createPackageJson = ({ }, } + if (testFramework === 'ava') { + const avaMeta = await fetch(`https://registry.npmjs.org/ava`).then( + (res) => res.json() as Promise, + ) + content.devDependencies!['ava'] = `^${avaMeta['dist-tags'].latest}` + content.ava = { + timeout: '1m', + } + } + + if (hasWasmTarget) { + const emnapiCoreMeta = await fetch( + `https://registry.npmjs.org/@emnapi/core`, + ).then((res) => res.json() as Promise) + const latest = emnapiCoreMeta['dist-tags'].latest + content.devDependencies!['@emnapi/core'] = `^${latest}` + const emnapiRuntimeMeta = await fetch( + `https://registry.npmjs.org/@emnapi/runtime`, + ).then((res) => res.json() as Promise) + const runtimeLatest = emnapiRuntimeMeta['dist-tags'].latest + content.devDependencies!['@emnapi/runtime'] = `^${runtimeLatest}` + } + return JSON.stringify(content, null, 2) } diff --git a/cli/src/api/wasi-worker-template.ts b/cli/src/api/templates/wasi-worker-template.ts similarity index 100% rename from cli/src/api/wasi-worker-template.ts rename to cli/src/api/templates/wasi-worker-template.ts diff --git a/cli/src/api/universalize.ts b/cli/src/api/universalize.ts index a0a05c70..51f14300 100644 --- a/cli/src/api/universalize.ts +++ b/cli/src/api/universalize.ts @@ -1,5 +1,5 @@ -import { spawnSync } from 'child_process' -import { join, resolve } from 'path' +import { spawnSync } from 'node:child_process' +import { join, resolve } from 'node:path' import { applyDefaultUniversalizeOptions, diff --git a/cli/src/api/version.ts b/cli/src/api/version.ts index af0a6f41..e1d339c4 100644 --- a/cli/src/api/version.ts +++ b/cli/src/api/version.ts @@ -1,4 +1,4 @@ -import { join, resolve } from 'path' +import { join, resolve } from 'node:path' import { applyDefaultVersionOptions, VersionOptions } from '../def/version.js' import { diff --git a/cli/src/commands/build.ts b/cli/src/commands/build.ts index 384f3b0c..b8a9b295 100644 --- a/cli/src/commands/build.ts +++ b/cli/src/commands/build.ts @@ -1,4 +1,4 @@ -import { execSync } from 'child_process' +import { execSync } from 'node:child_process' import { Option } from 'clipanion' diff --git a/cli/src/commands/new.ts b/cli/src/commands/new.ts index d27beabd..31a198ed 100644 --- a/cli/src/commands/new.ts +++ b/cli/src/commands/new.ts @@ -1,4 +1,4 @@ -import path from 'path' +import path from 'node:path' import { Option } from 'clipanion' import inquirer from 'inquirer' @@ -16,7 +16,7 @@ import { napiEngineRequirement } from '../utils/version.js' const debug = debugFactory('new') export class NewCommand extends BaseNewCommand { - interactive = Option.Boolean('--interactive,-i', false, { + interactive = Option.Boolean('--interactive,-i', true, { description: 'Ask project basic information interactively without just using the default.', }) @@ -37,9 +37,19 @@ export class NewCommand extends BaseNewCommand { const cmdOptions = super.getOptions() if (this.interactive) { + const targetPath: string = cmdOptions.path + ? cmdOptions.path + : await inquirer + .prompt({ + type: 'input', + name: 'path', + message: 'Target path to create the project, relative to cwd', + }) + .then(({ path }) => path) + cmdOptions.path = targetPath return { ...cmdOptions, - name: await this.fetchName(path.parse(cmdOptions.path).base), + name: await this.fetchName(path.parse(targetPath).base), minNodeApiVersion: await this.fetchNapiVersion(), targets: await this.fetchTargets(), license: await this.fetchLicense(), diff --git a/cli/src/def/new.ts b/cli/src/def/new.ts index 609e1f9e..5c21eca9 100644 --- a/cli/src/def/new.ts +++ b/cli/src/def/new.ts @@ -10,7 +10,7 @@ export abstract class BaseNewCommand extends Command { description: 'Create a new project with pre-configured boilerplate', }) - $$path = Option.String({ required: true }) + $$path = Option.String({ required: false }) $$name?: string = Option.String('--name,-n', { description: @@ -22,6 +22,10 @@ export abstract class BaseNewCommand extends Command { description: 'The minimum Node-API version to support', }) + packageManager = Option.String('--package-manager', 'yarn', { + description: 'The package manager to use. Only support yarn 4.x for now.', + }) + license = Option.String('--license,-l', 'MIT', { description: 'License for open-sourced project', }) @@ -47,6 +51,11 @@ export abstract class BaseNewCommand extends Command { description: 'Whether generate preconfigured GitHub Actions workflow', }) + testFramework = Option.String('--test-framework', 'ava', { + description: + 'The JavaScript test framework to use, only support `ava` for now', + }) + dryRun = Option.Boolean('--dry-run', false, { description: 'Whether to run the command in dry-run mode', }) @@ -56,12 +65,14 @@ export abstract class BaseNewCommand extends Command { path: this.$$path, name: this.$$name, minNodeApiVersion: this.minNodeApiVersion, + packageManager: this.packageManager, license: this.license, targets: this.targets, enableDefaultTargets: this.enableDefaultTargets, enableAllTargets: this.enableAllTargets, enableTypeDef: this.enableTypeDef, enableGithubActions: this.enableGithubActions, + testFramework: this.testFramework, dryRun: this.dryRun, } } @@ -74,7 +85,7 @@ export interface NewOptions { /** * The path where the NAPI-RS project will be created. */ - path: string + path?: string /** * The name of the project, default to the name of the directory if not provided */ @@ -85,6 +96,12 @@ export interface NewOptions { * @default 4 */ minNodeApiVersion?: number + /** + * The package manager to use. Only support yarn 4.x for now. + * + * @default 'yarn' + */ + packageManager?: string /** * License for open-sourced project * @@ -121,6 +138,12 @@ export interface NewOptions { * @default true */ enableGithubActions?: boolean + /** + * The JavaScript test framework to use, only support `ava` for now + * + * @default 'ava' + */ + testFramework?: string /** * Whether to run the command in dry-run mode * @@ -132,12 +155,14 @@ export interface NewOptions { export function applyDefaultNewOptions(options: NewOptions) { return { minNodeApiVersion: 4, + packageManager: 'yarn', license: 'MIT', targets: [], enableDefaultTargets: true, enableAllTargets: false, enableTypeDef: true, enableGithubActions: true, + testFramework: 'ava', dryRun: false, ...options, } diff --git a/cli/src/utils/__tests__/__snapshots__/target.spec.ts.md b/cli/src/utils/__tests__/__snapshots__/target.spec.ts.md index 89060666..ec16fbae 100644 --- a/cli/src/utils/__tests__/__snapshots__/target.spec.ts.md +++ b/cli/src/utils/__tests__/__snapshots__/target.spec.ts.md @@ -114,4 +114,11 @@ Generated by [AVA](https://avajs.dev). platformArchABI: 'linux-riscv64-gnu', triple: 'riscv64gc-unknown-linux-gnu', }, + { + abi: 'wasi', + arch: 'wasm32', + platform: 'wasi', + platformArchABI: 'wasm32-wasi', + triple: 'wasm32-wasi-preview1-threads', + }, ] diff --git a/cli/src/utils/__tests__/__snapshots__/target.spec.ts.snap b/cli/src/utils/__tests__/__snapshots__/target.spec.ts.snap index 51908e449f56e19f98f5907d21fc3360166a5e47..3dd9180326c3407b97de9b31bd84303e85ce8ee6 100644 GIT binary patch literal 1112 zcmV-e1gHB!RzVvWs;aLI49d(GN%hkCxf-1VGJ(_XgP zo@ZO#=2^#gX$KWu|NO;Tjdtm&`K1)m2>Vg8N~M9{E*OI6!3KB}yazVHm*5+48TpecmRg9SycD4A8yqh}Q*Qo61aB~KKCCrAR#zRje@g>Owz5@9=_!wLQe}O}pFsXWC?HT-UmAR2L41?RALWu>Q1@C|@a8tCUB->$?kScf# z48U99iYnOBlC6kSoCin2NpKE)rU|x;Wcw8*H$Wvz$T4sVyqXnks$|uy&~k*_ z3!csiHchgz^P*rYNVZ>4@&_;AY0_@J?X{ve z)Ee~$jw_VuQQ0aMTz&A-=>1|)ZH`-74W?`Ka*?tRQ(ZA>2k)BEHn%&EM%|j%c4`R& zmBR8QiSCFJH66D%G)Eil7K!O~w>N#R4Bs?iDn0y!Y++O0UdVMI%GGnvxb1-(=HtGY zZ0T0dZ~o8WjP2pZ>@TD{I6hrqjE(2_tr^6_lYi`J(s;Z$iYMXm>sTh7wf4Xd zU3>4e)Ns~wBOlHUd0`>W!ti+Y^h84=$uT7fG|?fIgF_l7I--4Hrio4{!Qe=gYLd<> z1+6hmGBoZCL*0g%G4ovI|8m98ivweW#gB{WTS}{Yw!P*jy}Alvd3;mt6C#cFnYORe z4wYJFy2jFj#$xLx=&VJjdb~PuU*+(W+IZcl?|NdGz%zXc7e@D%xzh&_;w_=Jf1li9 zkvH!6SlzS{C;UETZ2LZCj@e(jy?DY~zEB0Oy|-Gf=Q@4c^QqYAQ{G`lc!%w@yx4T3 zy2&oHxT&vsj&Jp6JRvK=34}|>Pyf5Z%5`fm7r=n}yFQmH3R>}lW!ySD4$ELkbBslZ ecK5bQv*X!)#~vIpyA98#HUD3iL?D7-5&!`3IV^Pm literal 1042 zcmV+t1nv7lRzVaI&z%$O z$6)y^5Fd*O00000000BMmP=?8Q5c2qG|A+hnbP{GQmnWT7g?w!x)8O3sHg~T)Rjyo zlXmQM5@wP%q99p_kA;HZ&V>~fd@KY(+!mn=bt4F(OK~B-aN$A}!E$vIK zS{W4;w?11aXcg^<`aMLVg#Jh|NiE>-F;D@gzPTm^T)126}kf*0U5_z1p&kVMFU zB=<{^F~`x4OOme?MIlmxNcf$Uq>RKigW3rtNlc!kvX9C8REC&*M`f7F)qRi=CJ$37 zGkKB9D3kM4#+dv;Wt_>;5Mf(GPEwg<@*0&XCSOvSW>O79X6VNt?}FS9#=$vo54;I` z@#F*fWB4$ozNS=FmD!d>2pI;Oz!6Xf4khW_qif$(}1ZI?+WR{ zS#f(V*N~s9YERmw8QaUpd~LFYr>d^?pTnuYhikDvmu^MtbPZ$Mct+&{*z;`Uf+k=eaV!aSFSajV0oi%9YHs?C-t2pjb1Ft8Fi=G%J za8K{Tf#!27yU+!2@rF@5y-92_-`no^XkE7vJN!PS{`P$;+hTv=>BSQM@rA14`ukRk zS8a3Ja9oXV^e&%as`m_AXnDTrnsuE$%-T);sAIbNbdM)wf8zwarQ1*cm0snzF5aC# M0fkBCygm^C0Be5vH2?qr diff --git a/cli/src/utils/cargo.ts b/cli/src/utils/cargo.ts index d538184a..466491a0 100644 --- a/cli/src/utils/cargo.ts +++ b/cli/src/utils/cargo.ts @@ -1,4 +1,4 @@ -import { execSync } from 'child_process' +import { execSync } from 'node:child_process' import { debug } from './log.js' diff --git a/cli/src/utils/config.ts b/cli/src/utils/config.ts index 2141cf02..8ccd6954 100644 --- a/cli/src/utils/config.ts +++ b/cli/src/utils/config.ts @@ -4,6 +4,18 @@ import { merge, omit } from 'lodash-es' import { fileExists, readFileAsync } from './misc.js' import { DEFAULT_TARGETS, parseTriple, Target } from './target.js' +export type ValueOfConstArray = T[Exclude>] + +export const SupportedPackageManagers = ['yarn'] as const +export const SupportedTestFrameworks = ['ava'] as const + +export type SupportedPackageManager = ValueOfConstArray< + typeof SupportedPackageManagers +> +export type SupportedTestFramework = ValueOfConstArray< + typeof SupportedTestFrameworks +> + export interface UserNapiConfig { /** * Name of the binary to be generated, default to `index` @@ -81,6 +93,10 @@ export interface CommonPackageJsonFields { dependencies?: Record devDependencies?: Record + + ava?: { + timeout?: string + } } export type NapiConfig = Required< diff --git a/cli/src/utils/metadata.ts b/cli/src/utils/metadata.ts index 935943e6..9bff1ccb 100644 --- a/cli/src/utils/metadata.ts +++ b/cli/src/utils/metadata.ts @@ -1,5 +1,5 @@ -import { execSync } from 'child_process' -import fs from 'fs' +import { execSync } from 'node:child_process' +import fs from 'node:fs' export type CrateTargetKind = | 'bin' diff --git a/cli/src/utils/misc.ts b/cli/src/utils/misc.ts index 4a42dd91..27549ce8 100644 --- a/cli/src/utils/misc.ts +++ b/cli/src/utils/misc.ts @@ -1,6 +1,14 @@ -import { readFile, writeFile, copyFile, mkdir, unlink, stat, readdir } from 'fs' -import { createRequire } from 'module' -import { promisify } from 'util' +import { + readFile, + writeFile, + copyFile, + mkdir, + unlink, + stat, + readdir, +} from 'node:fs' +import { createRequire } from 'node:module' +import { promisify } from 'node:util' import { debug } from './log.js' diff --git a/cli/src/utils/target.ts b/cli/src/utils/target.ts index 679df9b0..a44e7b01 100644 --- a/cli/src/utils/target.ts +++ b/cli/src/utils/target.ts @@ -2,6 +2,10 @@ import { execSync } from 'node:child_process' export type Platform = NodeJS.Platform | 'wasm' | 'wasi' +export const UNIVERSAL_TARGETS = { + 'universal-apple-darwin': ['aarch64-apple-darwin', 'x86_64-apple-darwin'], +} as const + export const AVAILABLE_TARGETS = [ 'aarch64-apple-darwin', 'aarch64-linux-android', @@ -18,12 +22,14 @@ export const AVAILABLE_TARGETS = [ 'armv7-linux-androideabi', 'universal-apple-darwin', 'riscv64gc-unknown-linux-gnu', + 'wasm32-wasi-preview1-threads', ] as const export type TargetTriple = (typeof AVAILABLE_TARGETS)[number] export const DEFAULT_TARGETS = [ 'x86_64-apple-darwin', + 'aarch64-apple-darwin', 'x86_64-pc-windows-msvc', 'x86_64-unknown-linux-gnu', ] as const @@ -48,6 +54,7 @@ type NodeJSArch = | 'x32' | 'x64' | 'universal' + | 'wasm32' const CpuToNodeArch: Record = { x86_64: 'x64', @@ -80,7 +87,7 @@ export interface Target { triple: string platformArchABI: string platform: Platform - arch: NodeJSArch | 'wasm32' + arch: NodeJSArch abi: string | null } @@ -98,7 +105,7 @@ export function parseTriple(rawTriple: string): Target { if (rawTriple === 'wasm32-wasi-preview1-threads') { return { triple: rawTriple, - platformArchABI: rawTriple, + platformArchABI: 'wasm32-wasi', platform: 'wasi', arch: 'wasm32', abi: 'wasi', diff --git a/examples/napi/index.wasi.cjs b/examples/napi/index.wasi.cjs index 006539a0..f256f35e 100644 --- a/examples/napi/index.wasi.cjs +++ b/examples/napi/index.wasi.cjs @@ -15,7 +15,7 @@ const __wasi = new __nodeWASI({ version: 'preview1', env: process.env, preopens: { - '/': __dirname, + '/': '/' } }) @@ -27,7 +27,7 @@ const __sharedMemory = new WebAssembly.Memory({ shared: true, }) -const { instance: __napiInstance, module: __wasiModule, napiModule: __napiModule } = __emnapiInstantiateNapiModuleSync(__nodeFs.readFileSync(__nodePath.join(__dirname, 'index.wasi-wasm32.wasm')), { +const { instance: __napiInstance, module: __wasiModule, napiModule: __napiModule } = __emnapiInstantiateNapiModuleSync(__nodeFs.readFileSync(__nodePath.join(__dirname, 'index.wasm')), { context: __emnapiContext, asyncWorkPoolSize: 4, wasi: __wasi,