feat(cli): brand new cli tool with both cli and programmatical usage (#1492)

BREAKING CHANGE: requires node >= 16 and some cli options have been renamed
This commit is contained in:
forehal 2023-04-06 11:04:53 +08:00
parent 7c4dc2a2bd
commit a781a4f27e
194 changed files with 8805 additions and 4158 deletions

View file

@ -14,3 +14,4 @@ target
scripts
triples/index.js
rollup.config.js
crates/cli/index.js

View file

@ -192,7 +192,7 @@ rules:
overrides:
- files:
- ./cli/**/*.ts
- ./**/*.ts
plugins:
- '@typescript-eslint'
parserOptions:

View file

@ -47,13 +47,10 @@ jobs:
- name: Install dependencies
run: yarn install --immutable --mode=skip-build
- name: 'Build TypeScript'
run: yarn build
- name: Cross build
run: |
export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang"
yarn build:test:android:armv7
yarn build:test -- --target armv7-linux-androideabi
du -sh examples/napi/index.node
${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip examples/napi/index.node
du -sh examples/napi/index.node

View file

@ -47,10 +47,7 @@ jobs:
- name: Install dependencies
run: yarn install --immutable --mode=skip-build
- name: 'Build TypeScript'
run: yarn build
- name: Cross build native tests
run: |
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang"
yarn build:test:android
yarn build:test -- --target aarch64-linux-android

View file

@ -44,12 +44,10 @@ jobs:
- name: 'Install dependencies'
run: yarn install --immutable --mode=skip-build
- name: 'Build TypeScript'
run: yarn build
- name: Unit tests with address sanitizer
run: |
yarn build:test:asan
yarn workspace @examples/napi build -- -Z build-std
yarn workspace @examples/compat-mode build -- -Z build-std
LD_PRELOAD=/usr/lib/gcc/x86_64-linux-gnu/9/libasan.so yarn test
env:
RUST_TARGET: x86_64-unknown-linux-gnu

View file

@ -46,9 +46,6 @@ jobs:
- name: 'Install dependencies'
run: yarn install --immutable --mode=skip-build
- name: 'Build ts'
run: yarn build
- name: 'Build bench'
run: yarn build:bench

View file

@ -44,9 +44,6 @@ jobs:
- name: 'Install dependencies'
run: yarn install --mode=skip-build --immutable
- name: 'Build TypeScript'
run: yarn build
- name: Build and run binary
run: |
yarn workspace binary build
@ -54,13 +51,6 @@ jobs:
env:
RUST_BACKTRACE: 1
- name: Pass -p and --cargo-name to build
run: |
node ./cli/scripts/index.js build -p napi-examples-binary --cargo-name napi-examples-binary
./napi-examples-binary
env:
RUST_BACKTRACE: 1
- name: Clear the cargo caches
run: |
cargo install cargo-cache --no-default-features --features ci-autoclean

View file

@ -30,9 +30,6 @@ jobs:
- name: Install dependencies
run: yarn install --immutable --mode=skip-build
- name: 'Build TypeScript'
run: yarn build
- name: Cross build native tests
uses: addnab/docker-run-action@v3
with:
@ -40,8 +37,7 @@ jobs:
options: -v ${{ github.workspace }}:/napi-rs -w /napi-rs
run: |
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-musl-gcc
yarn workspace compat-mode-examples build --target aarch64-unknown-linux-musl
yarn workspace examples build --target aarch64-unknown-linux-musl
yarn build:test -- --target aarch64-unknown-linux-musl
- run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

View file

@ -50,11 +50,8 @@ jobs:
- name: Install dependencies
run: yarn install --immutable --mode=skip-build
- name: 'Build TypeScript'
run: yarn build
- name: Cross build native tests
run: yarn build:test:aarch64
run: yarn build:test -- --target aarch64-unknown-linux-gnu --cross-compile
- run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

View file

@ -51,11 +51,8 @@ jobs:
- name: Install dependencies
run: yarn install --immutable --mode=skip-build
- name: 'Build TypeScript'
run: yarn build
- name: Cross build native tests
run: yarn build:test:armv7
run: yarn build:test -- --target armv7-unknown-linux-gnueabihf --cross-compile
- run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

View file

@ -30,9 +30,6 @@ jobs:
- name: 'Install dependencies'
run: yarn install --immutable --mode=skip-build
- name: 'Build TypeScript'
run: yarn build
- name: Setup and run tests
uses: addnab/docker-run-action@v3
with:
@ -40,5 +37,5 @@ jobs:
options: -v ${{ github.workspace }}:/napi-rs -w /napi-rs
run: |
cargo check -vvv
yarn build:test
yarn build:test -- --target x86_64-unknown-linux-musl
yarn test

View file

@ -49,9 +49,6 @@ jobs:
- name: 'Install dependencies'
run: yarn install --immutable
- name: 'Build TypeScript'
run: yarn build
- name: 'Pull docker image'
run: docker pull node:lts-slim

View file

@ -43,9 +43,6 @@ jobs:
- name: 'Install dependencies'
run: yarn install --mode=skip-build --immutable
- name: 'Build TypeScript'
run: yarn build
- name: Check build
uses: actions-rs/cargo@v1
with:

View file

@ -19,7 +19,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node: ['14', '16', '18']
node: ['16', '18']
os: [ubuntu-latest, macos-latest, windows-latest]
name: stable - ${{ matrix.os }} - node@${{ matrix.node }}
@ -51,9 +51,6 @@ jobs:
- name: 'Install dependencies'
run: yarn install --mode=skip-build --immutable
- name: 'Build TypeScript'
run: yarn build
- name: Check build
uses: actions-rs/cargo@v1
with:
@ -62,6 +59,7 @@ jobs:
- name: Unit tests
run: |
yarn test:cli
yarn build:test
yarn test --verbose
yarn tsc -p examples/napi/tsconfig.json --noEmit

View file

@ -30,9 +30,6 @@ jobs:
- name: 'Install dependencies'
run: yarn install --mode=skip-build --immutable
- name: 'Build TypeScript'
run: yarn build
- name: Install
uses: dtolnay/rust-toolchain@stable
with:
@ -55,7 +52,9 @@ jobs:
args: --all --bins --examples --tests --target aarch64-pc-windows-msvc -vvv
- name: Build release target
run: cargo build --release --target aarch64-pc-windows-msvc
run: |
yarn workspace @examples/napi build --target aarch64-pc-windows-msvc --release
yarn workspace @examples/compat-mode build --target aarch64-pc-windows-msvc --release
- name: Clear the cargo caches
run: |

View file

@ -31,9 +31,6 @@ jobs:
run: |
yarn install --mode=skip-build --immutable
- name: 'Build TypeScript'
run: yarn build
- name: Install
uses: dtolnay/rust-toolchain@stable
with:
@ -55,6 +52,11 @@ jobs:
command: check
args: --all --bins --examples --tests --target i686-pc-windows-msvc -vvv
- name: Build
run: |
yarn workspace @examples/napi build --target i686-pc-windows-msvc --release
yarn workspace @examples/compat-mode build --target i686-pc-windows-msvc --release
- name: Setup node
uses: actions/setup-node@v3
with:
@ -64,8 +66,6 @@ jobs:
- name: Build Tests
run: |
yarn workspace compat-mode-examples build-i686 --release
yarn workspace examples build-i686 --release
yarn test --verbose
node ./node_modules/electron/install.js
yarn test:electron

View file

@ -56,17 +56,15 @@ jobs:
version: 0.10.1
- name: Install dependencies
run: yarn install --immutable --mode=skip-build
- name: 'Build TypeScript'
run: yarn build
- name: Setup node
uses: actions/setup-node@v3
with:
# Testing for compatibility with node v12.x
node-version: 12
- name: Cross build native tests
- name: install MacOS SDK
if: contains(matrix.target, 'apple')
run: |
yarn workspace compat-mode-examples build --target ${{ matrix.target }} --zig
yarn workspace examples build --target ${{ matrix.target }} --zig
curl -L "https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX11.3.sdk.tar.xz" | tar -J -x -C /opt
- name: Cross build native tests
env:
SDKROOT: /opt/MacOSX11.3.sdk
run: |
yarn build:test -- --target ${{ matrix.target }} --cross-compile
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:

View file

@ -1,21 +0,0 @@
import { cpus } from 'os'
const configuration = {
extensions: ['ts', 'tsx'],
files: ['cli/**/*.spec.ts', 'examples/**/__test__/**/*.spec.ts'],
require: ['ts-node/register/transpile-only'],
environmentVariables: {
TS_NODE_PROJECT: './examples/tsconfig.json',
},
timeout: '5m',
workerThreads: true,
concurrency: process.env.CI ? 2 : cpus().length,
failFast: false,
verbose: !!process.env.CI,
}
if (parseInt(process.versions.napi, 10) < 4) {
configuration.compileEnhancements = false
}
export default configuration

View file

@ -3,9 +3,10 @@
"version": "1.0.0",
"private": true,
"scripts": {
"build": "node ../cli/scripts/index.js build --js false --release"
"build": "napi-raw build --js false --release"
},
"devDependencies": {
"@napi-rs/cli": "workspace:*",
"benny": "^3.7.1"
}
}

View file

@ -9,88 +9,27 @@
> Cli tools for napi-rs
```sh
# or npm, pnpm
yarn add @napi-rs/cli -D
yarn napi build
```
## Commands
| Command | desc | docs |
| --------------- | -------------------------------------------------------------- | --------------------------------------------------- |
| new | create new napi-rs project | [./docs/new.md](./docs/new.md) |
| build | build napi-rs project | [./docs/build.md](./docs/build.md) |
| create-npm-dirs | Create npm package dirs for different platforms | [./docs/create-npm-dirs](./docs/create-npm-dirs.md) |
| artifacts | Copy artifacts from Github Actions into specified dir | [./docs/artifacts.md](./docs/artifacts.md) |
| rename | Rename the napi-rs project | [./docs/rename.md](./docs/rename.md) |
| universalize | Combile built binaries into one universal binary | [./docs/universalize.md](./docs/universalize.md) |
| version | Update version in created npm packages by `create-npm-dirs` | [./docs/version.md](./docs/version.md) |
| pre-publish | Update package.json and copy addons into per platform packages | [./docs/pre-publish.md](./docs/pre-publish.md) |
### Debug mode
```bash
DEBUG="napi:*" napi [command]
```
### `napi build`
> Build command. Build rust codes and copy the dynamic lib binary file to the dist dir.
#### `--platform`
> default `false`
Append `platform-arch-[abi]` name to dist file. eg: `index.darwin-x64.node`.
#### `--release`
> default `false`
Is release build. This flag will be passed to `Cargo` directly.
#### `--features`
> default `''`
Cargo features, passthrough to `cargo build` command.
#### `--config,-c`
> default `package.json`
`napi-rs` config file name. `napi-rs` config example :
```js
{
"name": "@native-binding/fib",
"version": "0.1.0",
"napi": {
"name": "fib", // binary name
"triples": {
"defaults": true, // default true, if this value is true, will build `x86_64-pc-windows-msvc`, `x86_64-apple-darwin` and `x86_64-unknown-linux-gnu`
"additional": [
"x86_64-unknown-linux-musl",
"x86_64-unknown-freebsd",
"aarch64-unknown-linux-gnu"
]
}
}
}
```
#### `--cargo-name`
> default `undefined`
If not set, cli will read the `package.name` field in `Cargo.toml` under `process.cwd()`. The `-` in the name will be replaced with `_`.
#### `--target`
> default `undefined`
> Note you should have `rustup` installed if omit the `--target` flag. The `@napi-rs/cli` will try to find the default target on your system via `rustup` if no `--target` specified.
You can also define this value using the `RUST_TARGET` environment variable.
This value will be passed to `Cargo build` command directly. eg: `napi build --target x86_64-unknown-linux-musl`
#### `--cargo-flags`
> default `undefined`
Other flags you want pass to `Cargo build`.
#### `--cargo-cwd`
> default `undefined`
This flag can be used to build binaries that are not in the current directory. The path that is passed to this flag should be relative to the current directory.
### `napi artifacts`
> Copy artifact files in Github actions.

10
cli/ava.config.mjs Normal file
View file

@ -0,0 +1,10 @@
export default {
extensions: {
ts: 'module',
},
files: ['**/__tests__/**/*.spec.ts'],
nodeArguments: ['--loader=ts-node/esm/transpile-only'],
environmentVariables: {
TS_NODE_PROJECT: './tsconfig.json',
},
}

12
cli/cli.mjs Executable file
View file

@ -0,0 +1,12 @@
import { execSync } from 'child_process'
import { resolve } from 'path'
import { fileURLToPath } from 'url'
execSync(
`node --loader ts-node/esm/transpile-only ${resolve(fileURLToPath(import.meta.url), '../src/cli.ts')} ${process.argv
.slice(2)
.join(' ')}`,
{
stdio: 'inherit',
},
)

505
cli/codegen/commands.ts Normal file
View file

@ -0,0 +1,505 @@
export interface ArgSchema {
name: string
type: 'string'
description: string
required?: boolean
}
export interface OptionSchema {
name: string
type: string
description: string
required?: boolean
default?: any
short?: string
long?: string
}
export interface CommandSchema {
name: string
description: string
args: ArgSchema[]
options: OptionSchema[]
}
export type CommandDefineSchema = CommandSchema[]
const NEW_OPTIONS: CommandSchema = {
name: 'new',
description: 'Create a new project with pre-configured boilerplate',
args: [
{
name: 'path',
type: 'string',
description: 'The path where the napi-rs project will be created.',
required: true,
},
],
options: [
{
name: 'name',
type: 'string',
description:
'The name of the project, default to the name of the directory if not provided',
short: 'n',
},
{
name: 'minNodeApiVersion',
type: 'number',
description: 'The minimum Node-API version to support',
default: 4,
short: 'v',
long: 'min-node-api',
},
// will support it later
// {
// name: 'packageManager',
// type: 'string',
// description: 'The package manager to use',
// default: "'yarn'",
// },
{
name: 'license',
type: 'string',
description: 'License for open-sourced project',
short: 'l',
default: "'MIT'",
},
{
name: 'targets',
type: 'string[]',
description: 'All targets the crate will be compiled for.',
short: 't',
default: '[]',
},
{
name: 'enableDefaultTargets',
type: 'boolean',
description: 'Whether enable default targets',
default: true,
},
{
name: 'enableAllTargets',
type: 'boolean',
description: 'Whether enable all targets',
default: false,
},
{
name: 'enableTypeDef',
type: 'boolean',
description:
'Whether enable the `type-def` feature for typescript definitions auto-generation',
default: true,
},
{
name: 'enableGithubActions',
type: 'boolean',
description: 'Whether generate preconfigured GitHub Actions workflow',
default: true,
},
{
name: 'dryRun',
type: 'boolean',
description: 'Whether to run the command in dry-run mode',
default: false,
},
],
}
const BUILD_OPTIONS: CommandSchema = {
name: 'build',
description: 'Build the napi-rs project',
args: [],
options: [
{
name: 'target',
type: 'string',
description:
'Build for the target triple, bypassed to `cargo build --target`',
short: 't',
},
{
name: 'cwd',
type: 'string',
description:
'The working directory of where napi command will be executed in, all other paths options are relative to this path',
},
{
name: 'manifestPath',
type: 'string',
description: 'Path to `Cargo.toml`',
},
{
name: 'packageJsonPath',
type: 'string',
description: 'Path to `package.json`',
},
{
name: 'targetDir',
type: 'string',
description:
'Directory for all crate generated artifacts, see `cargo build --target-dir`',
},
{
name: 'outputDir',
type: 'string',
description:
'Path to where all the built files would be put. Default to the crate folder',
short: 'o',
},
{
name: 'platform',
type: 'boolean',
description:
'Add platform triple to the generated nodejs binding file, eg: `[name].linux-x64-gnu.node`',
},
{
name: 'jsPackageName',
type: 'string',
description:
'Package name in generated js binding file. Only works with `--platform` flag',
},
{
name: 'jsBinding',
type: 'string',
description:
'Path and filename of generated JS binding file. Only works with `--platform` flag. Relative to `--output_dir`.',
long: 'js',
},
{
name: 'noJsBinding',
type: 'boolean',
description:
'Whether to disable the generation JS binding file. Only works with `--platform` flag.',
long: 'no-js',
},
{
name: 'dts',
type: 'string',
description:
'Path and filename of generated type def file. Relative to `--output_dir`',
},
{
name: 'dtsHeader',
type: 'string',
description:
'Custom file header for generated type def file. Only works when `typedef` feature enabled.',
},
{
name: 'noDtsHeader',
type: 'boolean',
description:
'Whether to disable the default file header for generated type def file. Only works when `typedef` feature enabled.',
},
{
name: 'strip',
type: 'boolean',
description: 'Whether strip the library to achieve the minimum file size',
short: 's',
},
{
name: 'release',
type: 'boolean',
description: 'Build in release mode',
short: 'r',
},
{
name: 'verbose',
type: 'boolean',
description: 'Verbosely log build command trace',
short: 'v',
},
{
name: 'bin',
type: 'string',
description: 'Build only the specified binary',
},
{
name: 'package',
type: 'string',
description: 'Build the specified library or the one at cwd',
short: 'p',
},
{
name: 'crossCompile',
type: 'boolean',
description:
'[experimental] cross-compile for the specified target with `cargo-xwin` on windows and `cargo-zigbuild` on other platform',
short: 'x',
},
{
name: 'watch',
type: 'boolean',
description:
'watch the crate changes and build continiously with `cargo-watch` crates',
short: 'w',
},
{
name: 'features',
type: 'string[]',
description: 'Space-separated list of features to activate',
short: 'F',
},
{
name: 'allFeatures',
type: 'boolean',
description: 'Activate all available features',
},
{
name: 'noDefaultFeatures',
type: 'boolean',
description: 'Do not activate the `default` feature',
},
],
}
const ARTIFACTS_OPTIONS: CommandSchema = {
name: 'artifacts',
description:
'Copy artifacts from Github Actions into npm packages and ready to publish',
args: [],
options: [
{
name: 'cwd',
type: 'string',
description:
'The working directory of where napi command will be executed in, all other paths options are relative to this path',
default: 'process.cwd()',
},
{
name: 'packageJsonPath',
type: 'string',
description: 'Path to `package.json`',
default: "'package.json'",
},
{
name: 'outputDir',
type: 'string',
description:
'Path to the folder where all built `.node` files put, same as `--output-dir` of build command',
short: 'o',
default: "'./'",
},
{
name: 'npmDir',
type: 'string',
description: 'Path to the folder where the npm packages put',
default: "'npm'",
},
],
}
const CREATE_NPM_DIRS_OPTIONS: CommandSchema = {
name: 'createNpmDirs',
description: 'Create npm package dirs for different platforms',
args: [],
options: [
{
name: 'cwd',
type: 'string',
description:
'The working directory of where napi command will be executed in, all other paths options are relative to this path',
default: 'process.cwd()',
},
{
name: 'packageJsonPath',
type: 'string',
description: 'Path to `package.json`',
default: "'package.json'",
},
{
name: 'npmDir',
type: 'string',
description: 'Path to the folder where the npm packages put',
default: "'npm'",
},
{
name: 'dryRun',
type: 'boolean',
description: 'Dry run without touching file system',
default: false,
},
],
}
const RENAME_OPTIONS: CommandSchema = {
name: 'rename',
description: 'Rename the napi-rs project',
args: [],
options: [
{
name: 'cwd',
type: 'string',
description:
'The working directory of where napi command will be executed in, all other paths options are relative to this path',
default: 'process.cwd()',
},
{
name: 'packageJsonPath',
type: 'string',
description: 'Path to `package.json`',
default: "'package.json'",
},
{
name: 'npmDir',
type: 'string',
description: 'Path to the folder where the npm packages put',
default: "'npm'",
},
{
name: 'name',
type: 'string',
description: 'The new name of the project',
short: 'n',
},
{
name: 'binaryName',
type: 'string',
description: 'The new binary name *.node files',
short: 'b',
},
{
name: 'packageName',
type: 'string',
description: 'The new package name of the project',
},
{
name: 'manifestPath',
type: 'string',
description: 'Path to `Cargo.toml`',
default: "'Cargo.toml'",
},
{
name: 'repository',
type: 'string',
description: 'The new repository of the project',
},
{
name: 'description',
type: 'string',
description: 'The new description of the project',
},
],
}
const UNIVERSALIZE_OPTIONS: CommandSchema = {
name: 'universalize',
description: 'Combile built binaries into one universal binary',
args: [],
options: [
{
name: 'cwd',
type: 'string',
description:
'The working directory of where napi command will be executed in, all other paths options are relative to this path',
default: 'process.cwd()',
},
{
name: 'packageJsonPath',
type: 'string',
description: 'Path to `package.json`',
default: "'package.json'",
},
{
name: 'outputDir',
type: 'string',
description:
'Path to the folder where all built `.node` files put, same as `--output-dir` of build command',
short: 'o',
default: "'./'",
},
],
}
const VERSION_OPTIONS: CommandSchema = {
name: 'version',
description: 'Update version in created npm packages',
args: [],
options: [
{
name: 'cwd',
type: 'string',
description:
'The working directory of where napi command will be executed in, all other paths options are relative to this path',
default: 'process.cwd()',
},
{
name: 'packageJsonPath',
type: 'string',
description: 'Path to `package.json`',
default: "'package.json'",
},
{
name: 'npmDir',
type: 'string',
description: 'Path to the folder where the npm packages put',
default: "'npm'",
},
],
}
const PRE_PUBLISH_OPTIONS: CommandSchema = {
name: 'prePublish',
description: 'Update package.json and copy addons into per platform packages',
args: [],
options: [
{
name: 'cwd',
type: 'string',
description:
'The working directory of where napi command will be executed in, all other paths options are relative to this path',
default: 'process.cwd()',
},
{
name: 'packageJsonPath',
type: 'string',
description: 'Path to `package.json`',
default: "'package.json'",
},
{
name: 'npmDir',
type: 'string',
description: 'Path to the folder where the npm packages put',
default: "'npm'",
},
{
name: 'tagStyle',
type: "'npm' | 'lerna'",
description: 'git tag style, `npm` or `lerna`',
default: "'lerna'",
},
{
name: 'ghRelease',
type: 'boolean',
description: 'Whether create GitHub release',
default: true,
},
{
name: 'ghReleaseName',
type: 'string',
description: 'GitHub release name',
},
{
name: 'ghReleaseId',
type: 'string',
description: 'Existing GitHub release id',
},
{
name: 'dryRun',
type: 'boolean',
description: 'Dry run without touching file system',
default: false,
},
],
}
export const commandDefines: CommandDefineSchema = [
NEW_OPTIONS,
BUILD_OPTIONS,
ARTIFACTS_OPTIONS,
CREATE_NPM_DIRS_OPTIONS,
RENAME_OPTIONS,
UNIVERSALIZE_OPTIONS,
VERSION_OPTIONS,
PRE_PUBLISH_OPTIONS,
]

279
cli/codegen/index.ts Normal file
View file

@ -0,0 +1,279 @@
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import { kebabCase, startCase } from 'lodash-es'
import { commandDefines, CommandSchema, OptionSchema } from './commands.js'
const __filename = fileURLToPath(import.meta.url)
const defFolder = path.join(__filename, '../../src/def')
const docsTargetFolder = path.join(__filename, '../../docs')
function PascalCase(str: string) {
return startCase(str).replace(/\s/g, '')
}
/**
* convert command definition to command options interface
*/
function generateOptionsDef(command: CommandSchema) {
const optionsName = `${PascalCase(command.name)}Options`
const optLines: string[] = []
optLines.push('/**')
optLines.push(` * ${command.description}`)
optLines.push(' */')
optLines.push(`export interface ${optionsName} {`)
command.args.forEach((arg) => {
optLines.push(' /**')
optLines.push(` * ${arg.description}`)
optLines.push(' */')
optLines.push(` ${arg.name}${arg.required ? '' : '?'}: ${arg.type}`)
})
command.options.forEach((opt) => {
optLines.push(' /**')
optLines.push(` * ${opt.description}`)
if (typeof opt.default !== 'undefined') {
optLines.push(' *')
optLines.push(` * @default ${opt.default}`)
}
optLines.push(' */')
optLines.push(` ${opt.name}${opt.required ? '' : '?'}: ${opt.type}`)
})
optLines.push('}\n')
if (command.options.some((opt) => typeof opt.default !== 'undefined')) {
optLines.push(
`export function applyDefault${optionsName}(options: ${optionsName}) {`,
)
optLines.push(` return {`)
command.options.forEach((opt) => {
if (typeof opt.default !== 'undefined') {
optLines.push(` ${opt.name}: ${opt.default},`)
}
})
optLines.push(' ...options,')
optLines.push(' }')
optLines.push('}\n')
}
return optLines.join('\n')
}
function getOptionDescriptor(opt: OptionSchema) {
let desc = `--${opt.long ?? kebabCase(opt.name)}`
if (opt.short) {
desc += `,-${opt.short}`
}
return desc
}
function generateCommandDef(command: CommandSchema) {
const commandPath = kebabCase(command.name)
const avoidList = ['path', 'name']
const avoidName = (name: string) => {
return avoidList.includes(name) ? '$$' + name : name
}
const prepare: string[] = []
const cmdLines: string[] = []
cmdLines.push(`
export abstract class Base${PascalCase(command.name)}Command extends Command {
static paths = [['${commandPath}']]
static usage = Command.Usage({
description: '${command.description}',
})\n`)
command.args.forEach((arg) => {
cmdLines.push(
` ${avoidName(arg.name)} = Option.String({ required: ${
arg.required ?? false
} })`,
)
})
cmdLines.push('')
command.options.forEach((opt) => {
const optName = avoidName(opt.name)
let optionType = ''
switch (opt.type) {
case 'number':
optionType = 'String'
prepare.push("import * as typanion from 'typanion'")
break
case 'boolean':
optionType = 'Boolean'
break
case 'string[]':
optionType = 'Array'
break
case 'string':
default:
optionType = 'String'
}
const optionDesc = getOptionDescriptor(opt)
if (opt.required) {
cmdLines.push(` ${optName} = Option.${optionType}('${optionDesc}', {`)
cmdLines.push(' required: true,')
} else if (typeof opt.default !== 'undefined') {
const defaultValue =
typeof opt.default === 'number'
? `'${opt.default.toString()}'`
: opt.default
cmdLines.push(` ${optName} = Option.${optionType}(`)
cmdLines.push(` '${optionDesc}',`)
cmdLines.push(` ${defaultValue},`)
cmdLines.push(` {`)
} else {
cmdLines.push(
` ${optName}?: ${opt.type} = Option.${optionType}('${optionDesc}', {`,
)
}
if (opt.type === 'number') {
cmdLines.push(' validator: typanion.isNumber(),')
}
cmdLines.push(` description: '${opt.description}'`)
cmdLines.push(' })\n')
})
cmdLines.push(` getOptions() {`)
cmdLines.push(` return {`)
command.args
.map(({ name }) => name)
.concat(command.options.map(({ name }) => name))
.forEach((name) => {
cmdLines.push(` ${name}: this.${avoidName(name)},`)
})
cmdLines.push(' }')
cmdLines.push(' }')
cmdLines.push('}\n')
return prepare.join('\n') + '\n' + cmdLines.join('\n')
}
function generateDocs(command: CommandSchema, targetFolder: string): string {
const docsFileName = kebabCase(command.name)
const docsFile = path.join(targetFolder, `${docsFileName}.md`)
const options: string[] = []
command.args.forEach((arg) => {
options.push(
[
'',
arg.name,
`<${kebabCase(arg.name)}>`,
arg.required ? 'true' : 'false',
arg.type,
'',
arg.description,
'',
].join('|'),
)
})
command.options.forEach((opt) => {
options.push(
[
'',
opt.name,
getOptionDescriptor(opt),
opt.type.replace(/\|/g, '\\|'),
opt.required ? 'true' : 'false',
opt.default ?? '',
opt.description,
'',
].join('|'),
)
})
const content = `# ${startCase(command.name)}
> This file is generated by cli/codegen. Do not edit this file manually.
${command.description}
## Usage
\`\`\`sh
# CLI
napi ${kebabCase(command.name)}${command.args.reduce(
(h, arg) => h + ` <${arg.name}>`,
'',
)} [--options]
\`\`\`
\`\`\`typescript
// Programatically
import { NapiCli } from '@napi-rs/cli'
new NapiCli().${command.name}({
// options
})
\`\`\`
## Options
| Options | CLI Options | type | required | default | description |
| ------- | ----------- | ---- | -------- | ------- | ----------- |
| | --help,-h | | | | get help |
${options.join('\n')}
`
// make sure the target folder exists
fs.mkdirSync(targetFolder, { recursive: true })
// write file
fs.writeFileSync(docsFile, content)
return docsFile
}
function generateDef(cmd: CommandSchema, folder: string): string {
const defFileName = kebabCase(cmd.name)
const defFilePath = path.join(folder, `${defFileName}.ts`)
const def = `// This file is generated by codegen/index.ts
// Do not edit this file manually
import { Command, Option } from 'clipanion'
${generateCommandDef(cmd)}
${generateOptionsDef(cmd)}
`
// make sure the target folder exists
fs.mkdirSync(folder, { recursive: true })
// write file
fs.writeFileSync(defFilePath, def)
return defFilePath
}
function codegen() {
const outputs: string[] = []
commandDefines.forEach((command) => {
outputs.push(generateDef(command, defFolder))
outputs.push(generateDocs(command, docsTargetFolder))
})
outputs.forEach((output) => {
execSync(`yarn prettier -w ${output}`)
})
}
codegen()

31
cli/docs/artifacts.md Normal file
View file

@ -0,0 +1,31 @@
# Artifacts
> This file is generated by cli/codegen. Do not edit this file manually.
Copy artifacts from Github Actions into npm packages and ready to publish
## Usage
```sh
# CLI
napi artifacts [--options]
```
```typescript
// Programatically
import { NapiCli } from '@napi-rs/cli'
new NapiCli().artifacts({
// options
})
```
## Options
| Options | CLI Options | type | required | default | description |
| --------------- | ------------------- | ------ | -------- | -------------- | ------------------------------------------------------------------------------------------------------------------ |
| | --help,-h | | | | get help |
| cwd | --cwd | string | false | process.cwd() | The working directory of where napi command will be executed in, all other paths options are relative to this path |
| packageJsonPath | --package-json-path | string | false | 'package.json' | Path to `package.json` |
| outputDir | --output-dir,-o | string | false | './' | Path to the folder where all built `.node` files put, same as `--output-dir` of build command |
| npmDir | --npm-dir | string | false | 'npm' | Path to the folder where the npm packages put |

50
cli/docs/build.md Normal file
View file

@ -0,0 +1,50 @@
# Build
> This file is generated by cli/codegen. Do not edit this file manually.
Build the napi-rs project
## Usage
```sh
# CLI
napi build [--options]
```
```typescript
// Programatically
import { NapiCli } from '@napi-rs/cli'
new NapiCli().build({
// options
})
```
## Options
| Options | CLI Options | type | required | default | description |
| ----------------- | --------------------- | -------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------- |
| | --help,-h | | | | get help |
| target | --target,-t | string | false | | Build for the target triple, bypassed to `cargo build --target` |
| cwd | --cwd | string | false | | The working directory of where napi command will be executed in, all other paths options are relative to this path |
| manifestPath | --manifest-path | string | false | | Path to `Cargo.toml` |
| packageJsonPath | --package-json-path | string | false | | Path to `package.json` |
| targetDir | --target-dir | string | false | | Directory for all crate generated artifacts, see `cargo build --target-dir` |
| outputDir | --output-dir,-o | string | false | | Path to where all the built files would be put. Default to the crate folder |
| platform | --platform | boolean | false | | Add platform triple to the generated nodejs binding file, eg: `[name].linux-x64-gnu.node` |
| jsPackageName | --js-package-name | string | false | | Package name in generated js binding file. Only works with `--platform` flag |
| jsBinding | --js | string | false | | Path and filename of generated JS binding file. Only works with `--platform` flag. Relative to `--output_dir`. |
| noJsBinding | --no-js | boolean | false | | Whether to disable the generation JS binding file. Only works with `--platform` flag. |
| dts | --dts | string | false | | Path and filename of generated type def file. Relative to `--output_dir` |
| dtsHeader | --dts-header | string | false | | Custom file header for generated type def file. Only works when `typedef` feature enabled. |
| noDtsHeader | --no-dts-header | boolean | false | | Whether to disable the default file header for generated type def file. Only works when `typedef` feature enabled. |
| strip | --strip,-s | boolean | false | | Whether strip the library to achieve the minimum file size |
| release | --release,-r | boolean | false | | Build in release mode |
| verbose | --verbose,-v | boolean | false | | Verbosely log build command trace |
| bin | --bin | string | false | | Build only the specified binary |
| package | --package,-p | string | false | | Build the specified library or the one at cwd |
| crossCompile | --cross-compile,-x | boolean | false | | [experimental] cross-compile for the specified target with `cargo-xwin` on windows and `cargo-zigbuild` on other platform |
| watch | --watch,-w | boolean | false | | watch the crate changes and build continiously with `cargo-watch` crates |
| features | --features,-F | string[] | false | | Space-separated list of features to activate |
| allFeatures | --all-features | boolean | false | | Activate all available features |
| noDefaultFeatures | --no-default-features | boolean | false | | Do not activate the `default` feature |

View file

@ -0,0 +1,31 @@
# Create Npm Dirs
> This file is generated by cli/codegen. Do not edit this file manually.
Create npm package dirs for different platforms
## Usage
```sh
# CLI
napi create-npm-dirs [--options]
```
```typescript
// Programatically
import { NapiCli } from '@napi-rs/cli'
new NapiCli().createNpmDirs({
// options
})
```
## Options
| Options | CLI Options | type | required | default | description |
| --------------- | ------------------- | ------- | -------- | -------------- | ------------------------------------------------------------------------------------------------------------------ |
| | --help,-h | | | | get help |
| cwd | --cwd | string | false | process.cwd() | The working directory of where napi command will be executed in, all other paths options are relative to this path |
| packageJsonPath | --package-json-path | string | false | 'package.json' | Path to `package.json` |
| npmDir | --npm-dir | string | false | 'npm' | Path to the folder where the npm packages put |
| dryRun | --dry-run | boolean | false | false | Dry run without touching file system |

37
cli/docs/new.md Normal file
View file

@ -0,0 +1,37 @@
# New
> This file is generated by cli/codegen. Do not edit this file manually.
Create a new project with pre-configured boilerplate
## Usage
```sh
# CLI
napi new <path> [--options]
```
```typescript
// Programatically
import { NapiCli } from '@napi-rs/cli'
new NapiCli().new({
// options
})
```
## Options
| Options | CLI Options | type | required | default | description |
| -------------------- | ------------------------ | -------- | -------- | ------- | -------------------------------------------------------------------------------- |
| | --help,-h | | | | get help |
| path | <path> | true | 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 |
| 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 |
| dryRun | --dry-run | boolean | false | false | Whether to run the command in dry-run mode |

35
cli/docs/pre-publish.md Normal file
View file

@ -0,0 +1,35 @@
# Pre Publish
> This file is generated by cli/codegen. Do not edit this file manually.
Update package.json and copy addons into per platform packages
## Usage
```sh
# CLI
napi pre-publish [--options]
```
```typescript
// Programatically
import { NapiCli } from '@napi-rs/cli'
new NapiCli().prePublish({
// options
})
```
## Options
| Options | CLI Options | type | required | default | description |
| --------------- | ------------------- | ---------------- | -------- | -------------- | ------------------------------------------------------------------------------------------------------------------ |
| | --help,-h | | | | get help |
| cwd | --cwd | string | false | process.cwd() | The working directory of where napi command will be executed in, all other paths options are relative to this path |
| packageJsonPath | --package-json-path | string | false | 'package.json' | Path to `package.json` |
| npmDir | --npm-dir | string | false | 'npm' | Path to the folder where the npm packages put |
| tagStyle | --tag-style | 'npm' \| 'lerna' | false | 'lerna' | git tag style, `npm` or `lerna` |
| ghRelease | --gh-release | boolean | false | true | Whether create GitHub release |
| ghReleaseName | --gh-release-name | string | false | | GitHub release name |
| ghReleaseId | --gh-release-id | string | false | | Existing GitHub release id |
| dryRun | --dry-run | boolean | false | false | Dry run without touching file system |

36
cli/docs/rename.md Normal file
View file

@ -0,0 +1,36 @@
# Rename
> This file is generated by cli/codegen. Do not edit this file manually.
Rename the napi-rs project
## Usage
```sh
# CLI
napi rename [--options]
```
```typescript
// Programatically
import { NapiCli } from '@napi-rs/cli'
new NapiCli().rename({
// options
})
```
## Options
| Options | CLI Options | type | required | default | description |
| --------------- | ------------------- | ------ | -------- | -------------- | ------------------------------------------------------------------------------------------------------------------ |
| | --help,-h | | | | get help |
| cwd | --cwd | string | false | process.cwd() | The working directory of where napi command will be executed in, all other paths options are relative to this path |
| packageJsonPath | --package-json-path | string | false | 'package.json' | Path to `package.json` |
| npmDir | --npm-dir | string | false | 'npm' | Path to the folder where the npm packages put |
| name | --name,-n | string | false | | The new name of the project |
| binaryName | --binary-name,-b | string | false | | The new binary name \*.node files |
| packageName | --package-name | string | false | | The new package name of the project |
| manifestPath | --manifest-path | string | false | 'Cargo.toml' | Path to `Cargo.toml` |
| repository | --repository | string | false | | The new repository of the project |
| description | --description | string | false | | The new description of the project |

30
cli/docs/universalize.md Normal file
View file

@ -0,0 +1,30 @@
# Universalize
> This file is generated by cli/codegen. Do not edit this file manually.
Combile built binaries into one universal binary
## Usage
```sh
# CLI
napi universalize [--options]
```
```typescript
// Programatically
import { NapiCli } from '@napi-rs/cli'
new NapiCli().universalize({
// options
})
```
## Options
| Options | CLI Options | type | required | default | description |
| --------------- | ------------------- | ------ | -------- | -------------- | ------------------------------------------------------------------------------------------------------------------ |
| | --help,-h | | | | get help |
| cwd | --cwd | string | false | process.cwd() | The working directory of where napi command will be executed in, all other paths options are relative to this path |
| packageJsonPath | --package-json-path | string | false | 'package.json' | Path to `package.json` |
| outputDir | --output-dir,-o | string | false | './' | Path to the folder where all built `.node` files put, same as `--output-dir` of build command |

30
cli/docs/version.md Normal file
View file

@ -0,0 +1,30 @@
# Version
> This file is generated by cli/codegen. Do not edit this file manually.
Update version in created npm packages
## Usage
```sh
# CLI
napi version [--options]
```
```typescript
// Programatically
import { NapiCli } from '@napi-rs/cli'
new NapiCli().version({
// options
})
```
## Options
| Options | CLI Options | type | required | default | description |
| --------------- | ------------------- | ------ | -------- | -------------- | ------------------------------------------------------------------------------------------------------------------ |
| | --help,-h | | | | get help |
| cwd | --cwd | string | false | process.cwd() | The working directory of where napi command will be executed in, all other paths options are relative to this path |
| packageJsonPath | --package-json-path | string | false | 'package.json' | Path to `package.json` |
| npmDir | --npm-dir | string | false | 'npm' | Path to the folder where the npm packages put |

11
cli/esbuild.mjs Normal file
View file

@ -0,0 +1,11 @@
import * as esbuild from 'esbuild'
await esbuild.build({
entryPoints: ['./dist/index.js'],
outfile: './dist/index.cjs',
bundle: true,
platform: 'node',
define: {
'import.meta.url': '__filename',
},
})

View file

@ -2,30 +2,57 @@
"name": "@napi-rs/cli",
"version": "2.15.2",
"description": "Cli tools for napi-rs",
"author": "LongYinan <lynweklm@gmail.com>",
"homepage": "https://github.com/napi-rs/napi-rs",
"license": "MIT",
"type": "module",
"engines": {
"node": ">= 16"
},
"bin": {
"napi": "./dist/cli.js",
"napi-raw": "./cli.mjs"
},
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"exports": {
".": {
"import": {
"default": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"require": {
"default": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"./package.json": {
"import": "./package.json",
"require": "./package.json"
}
},
"files": [
"dist",
"src"
],
"keywords": [
"cli",
"rust",
"napi",
"n-api",
"node-api",
"node-addon",
"neon"
],
"author": "LongYinan <lynweklm@gmail.com>",
"homepage": "https://github.com/napi-rs/napi-rs",
"license": "MIT",
"bin": {
"napi": "./scripts/index.js"
},
"files": [
"scripts"
],
"engines": {
"node": ">= 10"
},
"maintainers": [
{
"name": "LongYinan",
"email": "lynweklm@gmail.com",
"homepage": "https://github.com/Brooooooklyn"
},
{
"name": "forehalo",
"homepage": "https://github.com/forehalo"
}
],
"repository": {
@ -39,26 +66,34 @@
"bugs": {
"url": "https://github.com/napi-rs/napi-rs/issues"
},
"dependencies": {
"@octokit/rest": "^19.0.7",
"clipanion": "^3.2.0",
"colorette": "^2.0.19",
"debug": "^4.3.4",
"inquirer": "^9.1.5",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.21",
"typanion": "^3.12.1"
},
"devDependencies": {
"@octokit/rest": "^19.0.5",
"@types/inquirer": "^9.0.3",
"@types/js-yaml": "^4.0.5",
"@types/lodash-es": "^4.17.6",
"clipanion": "^3.1.0",
"colorette": "^2.0.19",
"core-js": "^3.27.1",
"debug": "^4.3.4",
"env-paths": "^3.0.0",
"fdir": "^5.3.0",
"inquirer": "^9.1.4",
"js-yaml": "^4.1.0",
"lodash-es": "4.17.21",
"toml": "^3.0.0",
"tslib": "^2.4.1",
"typanion": "^3.12.1"
"ava": "^5.2.0",
"esbuild": "^0.17.14",
"prettier": "^2.8.7",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"scripts": {
"codegen": "node --loader ts-node/esm/transpile-only ./codegen/index.ts",
"build": "tsc && yarn build:cjs",
"build:cjs": "node ./esbuild.mjs",
"test": "ava"
}
}

View file

@ -1,132 +0,0 @@
import test from 'ava'
import { parseTriple } from '../parse-triple'
const triples = [
{
name: 'x86_64-unknown-linux-musl',
expected: {
abi: 'musl',
arch: 'x64',
platform: 'linux',
platformArchABI: 'linux-x64-musl',
raw: 'x86_64-unknown-linux-musl',
} as const,
},
{
name: 'x86_64-unknown-linux-gnu',
expected: {
abi: 'gnu',
arch: 'x64',
platform: 'linux',
platformArchABI: 'linux-x64-gnu',
raw: 'x86_64-unknown-linux-gnu',
} as const,
},
{
name: 'x86_64-pc-windows-msvc',
expected: {
abi: 'msvc',
arch: 'x64',
platform: 'win32',
platformArchABI: 'win32-x64-msvc',
raw: 'x86_64-pc-windows-msvc',
} as const,
},
{
name: 'x86_64-apple-darwin',
expected: {
abi: null,
arch: 'x64',
platform: 'darwin',
platformArchABI: 'darwin-x64',
raw: 'x86_64-apple-darwin',
} as const,
},
{
name: 'i686-pc-windows-msvc',
expected: {
abi: 'msvc',
arch: 'ia32',
platform: 'win32',
platformArchABI: 'win32-ia32-msvc',
raw: 'i686-pc-windows-msvc',
} as const,
},
{
name: 'x86_64-unknown-freebsd',
expected: {
abi: null,
arch: 'x64',
platform: 'freebsd',
platformArchABI: 'freebsd-x64',
raw: 'x86_64-unknown-freebsd',
} as const,
},
{
name: 'aarch64-unknown-linux-gnu',
expected: {
abi: 'gnu',
arch: 'arm64',
platform: 'linux',
platformArchABI: 'linux-arm64-gnu',
raw: 'aarch64-unknown-linux-gnu',
} as const,
},
{
name: 'aarch64-pc-windows-msvc',
expected: {
abi: 'msvc',
arch: 'arm64',
platform: 'win32',
platformArchABI: 'win32-arm64-msvc',
raw: 'aarch64-pc-windows-msvc',
} as const,
},
{
name: 'armv7-unknown-linux-gnueabihf',
expected: {
abi: 'gnueabihf',
arch: 'arm',
platform: 'linux',
platformArchABI: 'linux-arm-gnueabihf',
raw: 'armv7-unknown-linux-gnueabihf',
} as const,
},
{
name: 'aarch64-linux-android',
expected: {
abi: null,
arch: 'arm64',
platform: 'android',
platformArchABI: 'android-arm64',
raw: 'aarch64-linux-android',
},
} as const,
{
name: 'armv7-linux-androideabi',
expected: {
abi: 'eabi',
arch: 'arm',
platform: 'android',
platformArchABI: 'android-arm-eabi',
raw: 'armv7-linux-androideabi',
},
} as const,
{
name: 'universal-apple-darwin',
expected: {
abi: null,
arch: 'universal',
platform: 'darwin',
platformArchABI: 'darwin-universal',
raw: 'universal-apple-darwin',
},
} as const,
]
for (const triple of triples) {
test(`should parse ${triple.name}`, (t) => {
t.deepEqual(parseTriple(triple.name), triple.expected)
})
}

93
cli/src/api/artifacts.ts Normal file
View file

@ -0,0 +1,93 @@
import { join, parse, resolve } from 'path'
import * as colors from 'colorette'
import {
applyDefaultArtifactsOptions,
ArtifactsOptions,
} from '../def/artifacts.js'
import {
readNapiConfig,
debugFactory,
readFileAsync,
writeFileAsync,
UniArchsByPlatform,
readdirAsync,
} from '../utils/index.js'
const debug = debugFactory('artifacts')
export async function collectArtifacts(userOptions: ArtifactsOptions) {
const options = applyDefaultArtifactsOptions(userOptions)
const packageJsonPath = resolve(options.cwd, options.packageJsonPath)
const { targets, binaryName } = await readNapiConfig(packageJsonPath)
const distDirs = targets.map((platform) =>
resolve(options.cwd, options.npmDir, platform.platformArchABI),
)
const universalSourceBins = new Set(
targets
.filter((platform) => platform.arch === 'universal')
.flatMap((p) =>
UniArchsByPlatform[p.platform]?.map((a) => `${p.platform}-${a}`),
)
.filter(Boolean) as string[],
)
await collectNodeBinaries(resolve(options.cwd, options.outputDir)).then(
(output) =>
Promise.all(
output.map(async (filePath) => {
debug.info(`Read [${colors.yellowBright(filePath)}]`)
const sourceContent = await readFileAsync(filePath)
const parsedName = parse(filePath)
const [_binaryName, platformArchABI] = parsedName.name.split('.')
if (_binaryName !== binaryName) {
debug.warn(
`[${_binaryName}] is not matched with [${binaryName}], skip`,
)
return
}
const dir = distDirs.find((dir) => dir.includes(platformArchABI))
if (!dir && universalSourceBins.has(platformArchABI)) {
debug.warn(
`[${platformArchABI}] has no dist dir but it is source bin for universal arch, skip`,
)
return
}
if (!dir) {
throw new Error(`No dist dir found for ${filePath}`)
}
const distFilePath = join(dir, parsedName.base)
debug.info(
`Write file content to [${colors.yellowBright(distFilePath)}]`,
)
await writeFileAsync(distFilePath, sourceContent)
const distFilePathLocal = join(
parse(packageJsonPath).dir,
parsedName.base,
)
debug.info(
`Write file content to [${colors.yellowBright(distFilePathLocal)}]`,
)
await writeFileAsync(distFilePathLocal, sourceContent)
}),
),
)
}
async function collectNodeBinaries(root: string) {
const files = await readdirAsync(root, { withFileTypes: true })
const nodeBinaries = files
.filter((file) => file.isFile() && file.name.endsWith('.node'))
.map((file) => join(root, file.name))
const dirs = files.filter((file) => file.isDirectory())
for (const dir of dirs) {
nodeBinaries.push(...(await collectNodeBinaries(join(root, dir.name))))
}
return nodeBinaries
}

549
cli/src/api/build.ts Normal file
View file

@ -0,0 +1,549 @@
import { spawn } from 'child_process'
import { createHash } from 'crypto'
import { tmpdir } from 'os'
import { parse, join, resolve } from 'path'
import * as colors from 'colorette'
import { BuildOptions as RawBuildOptions } from '../def/build.js'
import {
CLI_VERSION,
copyFileAsync,
Crate,
debugFactory,
DEFAULT_TYPE_DEF_HEADER,
fileExists,
getSystemDefaultTarget,
getTargetLinker,
mkdirAsync,
NapiConfig,
parseMetadata,
parseTriple,
processTypeDef,
readNapiConfig,
Target,
targetToEnvVar,
tryInstallCargoBinary,
unlinkAsync,
writeFileAsync,
} from '../utils/index.js'
import { createJsBinding } from './templates/index.js'
const debug = debugFactory('build')
type OutputKind = 'js' | 'dts' | 'node' | 'exe'
type Output = {
kind: OutputKind
path: string
}
type BuildOptions = RawBuildOptions & {
cargoOptions?: string[]
}
export async function buildProject(options: BuildOptions) {
debug('napi build command receive options: %O', options)
const cwd = options.cwd ?? process.cwd()
const resolvePath = (...paths: string[]) => resolve(cwd, ...paths)
const manifestPath = resolvePath(options.manifestPath ?? 'Cargo.toml')
const metadata = parseMetadata(manifestPath)
const pkg = metadata.packages.find((p) => {
// package with given name
if (options.package) {
return p.name === options.package
} else {
return p.manifest_path === manifestPath
}
})
if (!pkg) {
throw new Error(
'Unable to find crate to build. It seems you are trying to build a crate in a workspace, try using `--package` option to specify the package to build.',
)
}
const crateDir = parse(pkg.manifest_path).dir
const builder = new Builder(
options,
pkg,
cwd,
options.target
? parseTriple(options.target)
: process.env.CARGO_BUILD_TARGET
? parseTriple(process.env.CARGO_BUILD_TARGET)
: getSystemDefaultTarget(),
crateDir,
resolvePath(options.outputDir ?? crateDir),
options.targetDir ??
process.env.CARGO_BUILD_TARGET_DIR ??
metadata.target_directory,
await readNapiConfig(
resolvePath(options.packageJsonPath ?? 'package.json'),
),
)
return builder.build()
}
class Builder {
private readonly args: string[] = []
private readonly envs: Record<string, string> = {}
private readonly outputs: Output[] = []
constructor(
private readonly options: BuildOptions,
private readonly crate: Crate,
private readonly cwd: string,
private readonly target: Target,
private readonly crateDir: string,
private readonly outputDir: string,
private readonly targetDir: string,
private readonly config: NapiConfig,
) {}
get cdyLibName() {
return this.crate.targets.find((t) => t.crate_types.includes('cdylib'))
?.name
}
get binName() {
return (
this.options.bin ??
// only available if not cdylib or bin name specified
(this.cdyLibName
? null
: this.crate.targets.find((t) => t.crate_types.includes('bin'))?.name)
)
}
build() {
if (!this.cdyLibName) {
const warning =
'Missing `crate-type = ["cdylib"]` in [lib] config. The build result will not be available as node addon.'
if (this.binName) {
debug.warn(warning)
} else {
throw new Error(warning)
}
}
return this.pickBinary()
.setPackage()
.setFeatures()
.setTarget()
.setEnvs()
.setBypassArgs()
.exec()
}
private exec() {
debug(`Start building crate: ${this.crate.name}`)
debug(' %i', `cargo ${this.args.join(' ')}`)
const controller = new AbortController()
const buildTask = new Promise<void>((resolve, reject) => {
const buildProcess = spawn('cargo', this.args, {
env: {
...process.env,
...this.envs,
},
stdio: 'inherit',
cwd: this.cwd,
signal: controller.signal,
})
buildProcess.once('exit', (code) => {
if (code === 0) {
debug('%i', `Build crate ${this.crate.name} successfully!`)
resolve()
} else {
reject(new Error(`Build failed with exit code ${code}`))
}
})
buildProcess.once('error', (e) => {
reject(
new Error(`Build failed with error: ${e.message}`, {
cause: e,
}),
)
})
})
return {
task: buildTask.then(() => this.postBuild()),
abort: () => controller.abort(),
}
}
private pickBinary() {
let set = false
if (this.options.watch) {
if (process.env.CI) {
debug.warn('Watch mode is not supported in CI environment')
} else {
debug('Use %i', 'cargo-watch')
tryInstallCargoBinary('cargo-watch', 'watch')
// yarn napi watch --target x86_64-unknown-linux-gnu [--cross-compile]
// ===>
// cargo watch [...] -- build --target x86_64-unknown-linux-gnu
// cargo watch [...] -- zigbuild --target x86_64-unknown-linux-gnu
this.args.push(
'watch',
'--why',
'-i',
'*.{js,ts,node}',
'-w',
this.crateDir,
'--',
'cargo',
)
set = true
}
}
if (this.options.crossCompile) {
if (this.target.platform === 'win32') {
if (process.platform === 'win32') {
debug.warn(
'You are trying to cross compile to win32 platform on win32 platform which is unnecessary.',
)
} else {
// use cargo-xwin to cross compile to win32 platform
debug('Use %i', 'cargo-xwin')
tryInstallCargoBinary('cargo-xwin', 'xwin')
this.args.push('xwin', 'build')
if (this.target.arch === 'ia32') {
this.envs.XWIN_ARCH = 'x86'
}
set = true
}
} else {
if (
this.target.platform === 'linux' &&
process.platform === 'linux' &&
this.target.arch === process.arch &&
(function (abi: string | null) {
const glibcVersionRuntime =
// @ts-expect-error
process.report?.getReport()?.header?.glibcVersionRuntime
const libc = glibcVersionRuntime ? 'gnu' : 'musl'
return abi === libc
})(this.target.abi)
) {
debug.warn(
'You are trying to cross compile to linux target on linux platform which is unnecessary.',
)
} else if (
this.target.platform === 'darwin' &&
process.platform === 'darwin'
) {
debug.warn(
'You are trying to cross compile to darwin target on darwin platform which is unnecessary.',
)
} else {
// use cargo-zigbuild to cross compile to other platforms
debug('Use %i', 'cargo-zigbuild')
tryInstallCargoBinary('cargo-zigbuild', 'zigbuild')
this.args.push('zigbuild')
set = true
}
}
}
if (!set) {
this.args.push('build')
}
return this
}
private setPackage() {
const args = []
if (this.options.package) {
args.push('--package', this.options.package)
}
if (this.binName) {
args.push('--bin', this.binName)
}
if (args.length) {
debug('Set package flags: ')
debug(' %O', args)
this.args.push(...args)
}
return this
}
private setTarget() {
debug('Set compiling target to: ')
debug(' %i', this.target.triple)
this.args.push('--target', this.target.triple)
return this
}
private setEnvs() {
// type definition intermediate file
this.envs.TYPE_DEF_TMP_PATH = this.getIntermediateTypeFile()
this.envs.CARGO_CFG_NAPI_RS_CLI_VERSION = CLI_VERSION
// RUSTFLAGS
let rustflags =
process.env.RUSTFLAGS ?? process.env.CARGO_BUILD_RUSTFLAGS ?? ''
if (
this.target.abi?.includes('musl') &&
!rustflags.includes('target-feature=-crt-static')
) {
rustflags += ' -C target-feature=-crt-static'
}
if (this.options.strip && !rustflags.includes('link-arg=-s')) {
rustflags += ' -C link-arg=-s'
}
if (rustflags.length) {
this.envs.RUSTFLAGS = rustflags
}
// END RUSTFLAGS
// LINKER
const linker = getTargetLinker(this.target.triple)
if (
linker &&
!process.env.RUSTC_LINKER &&
!process.env[`CARGET_TARGET_${targetToEnvVar(this.target.triple)}_LINKER`]
) {
this.envs.RUSTC_LINKER = linker
}
if (this.target.platform === 'android') {
const { ANDROID_NDK_LATEST_HOME } = process.env
if (!ANDROID_NDK_LATEST_HOME) {
debug.warn(
`${colors.red(
'ANDROID_NDK_LATEST_HOME',
)} environment variable is missing`,
)
}
const targetArch = this.target.arch === 'arm' ? 'armv7a' : 'aarch64'
const targetPlatform =
this.target.arch === 'arm' ? 'androideabi24' : 'android24'
Object.assign(this.envs, {
CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/${targetArch}-linux-android24-clang`,
CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/${targetArch}-linux-androideabi24-clang`,
CC: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/${targetArch}-linux-${targetPlatform}-clang`,
CXX: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/${targetArch}-linux-${targetPlatform}-clang++`,
AR: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar`,
ANDROID_NDK: ANDROID_NDK_LATEST_HOME,
PATH: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin:${process.env.PATH}`,
})
}
// END LINKER
debug('Set envs: ')
Object.entries(this.envs).forEach(([k, v]) => {
debug(' %i', `${k}=${v}`)
})
return this
}
private setFeatures() {
const args = []
if (this.options.allFeatures) {
args.push('--all-features')
} else if (this.options.noDefaultFeatures) {
args.push('--no-default-features')
} else if (this.options.features) {
args.push('--features', ...this.options.features)
}
debug('Set features flags: ')
debug(' %O', args)
this.args.push(...args)
return this
}
private setBypassArgs() {
if (this.options.release) {
this.args.push('--release')
}
if (this.options.verbose) {
this.args.push('--verbose')
}
if (this.options.targetDir) {
this.args.push('--target-dir', this.options.targetDir)
}
if (this.options.cargoOptions?.length) {
this.args.push(...this.options.cargoOptions)
}
return this
}
private getIntermediateTypeFile() {
return join(
tmpdir(),
`${this.crate.name}-${createHash('sha256')
.update(this.crate.manifest_path)
.update(CLI_VERSION)
.digest('hex')
.substring(0, 8)}.napi_type_def.tmp`,
)
}
private async postBuild() {
try {
debug(`Try to create output directory:`)
debug(' %i', this.outputDir)
await mkdirAsync(this.outputDir, { recursive: true })
debug(`Output directory created`)
} catch (e) {
throw new Error(`Failed to create output directory ${this.outputDir}`, {
cause: e,
})
}
await this.copyArtifact()
// only for cdylib
if (this.cdyLibName) {
await this.generateTypeDef()
await this.writeJsBinding()
}
return this.outputs
}
private async copyArtifact() {
const [srcName, destName] = this.getArtifactNames()
if (!srcName || !destName) {
return
}
const src = join(
this.targetDir,
this.target.triple,
this.options.release ? 'release' : 'debug',
srcName,
)
const dest = join(this.outputDir, destName)
try {
if (await fileExists(dest)) {
debug('Old artifact found, remove it first')
await unlinkAsync(dest)
}
debug('Copy artifact to:')
debug(' %i', dest)
await copyFileAsync(src, dest)
this.outputs.push({
kind: dest.endsWith('.node') ? 'node' : 'exe',
path: dest,
})
} catch (e) {
throw new Error('Failed to copy artifact', {
cause: e,
})
}
}
private getArtifactNames() {
if (this.cdyLibName) {
const cdyLib = this.cdyLibName.replace(/-/g, '_')
const srcName =
this.target.platform === 'darwin'
? `lib${cdyLib}.dylib`
: this.target.platform === 'win32'
? `${cdyLib}.dll`
: `lib${cdyLib}.so`
let destName = this.config.binaryName
// add platform suffix to binary name
// index[.linux-x64-gnu].node
// ^^^^^^^^^^^^^^
if (this.options.platform) {
destName += `.${this.target.platformArchABI}`
}
destName += '.node'
return [srcName, destName]
} else if (this.binName) {
const srcName =
this.target.platform === 'win32' ? `${this.binName}.exe` : this.binName
return [srcName, srcName]
}
return []
}
private async generateTypeDef() {
if (!(await fileExists(this.envs.TYPE_DEF_TMP_PATH))) {
return
}
const dest = join(this.outputDir, this.options.dts ?? 'index.d.ts')
const dts = await processTypeDef(
this.envs.TYPE_DEF_TMP_PATH,
!this.options.noDtsHeader
? this.options.dtsHeader ?? DEFAULT_TYPE_DEF_HEADER
: '',
)
try {
debug('Writing type def to:')
debug(' %i', dest)
await writeFileAsync(dest, dts, 'utf-8')
this.outputs.push({
kind: 'dts',
path: dest,
})
} catch (e) {
debug.error('Failed to write type def file')
debug.error(e as Error)
}
}
private async writeJsBinding() {
if (!this.options.platform || this.options.noJsBinding) {
return
}
const dest = join(this.outputDir, this.options.jsBinding ?? 'index.js')
const js = createJsBinding(this.config.binaryName, this.config.packageName)
try {
debug('Writing js binding to:')
debug(' %i', dest)
await writeFileAsync(dest, js, 'utf-8')
this.outputs.push({
kind: 'js',
path: dest,
})
} catch (e) {
throw new Error('Failed to write js binding file', { cause: e })
}
}
}

View file

@ -0,0 +1,105 @@
import { join, resolve } from 'path'
import {
applyDefaultCreateNpmDirsOptions,
CreateNpmDirsOptions,
} from '../def/create-npm-dirs.js'
import {
debugFactory,
readNapiConfig,
mkdirAsync as rawMkdirAsync,
pick,
writeFileAsync as rawWriteFileAsync,
Target,
} from '../utils/index.js'
const debug = debugFactory('create-npm-dirs')
export async function createNpmDirs(userOptions: CreateNpmDirsOptions) {
const options = applyDefaultCreateNpmDirsOptions(userOptions)
async function mkdirAsync(dir: string) {
debug('Try to create dir: %i', dir)
if (options.dryRun) {
return
}
await rawMkdirAsync(dir, {
recursive: true,
})
}
async function writeFileAsync(file: string, content: string) {
debug('Writing file %i', file)
if (options.dryRun) {
debug(content)
return
}
await rawWriteFileAsync(file, content)
}
const packageJsonPath = resolve(options.cwd, options.packageJsonPath)
const npmPath = resolve(options.cwd, options.npmDir)
debug(`Read content from [${packageJsonPath}]`)
const { targets, binaryName, packageName, packageJson } =
await readNapiConfig(packageJsonPath)
for (const target of targets) {
const targetDir = join(npmPath, `${target.platformArchABI}`)
await mkdirAsync(targetDir)
const binaryFileName = `${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],
...pick(
packageJson,
'description',
'keywords',
'author',
'authors',
'homepage',
'license',
'engines',
'publishConfig',
'repository',
'bugs',
),
}
// Only works with yarn 3.1+
// https://github.com/yarnpkg/berry/pull/3981
if (target.abi === 'gnu') {
// @ts-expect-error
scopedPackageJson.libc = ['glibc']
} else if (target.abi === 'musl') {
// @ts-expect-error
scopedPackageJson.libc = ['musl']
}
const targetPackageJson = join(targetDir, 'package.json')
await writeFileAsync(
targetPackageJson,
JSON.stringify(scopedPackageJson, null, 2),
)
const targetReadme = join(targetDir, 'README.md')
await writeFileAsync(targetReadme, readme(packageName, target))
debug.info(`${packageName}-${target.platformArchABI} created`)
}
}
function readme(packageName: string, target: Target) {
return `# \`${packageName}-${target.platformArchABI}\`
This is the **${target.triple}** binary for \`${packageName}\`
`
}

206
cli/src/api/new.ts Normal file
View file

@ -0,0 +1,206 @@
import path from 'path'
import {
applyDefaultNewOptions,
NewOptions as RawNewOptions,
} from '../def/new.js'
import {
AVAILABLE_TARGETS,
CLI_VERSION,
debugFactory,
DEFAULT_TARGETS,
mkdirAsync,
writeFileAsync,
} from '../utils/index.js'
import { napiEngineRequirement } from '../utils/version.js'
import {
createBuildRs,
createCargoToml,
createGithubActionsCIYml,
createLibRs,
createPackageJson,
gitIgnore,
npmIgnore,
} from './templates/index.js'
const debug = debugFactory('new')
interface Output {
target: string
content: string
}
type NewOptions = Required<RawNewOptions>
function processOptions(options: RawNewOptions) {
debug('Processing options...')
options.path = path.resolve(process.cwd(), options.path)
debug(`Resolved target path to: ${options.path}`)
if (!options.name) {
options.name = path.parse(options.path).base
debug(`No project name provided, fix it to dir name: ${options.name}`)
}
if (!options.targets?.length) {
if (options.enableAllTargets) {
options.targets = AVAILABLE_TARGETS.concat()
debug('Enable all targets')
} else if (options.enableDefaultTargets) {
options.targets = DEFAULT_TARGETS.concat()
debug('Enable default targets')
} else {
throw new Error('At least one target must be enabled')
}
}
return applyDefaultNewOptions(options) as NewOptions
}
export async function newProject(userOptions: RawNewOptions) {
debug('Will create napi-rs project with given options:')
debug(userOptions)
const options = processOptions(userOptions)
debug('Targets to be enabled:')
debug(options.targets)
const outputs = generateFiles(options)
try {
debug(`Try to create target directory: ${options.path}`)
if (!options.dryRun) {
await mkdirAsync(options.path, { recursive: true })
}
} catch (e) {
throw new Error(`Failed to create target directory: ${options.path}`, {
cause: e,
})
}
await dumpOutputs(outputs, options.dryRun)
debug(`Project created at: ${options.path}`)
}
function generateFiles(options: NewOptions): Output[] {
return [
generateCargoToml,
generateLibRs,
generateBuildRs,
generatePackageJson,
generateGithubWorkflow,
generateIgnoreFiles,
].flatMap((generator) => {
const output = generator(options)
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) }]
}
})
}
function generateCargoToml(options: NewOptions): Output {
return {
target: './Cargo.toml',
content: createCargoToml({
name: options.name,
license: options.license,
features: [`napi${options.minNodeApiVersion}`],
deriveFeatures: options.enableTypeDef ? ['type-def'] : [],
}),
}
}
function generateLibRs(_options: NewOptions): Output {
return {
target: './src/lib.rs',
content: createLibRs(),
}
}
function generateBuildRs(_options: NewOptions): Output {
return {
target: './build.rs',
content: createBuildRs(),
}
}
function generatePackageJson(options: NewOptions): Output {
return {
target: './package.json',
content: createPackageJson({
name: options.name,
binaryName: getBinaryName(options.name),
targets: options.targets,
license: options.license,
engineRequirement: napiEngineRequirement(options.minNodeApiVersion),
cliVersion: CLI_VERSION,
}),
}
}
function generateGithubWorkflow(options: NewOptions): Output | null {
if (!options.enableGithubActions) {
return null
}
return {
target: './.github/workflows/ci.yml',
content: createGithubActionsCIYml(
getBinaryName(options.name),
options.targets,
),
}
}
function generateIgnoreFiles(_options: NewOptions): Output[] {
return [
{
target: './.gitignore',
content: gitIgnore,
},
{
target: './.npmignore',
content: npmIgnore,
},
]
}
async function dumpOutputs(outputs: Output[], dryRun?: boolean) {
for (const output of outputs) {
if (!output) {
continue
}
debug(`Writing project file: ${output.target}`)
// only output content to logger instead of writing to file system
if (dryRun) {
debug(output.content)
continue
}
try {
await mkdirAsync(path.dirname(output.target), { recursive: true })
await writeFileAsync(output.target, output.content, 'utf-8')
} catch (e) {
throw new Error(`Failed to write file: ${output.target}`, { cause: e })
}
}
}
function getBinaryName(name: string): string {
return name.split('/').pop()!
}
export { NewOptions }

216
cli/src/api/pre-publish.ts Normal file
View file

@ -0,0 +1,216 @@
import { execSync } from 'child_process'
import { existsSync, statSync } from 'fs'
import { join, relative, resolve } from 'path'
import { Octokit } from '@octokit/rest'
import {
applyDefaultPrePublishOptions,
PrePublishOptions,
} from '../def/pre-publish.js'
import {
readFileAsync,
readNapiConfig,
debugFactory,
updatePackageJson,
} from '../utils/index.js'
import { version } from './version.js'
const debug = debugFactory('pre-publish')
interface PackageInfo {
name: string
version: string
tag: string
}
export async function prePublish(userOptions: PrePublishOptions) {
debug('Receive pre-publish options:')
debug(' %O', userOptions)
const options = applyDefaultPrePublishOptions(userOptions)
const packageJsonPath = relative(options.cwd, options.packageJsonPath)
const { packageJson, targets, packageName, binaryName, npmClient } =
await readNapiConfig(packageJsonPath)
async function createGhRelease(packageName: string, version: string) {
if (!options.ghRelease) {
return {
owner: null,
repo: null,
pkgInfo: { name: null, version: null, tag: null },
}
}
const { repo, owner, pkgInfo, octokit } = getRepoInfo(packageName, version)
if (!repo || !owner) {
return {
owner: null,
repo: null,
pkgInfo: { name: null, version: null, tag: null },
}
}
if (!options.dryRun) {
try {
await octokit.repos.createRelease({
owner,
repo,
tag_name: pkgInfo.tag,
name: options.ghReleaseName,
prerelease:
version.includes('alpha') ||
version.includes('beta') ||
version.includes('rc'),
})
} catch (e) {
debug(
`Params: ${JSON.stringify(
{ owner, repo, tag_name: pkgInfo.tag },
null,
2,
)}`,
)
console.error(e)
}
}
return { owner, repo, pkgInfo, octokit }
}
function getRepoInfo(packageName: string, version: string) {
const headCommit = execSync('git log -1 --pretty=%B', {
encoding: 'utf-8',
}).trim()
const { GITHUB_REPOSITORY } = process.env
if (!GITHUB_REPOSITORY) {
return {
owner: null,
repo: null,
pkgInfo: { name: null, version: null, tag: null },
}
}
debug(`Github repository: ${GITHUB_REPOSITORY}`)
const [owner, repo] = GITHUB_REPOSITORY.split('/')
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
})
let pkgInfo: PackageInfo | undefined
if (options.tagStyle === 'lerna') {
const packagesToPublish = headCommit
.split('\n')
.map((line) => line.trim())
.filter((line, index) => line.length && index)
.map((line) => line.substring(2))
.map(parseTag)
pkgInfo = packagesToPublish.find(
(pkgInfo) => pkgInfo.name === packageName,
)
if (!pkgInfo) {
throw new TypeError(
`No release commit found with ${packageName}, original commit info: ${headCommit}`,
)
}
} else {
pkgInfo = {
tag: `v${version}`,
version,
name: packageName,
}
}
return { owner, repo, pkgInfo, octokit }
}
if (!options.dryRun) {
await version(userOptions)
await updatePackageJson(packageJsonPath, {
optionalDependencies: targets.reduce((deps, target) => {
deps[`${packageName}-${target.platformArchABI}`] = packageJson.version
return deps
}, {} as Record<string, string>),
})
}
const { owner, repo, pkgInfo, octokit } = options.ghReleaseId
? getRepoInfo(packageName, packageJson.version)
: await createGhRelease(packageName, packageJson.version)
for (const target of targets) {
const pkgDir = resolve(
options.cwd,
options.npmDir,
`${target.platformArchABI}`,
)
const filename = `${binaryName}.${target.platformArchABI}.node`
const dstPath = join(pkgDir, filename)
if (!options.dryRun) {
if (!existsSync(dstPath)) {
debug.warn(`%s doesn't exist`, dstPath)
continue
}
execSync(`${npmClient} publish`, {
cwd: pkgDir,
env: process.env,
})
if (options.ghRelease && repo && owner) {
debug.info(`Creating GitHub release ${pkgInfo.tag}`)
try {
const releaseId = options.ghReleaseId
? Number(options.ghReleaseId)
: (
await octokit!.repos.getReleaseByTag({
repo: repo,
owner: owner,
tag: pkgInfo.tag,
})
).data.id
const dstFileStats = statSync(dstPath)
const assetInfo = await octokit!.repos.uploadReleaseAsset({
owner: owner,
repo: repo,
name: filename,
release_id: releaseId,
mediaType: { format: 'raw' },
headers: {
'content-length': dstFileStats.size,
'content-type': 'application/octet-stream',
},
data: await readFileAsync(dstPath, { encoding: 'utf-8' }),
})
debug.info(`GitHub release created`)
debug.info(`Download URL: %s`, assetInfo.data.browser_download_url)
} catch (e) {
debug.error(
`Param: ${JSON.stringify(
{ owner, repo, tag: pkgInfo.tag, filename: dstPath },
null,
2,
)}`,
)
debug.error(e)
}
}
}
}
}
function parseTag(tag: string) {
const segments = tag.split('@')
const version = segments.pop()!
const name = segments.join('@')
return {
name,
version,
tag,
}
}

51
cli/src/api/rename.ts Normal file
View file

@ -0,0 +1,51 @@
import { resolve } from 'path'
import { isNil, merge, omitBy, pick } from 'lodash-es'
import { applyDefaultRenameOptions, RenameOptions } from '../def/rename.js'
import { readFileAsync, writeFileAsync } from '../utils/index.js'
import { createNpmDirs } from './create-npm-dirs.js'
export async function renameProject(userOptions: RenameOptions) {
const options = applyDefaultRenameOptions(userOptions)
const packageJsonPath = resolve(options.cwd, options.packageJsonPath)
const cargoTomlPath = resolve(options.cwd, options.manifestPath)
const packageJsonContent = await readFileAsync(packageJsonPath, 'utf8')
const packageJsonData = JSON.parse(packageJsonContent)
merge(
packageJsonData,
omitBy(pick(options, ['name', 'description', 'author', 'license']), isNil),
{
napi: omitBy(
{
binaryName: options.binaryName,
packageName: options.packageName,
},
isNil,
),
},
)
await writeFileAsync(
packageJsonPath,
JSON.stringify(packageJsonData, null, 2),
)
let tomlContent = await readFileAsync(cargoTomlPath, 'utf8')
tomlContent = tomlContent.replace(
/name\s?=\s?"([\w+])"/,
`name = "${options.binaryName}"`,
)
await writeFileAsync(cargoTomlPath, tomlContent)
await createNpmDirs({
cwd: options.cwd,
packageJsonPath: options.packageJsonPath,
npmDir: options.npmDir,
dryRun: false,
})
}

View file

@ -1,4 +1,4 @@
export const GitIgnore = `# Created by https://www.toptal.com/developers/gitignore/api/node
export const gitIgnore = `# Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node
### Node ###

View file

@ -1,4 +1,4 @@
export const NPMIgnoreFiles = `target
export const npmIgnore = `target
Cargo.lock
.cargo
.github

View file

@ -0,0 +1,4 @@
export const createBuildRs = () => `fn main() {
napi_build::setup();
}
`

View file

@ -0,0 +1,35 @@
export const createCargoToml = ({
name,
license,
features,
deriveFeatures,
}: {
name: string
license: string
features: string[]
deriveFeatures: string[]
}) => `[package]
name = "${name.replace('@', '').replace('/', '_').toLowerCase()}"
version = "1.0.0"
edition = "2021"
license = "${license}"
[lib]
crate-type = ["cdylib"]
[dependencies.napi]
version = "2"
default-features = false
# see https://nodejs.org/api/n-api.html#node-api-version-matrix
features = ${JSON.stringify(features)}
[dependencies.napi-derive]
version = "2"
features = ${JSON.stringify(deriveFeatures)}
[build-dependencies]
napi-build = "2"
[profile.release]
lto = true
`

View file

@ -1,9 +1,8 @@
export const YAML = (app: string) => `
export const YAML = () => `
name: CI
env:
DEBUG: 'napi:*'
APP_NAME: '${app}'
MACOSX_DEPLOYMENT_TARGET: '10.13'
on:
@ -30,14 +29,14 @@ jobs:
- host: macos-latest
target: 'x86_64-apple-darwin'
build: |
yarn build
yarn build --platform
strip -x *.node
- host: windows-latest
build: yarn build
build: yarn build --platform
target: 'x86_64-pc-windows-msvc'
- host: windows-latest
build: |
yarn build --target i686-pc-windows-msvc
yarn build --platform --target i686-pc-windows-msvc
yarn test
target: 'i686-pc-windows-msvc'
- host: ubuntu-latest
@ -45,26 +44,26 @@ jobs:
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
build: >-
set -e &&\n
yarn build --target x86_64-unknown-linux-gnu &&\n
yarn build --platform --target x86_64-unknown-linux-gnu &&\n
strip *.node
- 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 &&
yarn build --platform &&
strip *.node
- host: macos-latest
target: 'aarch64-apple-darwin'
build: |
yarn build --target aarch64-apple-darwin
yarn build --platform --target aarch64-apple-darwin
strip -x *.node
- 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 --target aarch64-unknown-linux-gnu &&\n
yarn build --platform --target aarch64-unknown-linux-gnu &&\n
aarch64-unknown-linux-gnu-strip *.node
- host: ubuntu-latest
target: 'armv7-unknown-linux-gnueabihf'
@ -72,17 +71,17 @@ jobs:
sudo apt-get update
sudo apt-get install gcc-arm-linux-gnueabihf -y
build: |
yarn build --target armv7-unknown-linux-gnueabihf
yarn build --platform --target armv7-unknown-linux-gnueabihf
arm-linux-gnueabihf-strip *.node
- host: ubuntu-latest
target: 'aarch64-linux-android'
build: |
yarn build --target aarch64-linux-android
yarn build --platform --target aarch64-linux-android
\${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip *.node
- host: ubuntu-latest
target: 'armv7-linux-androideabi'
build: |
yarn build --target armv7-linux-androideabi
yarn build --platform --target armv7-linux-androideabi
\${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip *.node
- host: ubuntu-latest
target: 'aarch64-unknown-linux-musl'
@ -90,11 +89,11 @@ jobs:
build: >-
set -e &&\n
rustup target add aarch64-unknown-linux-musl &&\n
yarn build --target aarch64-unknown-linux-musl &&\n
yarn build --platform --target aarch64-unknown-linux-musl &&\n
/aarch64-linux-musl-cross/bin/aarch64-linux-musl-strip *.node
- host: windows-latest
target: 'aarch64-pc-windows-msvc'
build: yarn build --target aarch64-pc-windows-msvc
build: yarn build --platform --target aarch64-pc-windows-msvc
name: stable - \${{ matrix.settings.target }} - node@18
runs-on: \${{ matrix.settings.host }}
@ -172,7 +171,7 @@ jobs:
uses: actions/upload-artifact@v3
with:
name: bindings-\${{ matrix.settings.target }}
path: \${{ env.APP_NAME }}.*.node
path: "*.node"
if-no-files-found: error
build-freebsd:
@ -223,7 +222,7 @@ jobs:
uses: actions/upload-artifact@v3
with:
name: bindings-freebsd
path: \${{ env.APP_NAME }}.*.node
path: "*.node"
if-no-files-found: error
test-macOS-windows-binding:
@ -508,7 +507,7 @@ jobs:
uses: actions/upload-artifact@v3
with:
name: bindings-universal-apple-darwin
path: \${{ env.APP_NAME }}.*.node
path: "*.node"
if-no-files-found: error
publish:

View file

@ -1,8 +1,12 @@
import { load, dump } from 'js-yaml'
import { NodeArchToCpu, UniArchsByPlatform, parseTriple } from '../parse-triple'
import {
NodeArchToCpu,
UniArchsByPlatform,
parseTriple,
} from '../../utils/index.js'
import { YAML } from './ci-template'
import { YAML } from './ci-template.js'
const BUILD_FREEBSD = 'build-freebsd'
const TEST_MACOS_WINDOWS = 'test-macOS-windows-binding'
@ -29,7 +33,9 @@ export const createGithubActionsCIYml = (
return [t]
}),
)
const fullTemplate = load(YAML(binaryName)) as any
const fullTemplate = load(YAML()) as any
const requiredSteps = []
const enableWindowsX86 = allTargets.has('x86_64-pc-windows-msvc')
const enableMacOSX86 = allTargets.has('x86_64-apple-darwin')
@ -40,7 +46,6 @@ export const createGithubActionsCIYml = (
const enableLinuxArm7 = allTargets.has('armv7-unknown-linux-gnueabihf')
const enableFreeBSD = allTargets.has('x86_64-unknown-freebsd')
const enableMacOSUni = allTargets.has('universal-apple-darwin')
fullTemplate.env.APP_NAME = binaryName
fullTemplate.jobs.build.strategy.matrix.settings =
fullTemplate.jobs.build.strategy.matrix.settings.filter(
({ target }: { target: string }) => allTargets.has(target),

View file

@ -0,0 +1,8 @@
export * from './.gitignore.js'
export * from './.npmignore.js'
export * from './build.rs.js'
export * from './cargo.toml.js'
export * from './ci.yml.js'
export * from './lib.rs.js'
export * from './package.json.js'
export * from './js-binding.js'

View file

@ -0,0 +1,130 @@
/* eslint-disable @typescript-eslint/switch-exhaustiveness-check */
function loadNapiModule(binaryName: string, packageName: string) {
const { existsSync, readFileSync } = require('fs')
const { join } = require('path')
const { platform, arch } = process
const candidates: string[] = []
function isMusl() {
// For Node 10
if (!process.report || typeof process.report.getReport !== 'function') {
try {
const lddPath = require('child_process')
.execSync('which ldd')
.toString()
.trim()
return readFileSync(lddPath, 'utf8').includes('musl')
} catch (e) {
return true
}
} else {
// @ts-expect-error
const { glibcVersionRuntime } = process.report.getReport().header
return !glibcVersionRuntime
}
}
switch (platform) {
case 'android':
switch (arch) {
case 'arm64':
candidates.push('android-arm64')
break
case 'arm':
candidates.push('android-arm-eabi')
break
}
break
case 'win32':
switch (arch) {
case 'x64':
candidates.push('win32-x64-msvc')
break
case 'ia32':
candidates.push('win32-ia32-msvc')
break
case 'arm64':
candidates.push('win32-arm64-msvc')
break
}
break
case 'darwin':
candidates.push('darwin-universal')
switch (arch) {
case 'x64':
candidates.push('darwin-x64')
break
case 'arm64':
candidates.push('darwin-arm64')
break
}
break
case 'freebsd':
if (arch === 'x64') {
candidates.push('freebsd-x64')
}
break
case 'linux':
switch (arch) {
case 'x64':
if (isMusl()) {
candidates.push('linux-x64-musl')
} else {
candidates.push('linux-x64-gnu')
}
break
case 'arm64':
if (isMusl()) {
candidates.push('linux-arm64-musl')
} else {
candidates.push('linux-arm64-gnu')
}
break
case 'arm':
candidates.push('linux-arm-gnueabihf')
break
}
break
}
let nativeBinding: any
let loadError: any
for (const suffix of candidates) {
const localPath = join(__dirname, `${binaryName}.${suffix}.node`)
const pkgPath = `${packageName}-${suffix}`
try {
if (existsSync(localPath)) {
nativeBinding = require(localPath)
} else {
nativeBinding = require(pkgPath)
}
} catch (e) {
loadError = e
continue
}
loadError = null
break
}
if (!nativeBinding) {
if (loadError) {
throw loadError
}
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
}
return nativeBinding
}
export function createJsBinding(localName: string, pkgName: string): string {
return `${loadNapiModule.toString()}
module.exports = loadNapiModule('${localName}', '${pkgName}')
`
}

View file

@ -1,4 +1,4 @@
export const LibRs = `#![deny(clippy::all)]
export const createLibRs = () => `#![deny(clippy::all)]
#[macro_use]
extern crate napi_derive;

View file

@ -0,0 +1,44 @@
export const createPackageJson = ({
name,
binaryName,
targets,
license,
engineRequirement,
cliVersion,
}: {
name: string
binaryName: string
targets: string[]
license: string
engineRequirement: string
cliVersion: string
}) => {
return `{
"name": "${name}",
"version": "1.0.0",
"main": "index.js",
"types": "index.d.ts",
"license": "${license}",
"engines": {
"node": "${engineRequirement}"
},
"napi": {
"name": "${binaryName}",
"targets": [
${targets.map((t) => `"${t}"`).join(',\n ')}
]
},
"scripts": {
"test": "yarn build:debug --platform && node -e \\"assert(require('.').sum(1, 2) === 3)\\"",
"build": "napi build --release --platform --strip",
"build:debug": "napi build",
"prepublishOnly": "napi prepublish -t npm",
"artifacts": "napi artifacts",
"universal": "napi universal",
"version": "napi version"
},
"devDependencies": {
"@napi-rs/cli": "^${cliVersion}"
}
}`
}

View file

@ -0,0 +1,76 @@
import { spawnSync } from 'child_process'
import { join, resolve } from 'path'
import {
applyDefaultUniversalizeOptions,
UniversalizeOptions,
} from '../def/universalize.js'
import { readNapiConfig } from '../utils/config.js'
import { debugFactory } from '../utils/log.js'
import { fileExists } from '../utils/misc.js'
import { UniArchsByPlatform } from '../utils/target.js'
const debug = debugFactory('universalize')
const universalizers: Partial<
Record<NodeJS.Platform, (inputs: string[], output: string) => void>
> = {
darwin: (inputs, output) => {
spawnSync('lipo', ['-create', '-output', output, ...inputs], {
stdio: 'inherit',
})
},
}
export async function universalizeBinaries(userOptions: UniversalizeOptions) {
const options = applyDefaultUniversalizeOptions(userOptions)
const packageJsonPath = join(options.cwd, options.packageJsonPath)
const config = await readNapiConfig(packageJsonPath)
const target = config.targets.find(
(t) => t.platform === process.platform && t.arch === 'universal',
)
if (!target) {
throw new Error(
`'universal' arch for platform '${process.platform}' not found in config!`,
)
}
const srcFiles = UniArchsByPlatform[process.platform]?.map(
(arch) => `${config.binaryName}.${process.platform}-${arch}.node`,
)
if (!srcFiles || !universalizers[process.platform]) {
throw new Error(
`'universal' arch for platform '${process.platform}' not supported.`,
)
}
debug(`Looking up source binaries to combine: `)
debug(' %O', srcFiles)
const srcFileLookup = await Promise.all(
srcFiles.map((f) => fileExists(resolve(options.cwd, options.outputDir, f))),
)
const notFoundFiles = srcFiles.filter((_, i) => !srcFileLookup[i])
if (notFoundFiles.length) {
throw new Error(
`Some binary files were not found: ${JSON.stringify(notFoundFiles)}`,
)
}
const output = resolve(
options.cwd,
options.outputDir,
`${config.binaryName}.${process.platform}-universal.node`,
)
universalizers[process.platform]?.(srcFiles, output)
debug(`Produced universal binary: ${output}`)
}

26
cli/src/api/version.ts Normal file
View file

@ -0,0 +1,26 @@
import { join, resolve } from 'path'
import { applyDefaultVersionOptions, VersionOptions } from '../def/version.js'
import {
readNapiConfig,
debugFactory,
updatePackageJson,
} from '../utils/index.js'
const debug = debugFactory('version')
export async function version(userOptions: VersionOptions) {
const options = applyDefaultVersionOptions(userOptions)
const packageJsonPath = resolve(options.cwd, options.packageJsonPath)
const config = await readNapiConfig(packageJsonPath)
for (const target of config.targets) {
const pkgDir = resolve(options.cwd, options.npmDir, target.platformArchABI)
debug(`Update version to %i in [%i]`, config.packageJson.version, pkgDir)
await updatePackageJson(join(pkgDir, 'package.json'), {
version: config.packageJson.version,
})
}
}

View file

@ -1,56 +0,0 @@
export const ARM_FEATURES_H = `/* Macros to test for CPU features on ARM. Generic ARM version.
Copyright (C) 2012-2022 Free Software Foundation, Inc.
This file is part of the GNU C Library.
The GNU C Library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
The GNU C Library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with the GNU C Library. If not, see
<https://www.gnu.org/licenses/>. */
#ifndef _ARM_ARM_FEATURES_H
#define _ARM_ARM_FEATURES_H 1
/* An OS-specific arm-features.h file should define ARM_HAVE_VFP to
an appropriate expression for testing at runtime whether the VFP
hardware is present. We'll then redefine it to a constant if we
know at compile time that we can assume VFP. */
#ifndef __SOFTFP__
/* The compiler is generating VFP instructions, so we're already
assuming the hardware exists. */
# undef ARM_HAVE_VFP
# define ARM_HAVE_VFP 1
#endif
/* An OS-specific arm-features.h file may define ARM_ASSUME_NO_IWMMXT
to indicate at compile time that iWMMXt hardware is never present
at runtime (or that we never care about its state) and so need not
be checked for. */
/* A more-specific arm-features.h file may define ARM_ALWAYS_BX to indicate
that instructions using pc as a destination register must never be used,
so a "bx" (or "blx") instruction is always required. */
/* The log2 of the minimum alignment required for an address that
is the target of a computed branch (i.e. a "bx" instruction).
A more-specific arm-features.h file may define this to set a more
stringent requirement.
Using this only makes sense for code in ARM mode (where instructions
always have a fixed size of four bytes), or for Thumb-mode code that is
specifically aligning all the related branch targets to match (since
Thumb instructions might be either two or four bytes). */
#ifndef ARM_BX_ALIGN_LOG2
# define ARM_BX_ALIGN_LOG2 2
#endif
/* An OS-specific arm-features.h file may define ARM_NO_INDEX_REGISTER to
indicate that the two-register addressing modes must never be used. */
#endif /* arm-features.h */
`

View file

@ -1,89 +0,0 @@
import { join, parse } from 'path'
import { Command, Option } from 'clipanion'
import * as chalk from 'colorette'
import { fdir } from 'fdir'
import { getNapiConfig } from './consts'
import { debugFactory } from './debug'
import { UniArchsByPlatform } from './parse-triple'
import { readFileAsync, writeFileAsync } from './utils'
const debug = debugFactory('artifacts')
export class ArtifactsCommand extends Command {
static usage = Command.Usage({
description: 'Copy artifacts from Github Actions into specified dir',
})
static paths = [['artifacts']]
sourceDir = Option.String('-d,--dir', 'artifacts')
distDir = Option.String('--dist', 'npm')
configFileName?: string = Option.String('-c,--config')
async execute() {
const { platforms, binaryName, packageJsonPath } = getNapiConfig(
this.configFileName,
)
const packageJsonDir = parse(packageJsonPath).dir
const sourceApi = new fdir()
.withFullPaths()
.crawl(join(process.cwd(), this.sourceDir))
const distDirs = platforms.map((platform) =>
join(process.cwd(), this.distDir, platform.platformArchABI),
)
const universalSourceBins = new Set(
platforms
.filter((platform) => platform.arch === 'universal')
.flatMap((p) =>
UniArchsByPlatform[p.platform].map((a) => `${p.platform}-${a}`),
),
)
await sourceApi.withPromise().then((output) =>
Promise.all(
(output as string[]).map(async (filePath) => {
debug(`Read [${chalk.yellowBright(filePath)}]`)
const sourceContent = await readFileAsync(filePath)
const parsedName = parse(filePath)
const [_binaryName, platformArchABI] = parsedName.name.split('.')
if (_binaryName !== binaryName) {
debug(
`[${chalk.yellowBright(
_binaryName,
)}] is not matched with [${chalk.greenBright(binaryName)}], skip`,
)
return
}
const dir = distDirs.find((dir) => dir.includes(platformArchABI))
if (!dir && universalSourceBins.has(platformArchABI)) {
debug(
`[${chalk.yellowBright(
platformArchABI,
)}] has no dist dir but it is source bin for universal arch, skip`,
)
return
}
if (!dir) {
throw new TypeError(`No dist dir found for ${filePath}`)
}
const distFilePath = join(dir, parsedName.base)
debug(`Write file content to [${chalk.yellowBright(distFilePath)}]`)
await writeFileAsync(distFilePath, sourceContent)
const distFilePathLocal = join(packageJsonDir, parsedName.base)
debug(
`Write file content to [${chalk.yellowBright(distFilePathLocal)}]`,
)
await writeFileAsync(distFilePathLocal, sourceContent)
}),
),
)
}
}

View file

@ -1,921 +0,0 @@
import { execSync } from 'child_process'
import { createHash } from 'crypto'
import { existsSync, mkdirSync } from 'fs'
import { tmpdir } from 'os'
import { join, parse, sep } from 'path'
import { Command, Option } from 'clipanion'
import * as chalk from 'colorette'
import envPaths from 'env-paths'
import { groupBy } from 'lodash-es'
import { version } from '../package.json'
import { ARM_FEATURES_H } from './arm-features.h'
import { getNapiConfig } from './consts'
import { debugFactory } from './debug'
import { createJsBinding } from './js-binding-template'
import { getHostTargetTriple, parseTriple } from './parse-triple'
import {
copyFileAsync,
mkdirAsync,
readFileAsync,
unlinkAsync,
writeFileAsync,
} from './utils'
const debug = debugFactory('build')
const ZIG_PLATFORM_TARGET_MAP = {
'x86_64-unknown-linux-musl': 'x86_64-linux-musl',
'x86_64-unknown-linux-gnu': 'x86_64-linux-gnu',
// Doesn't support Windows MSVC for now
// 'x86_64-pc-windows-gnu': 'x86_64-windows-gnu',
// https://github.com/ziglang/zig/issues/1759
// 'x86_64-unknown-freebsd': 'x86_64-freebsd',
'x86_64-apple-darwin': 'x86_64-macos',
'aarch64-apple-darwin': 'aarch64-macos',
'aarch64-unknown-linux-gnu': 'aarch64-linux-gnu',
'aarch64-unknown-linux-musl': 'aarch64-linux-musl',
'armv7-unknown-linux-gnueabihf': 'arm-linux-gnueabihf',
}
const DEFAULT_GLIBC_TARGET = process.env.GLIBC_ABI_TARGET ?? '2.17'
const SHEBANG_NODE = process.platform === 'win32' ? '' : '#!/usr/bin/env node\n'
const SHEBANG_SH = process.platform === 'win32' ? '' : '#!/usr/bin/env sh\n'
function processZigLinkerArgs(platform: string, args: string[]) {
if (platform.includes('apple')) {
const newArgs = args.filter(
(arg, index) =>
!arg.startsWith('-Wl,-exported_symbols_list') &&
arg !== '-Wl,-dylib' &&
arg !== '-liconv' &&
arg !== '-Wl,-dead_strip' &&
!(arg === '-framework' && args[index + 1] === 'CoreFoundation') &&
!(arg === 'CoreFoundation' && args[index - 1] === '-framework'),
)
newArgs.push('-Wl,"-undefined=dynamic_lookup"', '-dead_strip', '-lunwind')
return newArgs
}
if (platform.includes('linux')) {
return args
.map((arg) => {
if (arg === '-lgcc_s') {
return '-lunwind'
}
return arg
})
.filter((arg) => arg !== '-march=armv7-a')
}
return args
}
export class BuildCommand extends Command {
static usage = Command.Usage({
description: 'Build and copy native module into specified dir',
})
static paths = [['build']]
appendPlatformToFilename = Option.Boolean(`--platform`, false, {
description: `Add platform triple to the .node file. ${chalk.green(
'[name].linux-x64-gnu.node',
)} for example`,
})
isRelease = Option.Boolean(`--release`, false, {
description: `Bypass to ${chalk.green('cargo build --release')}`,
})
configFileName?: string = Option.String('--config,-c', {
description: `napi config path, only JSON format accepted. Default to ${chalk.underline(
chalk.green('package.json'),
)}`,
})
cargoName?: string = Option.String('--cargo-name', {
description: `Override the ${chalk.green(
'name',
)} field in ${chalk.underline(chalk.yellowBright('Cargo.toml'))}`,
})
targetTripleDir = Option.String(
'--target',
process.env.RUST_TARGET ?? process.env.CARGO_BUILD_TARGET ?? '',
{
description: `Bypass to ${chalk.green('cargo build --target')}`,
},
)
features?: string = Option.String('--features', {
description: `Bypass to ${chalk.green('cargo build --features')}`,
})
bin?: string = Option.String('--bin', {
description: `Bypass to ${chalk.green('cargo build --bin')}`,
})
dts?: string = Option.String('--dts', 'index.d.ts', {
description: `The filename and path of ${chalk.green(
'.d.ts',
)} file, relative to cwd`,
})
constEnum?: boolean = Option.Boolean('--const-enum', true, {
description: `Generate ${chalk.green(
'const enum',
)} in .d.ts file or not, default is ${chalk.green('true')}`,
})
noDtsHeader = Option.Boolean('--no-dts-header', false, {
description: `Don't generate ${chalk.green('.d.ts')} header`,
})
project = Option.String('-p', {
description: `Bypass to ${chalk.green('cargo -p')}`,
})
cargoFlags = Option.String('--cargo-flags', '', {
description: `All the others flag passed to ${chalk.yellow('cargo build')}`,
})
jsBinding = Option.String('--js', 'index.js', {
description: `Path to the JS binding file, pass ${chalk.underline(
chalk.yellow('false'),
)} to disable it. Only affect if ${chalk.green('--target')} is specified.`,
})
jsPackageName = Option.String('--js-package-name', {
description: `Package name in generated js binding file, Only affect if ${chalk.green(
'--target',
)} specified and ${chalk.green('--js')} is not false.`,
required: false,
})
cargoCwd?: string = Option.String('--cargo-cwd', {
description: `The cwd of ${chalk.underline(
chalk.yellow('Cargo.toml'),
)} file`,
})
pipe?: string = Option.String('--pipe', {
description: `Pipe [${chalk.green(
'.js/.ts',
)}] files to this command, eg ${chalk.green('prettier -w')}`,
})
// https://github.com/napi-rs/napi-rs/issues/297
disableWindowsX32Optimize?: boolean = Option.Boolean(
'--disable-windows-x32-optimize',
false,
{
description: `Disable windows x32 ${chalk.green(
'lto',
)} and increase ${chalk.green(
'codegen-units',
)}. Disabled by default. See ${chalk.underline(
chalk.blue('https://github.com/napi-rs/napi-rs/issues/297'),
)}`,
},
)
destDir = Option.String({
required: false,
})
useZig = Option.Boolean(`--zig`, false, {
description: `Use ${chalk.green('zig')} as linker ${chalk.yellowBright(
'(Experimental)',
)}`,
})
zigABIVersion = Option.String(`--zig-abi-suffix`, {
description: `The suffix of the ${chalk.green(
'zig --target',
)} ABI version. Eg. ${chalk.cyan(
'--target x86_64-unknown-linux-gnu',
)} ${chalk.green('--zig-abi-suffix=2.17')}`,
})
zigLinkOnly = Option.Boolean(`--zig-link-only`, false, {
description: `Only link the library with ${chalk.green('zig')}`,
})
isStrip = Option.Boolean(`--strip`, false, {
description: `${chalk.green('Strip')} the library for minimum file size`,
})
async execute() {
const cwd = this.cargoCwd
? join(process.cwd(), this.cargoCwd)
: process.cwd()
const cargoTomlPath = join(cwd, 'Cargo.toml')
let cargoMetadata: any
try {
debug('Start parse toml')
cargoMetadata = JSON.parse(
execSync(
`cargo metadata --format-version 1 --manifest-path "${cargoTomlPath}"`,
{
stdio: 'pipe',
maxBuffer: 1024 * 1024 * 10,
},
).toString('utf8'),
)
} catch (e) {
throw new TypeError('Could not parse the Cargo.toml: ' + e)
}
const packages = cargoMetadata.packages
let cargoPackageName: string
if (this.cargoName) {
cargoPackageName = this.cargoName
} else {
const root = cargoMetadata.resolve.root
if (root) {
const rootPackage = packages.find((p: { id: string }) => p.id === root)
cargoPackageName = rootPackage.name
} else {
throw new TypeError('No package.name field in Cargo.toml')
}
}
const cargoPackage = packages.find(
(p: { name: string }) => p.name === cargoPackageName,
)
if (
!this.bin &&
cargoPackage?.targets?.length === 1 &&
cargoPackage?.targets[0].kind.length === 1 &&
cargoPackage?.targets[0].kind[0] === 'bin'
) {
this.bin = cargoPackageName
}
const releaseFlag = this.isRelease ? `--release` : ''
const targetFlag = this.targetTripleDir
? `--target ${this.targetTripleDir}`
: ''
const featuresFlag = this.features ? `--features ${this.features}` : ''
const binFlag = this.bin ? `--bin ${this.bin}` : ''
const triple = this.targetTripleDir
? parseTriple(this.targetTripleDir)
: getHostTargetTriple()
debug(`Current triple is: ${chalk.green(triple.raw)}`)
const pFlag = this.project ? `-p ${this.project}` : ''
const externalFlags = [
releaseFlag,
targetFlag,
featuresFlag,
binFlag,
pFlag,
this.cargoFlags,
]
.filter((flag) => Boolean(flag))
.join(' ')
const additionalEnv = {}
const isCrossForWin =
triple.platform === 'win32' && process.platform !== 'win32'
const isCrossForLinux =
triple.platform === 'linux' &&
(process.platform !== 'linux' ||
triple.arch !== process.arch ||
(function () {
const glibcVersionRuntime =
// @ts-expect-error
process.report?.getReport()?.header?.glibcVersionRuntime
const libc = glibcVersionRuntime ? 'gnu' : 'musl'
return triple.abi !== libc
})())
const isCrossForMacOS =
triple.platform === 'darwin' && process.platform !== 'darwin'
const cargo = process.env.CARGO ?? (isCrossForWin ? 'cargo-xwin' : 'cargo')
if (isCrossForWin && triple.arch === 'ia32') {
additionalEnv['XWIN_ARCH'] = 'x86'
}
const cargoCommand = `${cargo} build ${externalFlags}`
debug(`Run ${chalk.green(cargoCommand)}`)
const rustflags = process.env.RUSTFLAGS
? process.env.RUSTFLAGS.split(' ')
: []
if (triple.raw.includes('musl') && !this.bin) {
if (!rustflags.includes('target-feature=-crt-static')) {
rustflags.push('-C target-feature=-crt-static')
}
}
if (this.isStrip && !rustflags.includes('-C link-arg=-s')) {
rustflags.push('-C link-arg=-s')
}
let useZig = false
if (this.useZig || isCrossForLinux || isCrossForMacOS) {
try {
execSync('zig version')
useZig = true
} catch (e) {
if (this.useZig) {
throw new TypeError(
`Could not find ${chalk.green('zig')} on the PATH`,
)
} else {
debug(
`Could not find ${chalk.green(
'zig',
)} on the PATH, fallback to normal linker`,
)
}
}
}
if (useZig) {
const zigABIVersion =
this.zigABIVersion ??
(isCrossForLinux && triple.abi === 'gnu' ? DEFAULT_GLIBC_TARGET : null)
const mappedZigTarget = ZIG_PLATFORM_TARGET_MAP[triple.raw]
const zigTarget = `${mappedZigTarget}${
zigABIVersion ? `.${zigABIVersion}` : ''
}`
debug(`Using Zig with target ${chalk.green(zigTarget)}`)
if (!mappedZigTarget) {
throw new Error(`${triple.raw} can not be cross compiled by zig`)
}
const paths = envPaths('napi-rs')
const shellFileExt = process.platform === 'win32' ? 'cmd' : 'sh'
const linkerWrapperShell = join(
paths.cache,
`zig-linker-${triple.raw}.${shellFileExt}`,
)
const CCWrapperShell = join(
paths.cache,
`zig-cc-${triple.raw}.${shellFileExt}`,
)
const CXXWrapperShell = join(
paths.cache,
`zig-cxx-${triple.raw}.${shellFileExt}`,
)
const linkerWrapper = join(paths.cache, `zig-cc-${triple.raw}.js`)
mkdirSync(paths.cache, { recursive: true })
const forwardArgs = process.platform === 'win32' ? '"%*"' : '$@'
if (triple.arch === 'arm') {
await patchArmFeaturesHForArmTargets()
}
await writeFileAsync(
linkerWrapperShell,
process.platform === 'win32'
? `@IF EXIST "%~dp0\\node.exe" (
"%~dp0\\node.exe" "${linkerWrapper}" %*
) ELSE (
@SETLOCAL
@SET PATHEXT=%PATHEXT:;.JS;=;%
node "${linkerWrapper}" %*
)`
: `${SHEBANG_SH}node ${linkerWrapper} ${forwardArgs}`,
{
mode: '777',
},
)
await writeFileAsync(
CCWrapperShell,
`${SHEBANG_SH}node ${linkerWrapper} cc ${forwardArgs}`,
{
mode: '777',
},
)
await writeFileAsync(
CXXWrapperShell,
`${SHEBANG_SH}node ${linkerWrapper} c++ ${forwardArgs}`,
{
mode: '777',
},
)
await writeFileAsync(
linkerWrapper,
`${SHEBANG_NODE}const{writeFileSync} = require('fs')\n${processZigLinkerArgs.toString()}\nconst {status} = require('child_process').spawnSync('zig', [process.argv[2] === "c++" || process.argv[2] === "cc" ? "" : "cc", ...processZigLinkerArgs('${
triple.raw
}', process.argv.slice(2)), '-target', '${zigTarget}'], { stdio: 'inherit', shell: true })\nwriteFileSync('${linkerWrapper.replaceAll(
'\\',
'/',
)}.args.log', processZigLinkerArgs('${
triple.raw
}', process.argv.slice(2)).join(' '))\n\nprocess.exit(status || 0)\n`,
{
mode: '777',
},
)
const envTarget = triple.raw.replaceAll('-', '_').toUpperCase()
if (!this.zigLinkOnly) {
Object.assign(additionalEnv, {
CC: CCWrapperShell,
CXX: CXXWrapperShell,
TARGET_CC: CCWrapperShell,
TARGET_CXX: CXXWrapperShell,
})
}
additionalEnv[`CARGO_TARGET_${envTarget}_LINKER`] = linkerWrapperShell
}
debug(`Platform: ${JSON.stringify(triple, null, 2)}`)
if (triple.platform === 'android') {
const { ANDROID_NDK_LATEST_HOME } = process.env
if (!ANDROID_NDK_LATEST_HOME) {
console.info(
`${chalk.yellow(
'ANDROID_NDK_LATEST_HOME',
)} environment variable is missing`,
)
}
const targetArch = triple.arch === 'arm' ? 'armv7a' : 'aarch64'
const targetPlatform =
triple.arch === 'arm' ? 'androideabi24' : 'android24'
Object.assign(additionalEnv, {
CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/${targetArch}-linux-android24-clang`,
CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/${targetArch}-linux-androideabi24-clang`,
CC: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/${targetArch}-linux-${targetPlatform}-clang`,
CXX: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/${targetArch}-linux-${targetPlatform}-clang++`,
AR: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar`,
ANDROID_NDK: ANDROID_NDK_LATEST_HOME,
PATH: `${ANDROID_NDK_LATEST_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin:${process.env.PATH}`,
})
}
const {
binaryName,
packageName,
tsConstEnum: tsConstEnumFromConfig,
} = getNapiConfig(this.configFileName)
const tsConstEnum = this.constEnum ?? tsConstEnumFromConfig
if (triple.platform === 'wasi') {
try {
const emnapiDir = require.resolve('emnapi')
const linkDir = join(emnapiDir, '..', 'lib', 'wasm32-wasi')
additionalEnv['EMNAPI_LINK_DIR'] = linkDir
rustflags.push('-Z wasi-exec-model=reactor')
} catch (e) {
const err = new Error(`Could not find emnapi, please install emnapi`)
err.cause = e
throw err
}
}
if (rustflags.length > 0) {
additionalEnv['RUSTFLAGS'] = rustflags.join(' ')
}
let cargoArtifactName = this.cargoName
if (!cargoArtifactName) {
if (this.bin) {
cargoArtifactName = cargoPackageName
} else {
cargoArtifactName = cargoPackageName.replace(/-/g, '_')
}
if (
!this.bin &&
!cargoPackage.targets.some((target: { crate_types: string[] }) =>
target.crate_types.includes('cdylib'),
)
) {
throw new TypeError(
`Missing ${chalk.green('crate-type = ["cdylib"]')} in ${chalk.green(
'[lib]',
)}`,
)
}
}
if (this.bin) {
debug(`Binary name: ${chalk.greenBright(cargoArtifactName)}`)
} else {
debug(`Dylib name: ${chalk.greenBright(cargoArtifactName)}`)
}
const cwdSha = createHash('sha256')
.update(process.cwd())
.update(version)
.digest('hex')
.substring(0, 8)
const intermediateTypeFile = join(
tmpdir(),
`${cargoArtifactName}-${cwdSha}.napi_type_def.tmp`,
)
const intermediateWasiRegisterFile = join(
tmpdir(),
`${cargoArtifactName}-${cwdSha}.napi_wasi_register.tmp`,
)
debug(`intermediate type def file: ${intermediateTypeFile}`)
const commandEnv = {
...process.env,
...additionalEnv,
TYPE_DEF_TMP_PATH: intermediateTypeFile,
WASI_REGISTER_TMP_PATH: intermediateWasiRegisterFile,
CARGO_CFG_NAPI_RS_CLI_VERSION: version,
}
try {
execSync(cargoCommand, {
env: commandEnv,
stdio: 'inherit',
cwd,
})
} catch (e) {
if (cargo === 'cargo-xwin') {
console.warn(
`You are cross compiling ${chalk.underline(
triple.raw,
)} target on ${chalk.green(process.platform)} host`,
)
} else if (isCrossForLinux || isCrossForMacOS) {
console.warn(
`You are cross compiling ${chalk.underline(
triple.raw,
)} on ${chalk.green(process.platform)} host`,
)
}
throw e
}
const platform = triple.platform
let libExt = ''
debug(`Platform: ${chalk.greenBright(platform)}`)
// Platform based massaging for build commands
if (!this.bin) {
switch (platform) {
case 'darwin':
libExt = '.dylib'
cargoArtifactName = `lib${cargoArtifactName}`
break
case 'win32':
libExt = '.dll'
break
case 'linux':
case 'freebsd':
case 'openbsd':
case 'android':
case 'sunos':
cargoArtifactName = `lib${cargoArtifactName}`
libExt = '.so'
break
default:
throw new TypeError(
'Operating system not currently supported or recognized by the build script',
)
}
}
const targetRootDir =
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
process.env.CARGO_TARGET_DIR ||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
process.env.CARGO_BUILD_TARGET_DIR ||
(await findUp(cwd))
if (!targetRootDir) {
throw new TypeError('No target dir found')
}
const targetDir = join(
this.targetTripleDir,
this.isRelease ? 'release' : 'debug',
)
const platformName = this.appendPlatformToFilename
? `.${triple.platformArchABI}`
: ''
debug(`Platform name: ${platformName || chalk.green('[Empty]')}`)
const distFileName = this.bin
? cargoArtifactName!
: `${binaryName}${platformName}.node`
const distModulePath = join(this.destDir ?? '.', distFileName)
const parsedDist = parse(distModulePath)
if (parsedDist.dir && !existsSync(parsedDist.dir)) {
await mkdirAsync(parsedDist.dir, { recursive: true }).catch((e) => {
console.warn(
chalk.bgYellowBright(
`Create dir [${parsedDist.dir}] failed, reason: ${e.message}`,
),
)
})
}
const sourcePath = join(
targetRootDir,
targetDir,
`${cargoArtifactName}${libExt}`,
)
if (existsSync(distModulePath)) {
debug(`remove old binary [${chalk.yellowBright(distModulePath)}]`)
await unlinkAsync(distModulePath)
}
debug(`Write binary content to [${chalk.yellowBright(distModulePath)}]`)
await copyFileAsync(sourcePath, distModulePath)
if (!this.bin) {
const dtsFilePath = join(
process.cwd(),
this.destDir ?? '.',
this.dts ?? 'index.d.ts',
)
const jsBindingFilePath =
this.jsBinding &&
this.jsBinding !== 'false' &&
this.appendPlatformToFilename
? join(process.cwd(), this.destDir ?? '.', this.jsBinding)
: null
const idents = await processIntermediateTypeFile(
intermediateTypeFile,
dtsFilePath,
this.noDtsHeader,
tsConstEnum,
)
await writeJsBinding(
binaryName,
this.jsPackageName ?? packageName,
jsBindingFilePath,
idents,
)
if (this.pipe) {
if (jsBindingFilePath) {
const pipeCommand = `${this.pipe} ${jsBindingFilePath}`
console.info(`Run ${chalk.green(pipeCommand)}`)
try {
execSync(pipeCommand, { stdio: 'inherit', env: commandEnv })
} catch (e) {
console.warn(
chalk.bgYellowBright(
'Pipe the js binding file to command failed',
),
e,
)
}
}
const pipeCommand = `${this.pipe} ${dtsFilePath}`
console.info(`Run ${chalk.green(pipeCommand)}`)
try {
execSync(pipeCommand, { stdio: 'inherit', env: commandEnv })
} catch (e) {
console.warn(
chalk.bgYellowBright('Pipe the dts file to command failed'),
e,
)
}
}
}
}
}
async function findUp(dir = process.cwd()): Promise<string | null> {
const dist = join(dir, 'target')
if (existsSync(dist)) {
return dist
}
const dirs = dir.split(sep)
if (dirs.length < 2) {
return null
}
dirs.pop()
return findUp(dirs.join(sep))
}
interface TypeDef {
kind: 'fn' | 'struct' | 'impl' | 'enum' | 'interface'
name: string
original_name?: string
def: string
js_mod?: string
js_doc: string
}
async function processIntermediateTypeFile(
source: string,
target: string,
noDtsHeader: boolean,
tsConstEnum: boolean,
): Promise<string[]> {
const idents: string[] = []
if (!existsSync(source)) {
debug(`do not find tmp type file. skip type generation`)
return idents
}
const tmpFile = await readFileAsync(source, 'utf8')
const lines = tmpFile
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
// compatible with old version
if (line.startsWith('{')) {
return line
} else {
const [_crateName, ...rest] = line.split(':')
return rest.join(':')
}
})
if (!lines.length) {
return idents
}
const allDefs = lines.map((line) => JSON.parse(line) as TypeDef)
function convertDefs(defs: TypeDef[], nested = false): string {
const classes = new Map<
string,
{ def: string; js_doc: string; original_name?: string }
>()
const impls = new Map<string, string>()
let dts = ''
const nest = nested ? 2 : 0
defs.forEach((def) => {
switch (def.kind) {
case 'struct':
if (!nested) {
idents.push(def.name)
}
classes.set(def.name, {
original_name: def.original_name,
def: def.def,
js_doc: def.js_doc,
})
break
case 'impl':
const existed = impls.get(def.name)
impls.set(
def.name,
`${existed ? existed + '\n' : ''}${def.js_doc}${def.def}`,
)
break
case 'interface':
dts +=
indentLines(`${def.js_doc}export interface ${def.name} {`, nest) +
'\n'
dts += indentLines(def.def, nest + 2) + '\n'
dts += indentLines(`}`, nest) + '\n'
break
case 'enum':
if (!nested) {
idents.push(def.name)
}
const enumPrefix = tsConstEnum ? ' const' : ''
dts +=
indentLines(
`${def.js_doc}export${enumPrefix} enum ${def.name} {`,
nest,
) + '\n'
dts += indentLines(def.def, nest + 2) + '\n'
dts += indentLines(`}`, nest) + '\n'
break
default:
if (!nested) {
idents.push(def.name)
}
dts += indentLines(`${def.js_doc}${def.def}`, nest) + '\n'
}
})
for (const [name, { js_doc, def, original_name }] of classes.entries()) {
const implDef = impls.get(name)
if (original_name && name !== original_name) {
dts += indentLines(`export type ${original_name} = ${name}\n`, nest)
}
dts += indentLines(`${js_doc}export class ${name} {`, nest)
if (def) {
dts += '\n' + indentLines(def, nest + 2)
}
if (implDef) {
dts += '\n' + indentLines(implDef, nest + 2)
}
if (def || implDef) {
dts += '\n'
} else {
dts += ` `
}
dts += indentLines(`}`, nest) + '\n'
}
return dts
}
const topLevelDef = convertDefs(allDefs.filter((def) => !def.js_mod))
const namespaceDefs = Object.entries(
groupBy(
allDefs.filter((def) => def.js_mod),
'js_mod',
),
).reduce((acc, [mod, defs]) => {
idents.push(mod)
return acc + `export namespace ${mod} {\n${convertDefs(defs, true)}}\n`
}, '')
const dtsHeader = noDtsHeader
? ''
: `/* tslint:disable */
/* eslint-disable */
/* auto-generated by NAPI-RS */\n
`
const externalDef =
topLevelDef.indexOf('ExternalObject<') > -1 ||
namespaceDefs.indexOf('ExternalObject<') > -1
? `export class ExternalObject<T> {
readonly '': {
readonly '': unique symbol
[K: symbol]: T
}
}\n`
: ''
await writeFileAsync(
target,
dtsHeader + externalDef + topLevelDef + namespaceDefs,
'utf8',
)
return idents
}
function indentLines(input: string, spaces: number) {
return input
.split('\n')
.map(
(line) =>
''.padEnd(spaces, ' ') +
(line.startsWith(' *') ? line.trimEnd() : line.trim()),
)
.join('\n')
}
async function writeJsBinding(
localName: string,
packageName: string,
distFileName: string | null,
idents: string[],
) {
if (distFileName && idents.length) {
const template = createJsBinding(localName, packageName)
const declareCodes = `const { ${idents.join(', ')} } = nativeBinding\n`
const exportsCode = idents.reduce(
(acc, cur) => `${acc}\nmodule.exports.${cur} = ${cur}`,
'',
)
await writeFileAsync(
distFileName,
template + declareCodes + exportsCode + '\n',
'utf8',
)
}
}
async function patchArmFeaturesHForArmTargets() {
let zigExePath: string
let zigLibDir: string | undefined
try {
const zigEnv = JSON.parse(execSync(`zig env`, { encoding: 'utf8' }).trim())
zigExePath = zigEnv['zig_exe']
zigLibDir = zigEnv['lib_dir']
} catch (e) {
throw new Error(
'Cannot get zig env correctly, please ensure the zig is installed correctly on your system',
)
}
try {
const p = zigLibDir
? join(zigLibDir, 'libc/glibc/sysdeps/arm/arm-features.h')
: join(zigExePath, '../lib/libc/glibc/sysdeps/arm/arm-features.h')
if (!existsSync(p)) {
await writeFileAsync(p, ARM_FEATURES_H, {
mode: 0o644,
})
}
} catch (e) {
console.error(
Error(
`Cannot patch arm-features.h, error: ${
(e as Error).message || e
}. See: https://github.com/ziglang/zig/issues/3287`,
),
)
}
}

29
cli/src/cli.ts Normal file
View file

@ -0,0 +1,29 @@
import { Cli } from 'clipanion'
import { ArtifactsCommand } from './commands/artifacts.js'
import { BuildCommand } from './commands/build.js'
import { CreateNpmDirsCommand } from './commands/create-npm-dirs.js'
import { HelpCommand } from './commands/help.js'
import { NewCommand } from './commands/new.js'
import { PrePublishCommand } from './commands/pre-publish.js'
import { RenameCommand } from './commands/rename.js'
import { UniversalizeCommand } from './commands/universalize.js'
import { VersionCommand } from './commands/version.js'
import { CLI_VERSION } from './utils/misc.js'
const cli = new Cli({
binaryName: 'napi',
binaryVersion: CLI_VERSION,
})
cli.register(NewCommand)
cli.register(BuildCommand)
cli.register(CreateNpmDirsCommand)
cli.register(ArtifactsCommand)
cli.register(UniversalizeCommand)
cli.register(RenameCommand)
cli.register(PrePublishCommand)
cli.register(VersionCommand)
cli.register(HelpCommand)
void cli.runExit(process.argv.slice(2))

View file

@ -0,0 +1,23 @@
import { Command } from 'clipanion'
import { collectArtifacts } from '../api/artifacts.js'
import { BaseArtifactsCommand } from '../def/artifacts.js'
export class ArtifactsCommand extends BaseArtifactsCommand {
static usage = Command.Usage({
description: 'Copy artifacts from Github Actions into specified dir',
examples: [
[
'$0 artifacts --dir . --dist ./npm',
`Copy [binaryName].[platform].node under current dir(.) into packages under npm dir.
e.g: index.linux-x64-gnu.node --> ./npm/linux-x64-gnu/index.node`,
],
],
})
static paths = [['artifacts']]
async execute() {
await collectArtifacts(this.getOptions())
}
}

42
cli/src/commands/build.ts Normal file
View file

@ -0,0 +1,42 @@
import { execSync } from 'child_process'
import { Option } from 'clipanion'
import { buildProject } from '../api/build.js'
import { BaseBuildCommand } from '../def/build.js'
import { debugFactory } from '../utils/index.js'
const debug = debugFactory('build')
export class BuildCommand extends BaseBuildCommand {
pipe = Option.String('--pipe', {
description:
'Pipe all outputs file to given command. e.g. `napi build --pipe "npx prettier --write"`',
})
cargoOptions = Option.Rest()
async execute() {
const { task } = await buildProject({
...this.getOptions(),
cargoOptions: this.cargoOptions,
})
const outputs = await task
if (this.pipe) {
for (const output of outputs) {
debug('Piping output file to command: %s', this.pipe)
try {
execSync(`${this.pipe} ${output.path}`, {
stdio: 'inherit',
cwd: this.cwd,
})
} catch (e) {
debug.error(`Failed to pipe output file ${output.path} to command`)
debug.error(e)
}
}
}
}
}

View file

@ -0,0 +1,8 @@
import { createNpmDirs } from '../api/create-npm-dirs.js'
import { BaseCreateNpmDirsCommand } from '../def/create-npm-dirs.js'
export class CreateNpmDirsCommand extends BaseCreateNpmDirsCommand {
async execute() {
await createNpmDirs(this.getOptions())
}
}

138
cli/src/commands/new.ts Normal file
View file

@ -0,0 +1,138 @@
import path from 'path'
import { Option } from 'clipanion'
import inquirer from 'inquirer'
import { newProject } from '../api/new.js'
import { BaseNewCommand } from '../def/new.js'
import {
AVAILABLE_TARGETS,
debugFactory,
DEFAULT_TARGETS,
TargetTriple,
} from '../utils/index.js'
import { napiEngineRequirement } from '../utils/version.js'
const debug = debugFactory('new')
export class NewCommand extends BaseNewCommand {
interactive = Option.Boolean('--interactive,-i', false, {
description:
'Ask project basic information interactively without just using the default.',
})
async execute() {
try {
const options = await this.fetchOptions()
await newProject(options)
return 0
} catch (e) {
debug('Failed to create new project')
debug.error(e)
return 1
}
}
private async fetchOptions() {
const cmdOptions = super.getOptions()
if (this.interactive) {
return {
...cmdOptions,
name: await this.fetchName(path.parse(cmdOptions.path).base),
minNodeApiVersion: await this.fetchNapiVersion(),
targets: await this.fetchTargets(),
license: await this.fetchLicense(),
enableTypeDef: await this.fetchTypeDef(),
enableGithubActions: await this.fetchGithubActions(),
}
}
return cmdOptions
}
private async fetchName(defaultName: string): Promise<string> {
return (
this.$$name ??
(await inquirer
.prompt({
type: 'input',
name: 'name',
message: 'Package name (the name field in your package.json file)',
default: defaultName,
})
.then(({ name }) => name))
)
}
private async fetchLicense(): Promise<string> {
return inquirer
.prompt({
type: 'input',
name: 'license',
message: 'License for open-sourced project',
default: this.license,
})
.then(({ license }) => license)
}
private async fetchNapiVersion(): Promise<number> {
return inquirer
.prompt({
type: 'list',
name: 'minNodeApiVersion',
message: 'Minimum node-api version (with node version requirement)',
loop: false,
choices: new Array(8).fill(0).map((_, i) => ({
name: `napi${i + 1} (${napiEngineRequirement(i + 1)})`,
value: i + 1,
})),
// choice index
default: this.minNodeApiVersion - 1,
})
.then(({ minNodeApiVersion }) => minNodeApiVersion)
}
private async fetchTargets(): Promise<TargetTriple[]> {
if (this.enableDefaultTargets) {
return DEFAULT_TARGETS.concat()
}
if (this.enableAllTargets) {
return AVAILABLE_TARGETS.concat()
}
const { targets } = await inquirer.prompt({
name: 'targets',
type: 'checkbox',
loop: false,
message: 'Choose target(s) your crate will be compiled to',
default: DEFAULT_TARGETS,
choices: AVAILABLE_TARGETS,
})
return targets
}
private async fetchTypeDef(): Promise<boolean> {
const { enableTypeDef } = await inquirer.prompt({
name: 'enableTypeDef',
type: 'confirm',
message: 'Enable type definition auto-generation',
default: this.enableTypeDef,
})
return enableTypeDef
}
private async fetchGithubActions(): Promise<boolean> {
const { enableGithubActions } = await inquirer.prompt({
name: 'enableGithubActions',
type: 'confirm',
message: 'Enable Github Actions CI',
default: this.enableGithubActions,
})
return enableGithubActions
}
}

View file

@ -0,0 +1,9 @@
import { prePublish } from '../api/pre-publish.js'
import { BasePrePublishCommand } from '../def/pre-publish.js'
export class PrePublishCommand extends BasePrePublishCommand {
async execute() {
// @ts-expect-error const 'npm' | 'lerna' to string
await prePublish(this.getOptions())
}
}

View file

@ -0,0 +1,8 @@
import { renameProject } from '../api/rename.js'
import { BaseRenameCommand } from '../def/rename.js'
export class RenameCommand extends BaseRenameCommand {
async execute() {
await renameProject(this.getOptions())
}
}

View file

@ -0,0 +1,8 @@
import { universalizeBinaries } from '../api/universalize.js'
import { BaseUniversalizeCommand } from '../def/universalize.js'
export class UniversalizeCommand extends BaseUniversalizeCommand {
async execute() {
await universalizeBinaries(this.getOptions())
}
}

View file

@ -0,0 +1,8 @@
import { version } from '../api/version.js'
import { BaseVersionCommand } from '../def/version.js'
export class VersionCommand extends BaseVersionCommand {
async execute() {
await version(this.getOptions())
}
}

View file

@ -1,40 +0,0 @@
import { join } from 'path'
import { DefaultPlatforms, PlatformDetail, parseTriple } from './parse-triple'
export function getNapiConfig(
packageJson = 'package.json',
cwd = process.cwd(),
) {
const packageJsonPath = join(cwd, packageJson)
const pkgJson = require(packageJsonPath)
const { version: packageVersion, napi, name } = pkgJson
const additionPlatforms: PlatformDetail[] = (
napi?.triples?.additional ?? []
).map(parseTriple)
const defaultPlatforms =
napi?.triples?.defaults === false ? [] : [...DefaultPlatforms]
const tsConstEnum: boolean = napi?.ts?.constEnum ?? true
const platforms = [...defaultPlatforms, ...additionPlatforms]
const releaseVersion = process.env.RELEASE_VERSION
const releaseVersionWithoutPrefix = releaseVersion?.startsWith('v')
? releaseVersion.substring(1)
: releaseVersion
const version = releaseVersionWithoutPrefix ?? packageVersion
const packageName = napi?.package?.name ?? name
const npmClient: string = napi?.npmClient ?? 'npm'
const binaryName: string = napi?.name ?? 'index'
return {
platforms,
version,
packageName,
binaryName,
packageJsonPath,
content: pkgJson,
npmClient,
tsConstEnum,
}
}

View file

@ -1,105 +0,0 @@
import { mkdirSync } from 'fs'
import { join } from 'path'
import { Command, Option } from 'clipanion'
import * as chalk from 'colorette'
import { getNapiConfig } from './consts'
import { debugFactory } from './debug'
import { PlatformDetail } from './parse-triple'
import { writeFileAsync, pick } from './utils'
const debug = debugFactory('create-npm-dir')
export class CreateNpmDirCommand extends Command {
static usage = Command.Usage({
description: 'Create npm packages dir for platforms',
})
static paths = [['create-npm-dir']]
static create = async (
config: string,
targetDirPath: string,
cwd: string,
) => {
const pkgJsonDir = config
debug(`Read content from [${chalk.yellowBright(pkgJsonDir)}]`)
const { platforms, packageName, version, binaryName, content } =
getNapiConfig(pkgJsonDir, cwd)
for (const platformDetail of platforms) {
const targetDir = join(
targetDirPath,
'npm',
`${platformDetail.platformArchABI}`,
)
mkdirSync(targetDir, {
recursive: true,
})
const binaryFileName = `${binaryName}.${platformDetail.platformArchABI}.node`
const targetPackageJson = join(targetDir, 'package.json')
debug(`Write file [${chalk.yellowBright(targetPackageJson)}]`)
const packageJson: {
name: string
libc?: string[]
} = {
name: `${packageName}-${platformDetail.platformArchABI}`,
version,
os: [platformDetail.platform],
cpu:
platformDetail.arch !== 'universal'
? [platformDetail.arch]
: undefined,
main: binaryFileName,
files: [binaryFileName],
...pick(
content,
'description',
'keywords',
'author',
'authors',
'homepage',
'license',
'engines',
'publishConfig',
'repository',
'bugs',
),
}
// Only works with yarn 3.1+
// https://github.com/yarnpkg/berry/pull/3981
if (platformDetail.abi === 'gnu') {
packageJson.libc = ['glibc']
} else if (platformDetail.abi === 'musl') {
packageJson.libc = ['musl']
}
await writeFileAsync(
targetPackageJson,
JSON.stringify(packageJson, null, 2),
)
const targetReadme = join(targetDir, 'README.md')
debug(`Write target README.md [${chalk.yellowBright(targetReadme)}]`)
await writeFileAsync(targetReadme, readme(packageName, platformDetail))
}
}
targetDir: string = Option.String('-t,--target')!
config = Option.String('-c,--config', 'package.json')
async execute() {
await CreateNpmDirCommand.create(
this.config,
join(process.cwd(), this.targetDir),
process.cwd(),
)
}
}
function readme(packageName: string, platformDetail: PlatformDetail) {
return `# \`${packageName}-${platformDetail.platformArchABI}\`
This is the **${platformDetail.raw}** binary for \`${packageName}\`
`
}

View file

@ -1,3 +0,0 @@
import debug from 'debug'
export const debugFactory = (namespace: string) => debug(`napi:${namespace}`)

79
cli/src/def/artifacts.ts Normal file
View file

@ -0,0 +1,79 @@
// This file is generated by codegen/index.ts
// Do not edit this file manually
import { Command, Option } from 'clipanion'
export abstract class BaseArtifactsCommand extends Command {
static paths = [['artifacts']]
static usage = Command.Usage({
description:
'Copy artifacts from Github Actions into npm packages and ready to publish',
})
cwd = Option.String('--cwd', process.cwd(), {
description:
'The working directory of where napi command will be executed in, all other paths options are relative to this path',
})
packageJsonPath = Option.String('--package-json-path', 'package.json', {
description: 'Path to `package.json`',
})
outputDir = Option.String('--output-dir,-o', './', {
description:
'Path to the folder where all built `.node` files put, same as `--output-dir` of build command',
})
npmDir = Option.String('--npm-dir', 'npm', {
description: 'Path to the folder where the npm packages put',
})
getOptions() {
return {
cwd: this.cwd,
packageJsonPath: this.packageJsonPath,
outputDir: this.outputDir,
npmDir: this.npmDir,
}
}
}
/**
* Copy artifacts from Github Actions into npm packages and ready to publish
*/
export interface ArtifactsOptions {
/**
* The working directory of where napi command will be executed in, all other paths options are relative to this path
*
* @default process.cwd()
*/
cwd?: string
/**
* Path to `package.json`
*
* @default 'package.json'
*/
packageJsonPath?: string
/**
* Path to the folder where all built `.node` files put, same as `--output-dir` of build command
*
* @default './'
*/
outputDir?: string
/**
* Path to the folder where the npm packages put
*
* @default 'npm'
*/
npmDir?: string
}
export function applyDefaultArtifactsOptions(options: ArtifactsOptions) {
return {
cwd: process.cwd(),
packageJsonPath: 'package.json',
outputDir: './',
npmDir: 'npm',
...options,
}
}

242
cli/src/def/build.ts Normal file
View file

@ -0,0 +1,242 @@
// This file is generated by codegen/index.ts
// Do not edit this file manually
import { Command, Option } from 'clipanion'
export abstract class BaseBuildCommand extends Command {
static paths = [['build']]
static usage = Command.Usage({
description: 'Build the napi-rs project',
})
target?: string = Option.String('--target,-t', {
description:
'Build for the target triple, bypassed to `cargo build --target`',
})
cwd?: string = Option.String('--cwd', {
description:
'The working directory of where napi command will be executed in, all other paths options are relative to this path',
})
manifestPath?: string = Option.String('--manifest-path', {
description: 'Path to `Cargo.toml`',
})
packageJsonPath?: string = Option.String('--package-json-path', {
description: 'Path to `package.json`',
})
targetDir?: string = Option.String('--target-dir', {
description:
'Directory for all crate generated artifacts, see `cargo build --target-dir`',
})
outputDir?: string = Option.String('--output-dir,-o', {
description:
'Path to where all the built files would be put. Default to the crate folder',
})
platform?: boolean = Option.Boolean('--platform', {
description:
'Add platform triple to the generated nodejs binding file, eg: `[name].linux-x64-gnu.node`',
})
jsPackageName?: string = Option.String('--js-package-name', {
description:
'Package name in generated js binding file. Only works with `--platform` flag',
})
jsBinding?: string = Option.String('--js', {
description:
'Path and filename of generated JS binding file. Only works with `--platform` flag. Relative to `--output_dir`.',
})
noJsBinding?: boolean = Option.Boolean('--no-js', {
description:
'Whether to disable the generation JS binding file. Only works with `--platform` flag.',
})
dts?: string = Option.String('--dts', {
description:
'Path and filename of generated type def file. Relative to `--output_dir`',
})
dtsHeader?: string = Option.String('--dts-header', {
description:
'Custom file header for generated type def file. Only works when `typedef` feature enabled.',
})
noDtsHeader?: boolean = Option.Boolean('--no-dts-header', {
description:
'Whether to disable the default file header for generated type def file. Only works when `typedef` feature enabled.',
})
strip?: boolean = Option.Boolean('--strip,-s', {
description: 'Whether strip the library to achieve the minimum file size',
})
release?: boolean = Option.Boolean('--release,-r', {
description: 'Build in release mode',
})
verbose?: boolean = Option.Boolean('--verbose,-v', {
description: 'Verbosely log build command trace',
})
bin?: string = Option.String('--bin', {
description: 'Build only the specified binary',
})
package?: string = Option.String('--package,-p', {
description: 'Build the specified library or the one at cwd',
})
crossCompile?: boolean = Option.Boolean('--cross-compile,-x', {
description:
'[experimental] cross-compile for the specified target with `cargo-xwin` on windows and `cargo-zigbuild` on other platform',
})
watch?: boolean = Option.Boolean('--watch,-w', {
description:
'watch the crate changes and build continiously with `cargo-watch` crates',
})
features?: string[] = Option.Array('--features,-F', {
description: 'Space-separated list of features to activate',
})
allFeatures?: boolean = Option.Boolean('--all-features', {
description: 'Activate all available features',
})
noDefaultFeatures?: boolean = Option.Boolean('--no-default-features', {
description: 'Do not activate the `default` feature',
})
getOptions() {
return {
target: this.target,
cwd: this.cwd,
manifestPath: this.manifestPath,
packageJsonPath: this.packageJsonPath,
targetDir: this.targetDir,
outputDir: this.outputDir,
platform: this.platform,
jsPackageName: this.jsPackageName,
jsBinding: this.jsBinding,
noJsBinding: this.noJsBinding,
dts: this.dts,
dtsHeader: this.dtsHeader,
noDtsHeader: this.noDtsHeader,
strip: this.strip,
release: this.release,
verbose: this.verbose,
bin: this.bin,
package: this.package,
crossCompile: this.crossCompile,
watch: this.watch,
features: this.features,
allFeatures: this.allFeatures,
noDefaultFeatures: this.noDefaultFeatures,
}
}
}
/**
* Build the napi-rs project
*/
export interface BuildOptions {
/**
* Build for the target triple, bypassed to `cargo build --target`
*/
target?: string
/**
* The working directory of where napi command will be executed in, all other paths options are relative to this path
*/
cwd?: string
/**
* Path to `Cargo.toml`
*/
manifestPath?: string
/**
* Path to `package.json`
*/
packageJsonPath?: string
/**
* Directory for all crate generated artifacts, see `cargo build --target-dir`
*/
targetDir?: string
/**
* Path to where all the built files would be put. Default to the crate folder
*/
outputDir?: string
/**
* Add platform triple to the generated nodejs binding file, eg: `[name].linux-x64-gnu.node`
*/
platform?: boolean
/**
* Package name in generated js binding file. Only works with `--platform` flag
*/
jsPackageName?: string
/**
* Path and filename of generated JS binding file. Only works with `--platform` flag. Relative to `--output_dir`.
*/
jsBinding?: string
/**
* Whether to disable the generation JS binding file. Only works with `--platform` flag.
*/
noJsBinding?: boolean
/**
* Path and filename of generated type def file. Relative to `--output_dir`
*/
dts?: string
/**
* Custom file header for generated type def file. Only works when `typedef` feature enabled.
*/
dtsHeader?: string
/**
* Whether to disable the default file header for generated type def file. Only works when `typedef` feature enabled.
*/
noDtsHeader?: boolean
/**
* Whether strip the library to achieve the minimum file size
*/
strip?: boolean
/**
* Build in release mode
*/
release?: boolean
/**
* Verbosely log build command trace
*/
verbose?: boolean
/**
* Build only the specified binary
*/
bin?: string
/**
* Build the specified library or the one at cwd
*/
package?: string
/**
* [experimental] cross-compile for the specified target with `cargo-xwin` on windows and `cargo-zigbuild` on other platform
*/
crossCompile?: boolean
/**
* watch the crate changes and build continiously with `cargo-watch` crates
*/
watch?: boolean
/**
* Space-separated list of features to activate
*/
features?: string[]
/**
* Activate all available features
*/
allFeatures?: boolean
/**
* Do not activate the `default` feature
*/
noDefaultFeatures?: boolean
}

View file

@ -0,0 +1,79 @@
// This file is generated by codegen/index.ts
// Do not edit this file manually
import { Command, Option } from 'clipanion'
export abstract class BaseCreateNpmDirsCommand extends Command {
static paths = [['create-npm-dirs']]
static usage = Command.Usage({
description: 'Create npm package dirs for different platforms',
})
cwd = Option.String('--cwd', process.cwd(), {
description:
'The working directory of where napi command will be executed in, all other paths options are relative to this path',
})
packageJsonPath = Option.String('--package-json-path', 'package.json', {
description: 'Path to `package.json`',
})
npmDir = Option.String('--npm-dir', 'npm', {
description: 'Path to the folder where the npm packages put',
})
dryRun = Option.Boolean('--dry-run', false, {
description: 'Dry run without touching file system',
})
getOptions() {
return {
cwd: this.cwd,
packageJsonPath: this.packageJsonPath,
npmDir: this.npmDir,
dryRun: this.dryRun,
}
}
}
/**
* Create npm package dirs for different platforms
*/
export interface CreateNpmDirsOptions {
/**
* The working directory of where napi command will be executed in, all other paths options are relative to this path
*
* @default process.cwd()
*/
cwd?: string
/**
* Path to `package.json`
*
* @default 'package.json'
*/
packageJsonPath?: string
/**
* Path to the folder where the npm packages put
*
* @default 'npm'
*/
npmDir?: string
/**
* Dry run without touching file system
*
* @default false
*/
dryRun?: boolean
}
export function applyDefaultCreateNpmDirsOptions(
options: CreateNpmDirsOptions,
) {
return {
cwd: process.cwd(),
packageJsonPath: 'package.json',
npmDir: 'npm',
dryRun: false,
...options,
}
}

144
cli/src/def/new.ts Normal file
View file

@ -0,0 +1,144 @@
// This file is generated by codegen/index.ts
// Do not edit this file manually
import { Command, Option } from 'clipanion'
import * as typanion from 'typanion'
export abstract class BaseNewCommand extends Command {
static paths = [['new']]
static usage = Command.Usage({
description: 'Create a new project with pre-configured boilerplate',
})
$$path = Option.String({ required: true })
$$name?: string = Option.String('--name,-n', {
description:
'The name of the project, default to the name of the directory if not provided',
})
minNodeApiVersion = Option.String('--min-node-api,-v', '4', {
validator: typanion.isNumber(),
description: 'The minimum Node-API version to support',
})
license = Option.String('--license,-l', 'MIT', {
description: 'License for open-sourced project',
})
targets = Option.Array('--targets,-t', [], {
description: 'All targets the crate will be compiled for.',
})
enableDefaultTargets = Option.Boolean('--enable-default-targets', true, {
description: 'Whether enable default targets',
})
enableAllTargets = Option.Boolean('--enable-all-targets', false, {
description: 'Whether enable all targets',
})
enableTypeDef = Option.Boolean('--enable-type-def', true, {
description:
'Whether enable the `type-def` feature for typescript definitions auto-generation',
})
enableGithubActions = Option.Boolean('--enable-github-actions', true, {
description: 'Whether generate preconfigured GitHub Actions workflow',
})
dryRun = Option.Boolean('--dry-run', false, {
description: 'Whether to run the command in dry-run mode',
})
getOptions() {
return {
path: this.$$path,
name: this.$$name,
minNodeApiVersion: this.minNodeApiVersion,
license: this.license,
targets: this.targets,
enableDefaultTargets: this.enableDefaultTargets,
enableAllTargets: this.enableAllTargets,
enableTypeDef: this.enableTypeDef,
enableGithubActions: this.enableGithubActions,
dryRun: this.dryRun,
}
}
}
/**
* Create a new project with pre-configured boilerplate
*/
export interface NewOptions {
/**
* The path where the napi-rs project will be created.
*/
path: string
/**
* The name of the project, default to the name of the directory if not provided
*/
name?: string
/**
* The minimum Node-API version to support
*
* @default 4
*/
minNodeApiVersion?: number
/**
* License for open-sourced project
*
* @default 'MIT'
*/
license?: string
/**
* All targets the crate will be compiled for.
*
* @default []
*/
targets?: string[]
/**
* Whether enable default targets
*
* @default true
*/
enableDefaultTargets?: boolean
/**
* Whether enable all targets
*
* @default false
*/
enableAllTargets?: boolean
/**
* Whether enable the `type-def` feature for typescript definitions auto-generation
*
* @default true
*/
enableTypeDef?: boolean
/**
* Whether generate preconfigured GitHub Actions workflow
*
* @default true
*/
enableGithubActions?: boolean
/**
* Whether to run the command in dry-run mode
*
* @default false
*/
dryRun?: boolean
}
export function applyDefaultNewOptions(options: NewOptions) {
return {
minNodeApiVersion: 4,
license: 'MIT',
targets: [],
enableDefaultTargets: true,
enableAllTargets: false,
enableTypeDef: true,
enableGithubActions: true,
dryRun: false,
...options,
}
}

120
cli/src/def/pre-publish.ts Normal file
View file

@ -0,0 +1,120 @@
// This file is generated by codegen/index.ts
// Do not edit this file manually
import { Command, Option } from 'clipanion'
export abstract class BasePrePublishCommand extends Command {
static paths = [['pre-publish']]
static usage = Command.Usage({
description:
'Update package.json and copy addons into per platform packages',
})
cwd = Option.String('--cwd', process.cwd(), {
description:
'The working directory of where napi command will be executed in, all other paths options are relative to this path',
})
packageJsonPath = Option.String('--package-json-path', 'package.json', {
description: 'Path to `package.json`',
})
npmDir = Option.String('--npm-dir', 'npm', {
description: 'Path to the folder where the npm packages put',
})
tagStyle = Option.String('--tag-style', 'lerna', {
description: 'git tag style, `npm` or `lerna`',
})
ghRelease = Option.Boolean('--gh-release', true, {
description: 'Whether create GitHub release',
})
ghReleaseName?: string = Option.String('--gh-release-name', {
description: 'GitHub release name',
})
ghReleaseId?: string = Option.String('--gh-release-id', {
description: 'Existing GitHub release id',
})
dryRun = Option.Boolean('--dry-run', false, {
description: 'Dry run without touching file system',
})
getOptions() {
return {
cwd: this.cwd,
packageJsonPath: this.packageJsonPath,
npmDir: this.npmDir,
tagStyle: this.tagStyle,
ghRelease: this.ghRelease,
ghReleaseName: this.ghReleaseName,
ghReleaseId: this.ghReleaseId,
dryRun: this.dryRun,
}
}
}
/**
* Update package.json and copy addons into per platform packages
*/
export interface PrePublishOptions {
/**
* The working directory of where napi command will be executed in, all other paths options are relative to this path
*
* @default process.cwd()
*/
cwd?: string
/**
* Path to `package.json`
*
* @default 'package.json'
*/
packageJsonPath?: string
/**
* Path to the folder where the npm packages put
*
* @default 'npm'
*/
npmDir?: string
/**
* git tag style, `npm` or `lerna`
*
* @default 'lerna'
*/
tagStyle?: 'npm' | 'lerna'
/**
* Whether create GitHub release
*
* @default true
*/
ghRelease?: boolean
/**
* GitHub release name
*/
ghReleaseName?: string
/**
* Existing GitHub release id
*/
ghReleaseId?: string
/**
* Dry run without touching file system
*
* @default false
*/
dryRun?: boolean
}
export function applyDefaultPrePublishOptions(options: PrePublishOptions) {
return {
cwd: process.cwd(),
packageJsonPath: 'package.json',
npmDir: 'npm',
tagStyle: 'lerna',
ghRelease: true,
dryRun: false,
...options,
}
}

122
cli/src/def/rename.ts Normal file
View file

@ -0,0 +1,122 @@
// This file is generated by codegen/index.ts
// Do not edit this file manually
import { Command, Option } from 'clipanion'
export abstract class BaseRenameCommand extends Command {
static paths = [['rename']]
static usage = Command.Usage({
description: 'Rename the napi-rs project',
})
cwd = Option.String('--cwd', process.cwd(), {
description:
'The working directory of where napi command will be executed in, all other paths options are relative to this path',
})
packageJsonPath = Option.String('--package-json-path', 'package.json', {
description: 'Path to `package.json`',
})
npmDir = Option.String('--npm-dir', 'npm', {
description: 'Path to the folder where the npm packages put',
})
$$name?: string = Option.String('--name,-n', {
description: 'The new name of the project',
})
binaryName?: string = Option.String('--binary-name,-b', {
description: 'The new binary name *.node files',
})
packageName?: string = Option.String('--package-name', {
description: 'The new package name of the project',
})
manifestPath = Option.String('--manifest-path', 'Cargo.toml', {
description: 'Path to `Cargo.toml`',
})
repository?: string = Option.String('--repository', {
description: 'The new repository of the project',
})
description?: string = Option.String('--description', {
description: 'The new description of the project',
})
getOptions() {
return {
cwd: this.cwd,
packageJsonPath: this.packageJsonPath,
npmDir: this.npmDir,
name: this.$$name,
binaryName: this.binaryName,
packageName: this.packageName,
manifestPath: this.manifestPath,
repository: this.repository,
description: this.description,
}
}
}
/**
* Rename the napi-rs project
*/
export interface RenameOptions {
/**
* The working directory of where napi command will be executed in, all other paths options are relative to this path
*
* @default process.cwd()
*/
cwd?: string
/**
* Path to `package.json`
*
* @default 'package.json'
*/
packageJsonPath?: string
/**
* Path to the folder where the npm packages put
*
* @default 'npm'
*/
npmDir?: string
/**
* The new name of the project
*/
name?: string
/**
* The new binary name *.node files
*/
binaryName?: string
/**
* The new package name of the project
*/
packageName?: string
/**
* Path to `Cargo.toml`
*
* @default 'Cargo.toml'
*/
manifestPath?: string
/**
* The new repository of the project
*/
repository?: string
/**
* The new description of the project
*/
description?: string
}
export function applyDefaultRenameOptions(options: RenameOptions) {
return {
cwd: process.cwd(),
packageJsonPath: 'package.json',
npmDir: 'npm',
manifestPath: 'Cargo.toml',
...options,
}
}

View file

@ -0,0 +1,66 @@
// This file is generated by codegen/index.ts
// Do not edit this file manually
import { Command, Option } from 'clipanion'
export abstract class BaseUniversalizeCommand extends Command {
static paths = [['universalize']]
static usage = Command.Usage({
description: 'Combile built binaries into one universal binary',
})
cwd = Option.String('--cwd', process.cwd(), {
description:
'The working directory of where napi command will be executed in, all other paths options are relative to this path',
})
packageJsonPath = Option.String('--package-json-path', 'package.json', {
description: 'Path to `package.json`',
})
outputDir = Option.String('--output-dir,-o', './', {
description:
'Path to the folder where all built `.node` files put, same as `--output-dir` of build command',
})
getOptions() {
return {
cwd: this.cwd,
packageJsonPath: this.packageJsonPath,
outputDir: this.outputDir,
}
}
}
/**
* Combile built binaries into one universal binary
*/
export interface UniversalizeOptions {
/**
* The working directory of where napi command will be executed in, all other paths options are relative to this path
*
* @default process.cwd()
*/
cwd?: string
/**
* Path to `package.json`
*
* @default 'package.json'
*/
packageJsonPath?: string
/**
* Path to the folder where all built `.node` files put, same as `--output-dir` of build command
*
* @default './'
*/
outputDir?: string
}
export function applyDefaultUniversalizeOptions(options: UniversalizeOptions) {
return {
cwd: process.cwd(),
packageJsonPath: 'package.json',
outputDir: './',
...options,
}
}

65
cli/src/def/version.ts Normal file
View file

@ -0,0 +1,65 @@
// This file is generated by codegen/index.ts
// Do not edit this file manually
import { Command, Option } from 'clipanion'
export abstract class BaseVersionCommand extends Command {
static paths = [['version']]
static usage = Command.Usage({
description: 'Update version in created npm packages',
})
cwd = Option.String('--cwd', process.cwd(), {
description:
'The working directory of where napi command will be executed in, all other paths options are relative to this path',
})
packageJsonPath = Option.String('--package-json-path', 'package.json', {
description: 'Path to `package.json`',
})
npmDir = Option.String('--npm-dir', 'npm', {
description: 'Path to the folder where the npm packages put',
})
getOptions() {
return {
cwd: this.cwd,
packageJsonPath: this.packageJsonPath,
npmDir: this.npmDir,
}
}
}
/**
* Update version in created npm packages
*/
export interface VersionOptions {
/**
* The working directory of where napi command will be executed in, all other paths options are relative to this path
*
* @default process.cwd()
*/
cwd?: string
/**
* Path to `package.json`
*
* @default 'package.json'
*/
packageJsonPath?: string
/**
* Path to the folder where the npm packages put
*
* @default 'npm'
*/
npmDir?: string
}
export function applyDefaultVersionOptions(options: VersionOptions) {
return {
cwd: process.cwd(),
packageJsonPath: 'package.json',
npmDir: 'npm',
...options,
}
}

View file

@ -1,42 +1,31 @@
import 'core-js/es/string/replace-all'
import { collectArtifacts } from './api/artifacts.js'
import { buildProject } from './api/build.js'
import { createNpmDirs } from './api/create-npm-dirs.js'
import { newProject } from './api/new.js'
import { prePublish } from './api/pre-publish.js'
import { renameProject } from './api/rename.js'
import { universalizeBinaries } from './api/universalize.js'
import { version } from './api/version.js'
import { Cli } from 'clipanion'
import { version } from '../package.json'
import { ArtifactsCommand } from './artifacts'
import { BuildCommand } from './build'
import { CreateNpmDirCommand } from './create-npm-dir'
import { HelpCommand } from './help'
import { NewProjectCommand } from './new'
import { PrePublishCommand } from './pre-publish'
import { RenameCommand } from './rename'
import { UniversalCommand } from './universal'
import { VersionCommand } from './version'
const cli = new Cli({
binaryName: 'napi',
binaryVersion: version,
})
cli.register(ArtifactsCommand)
cli.register(BuildCommand)
cli.register(CreateNpmDirCommand)
cli.register(PrePublishCommand)
cli.register(VersionCommand)
cli.register(UniversalCommand)
cli.register(NewProjectCommand)
cli.register(RenameCommand)
cli.register(HelpCommand)
cli
.run(process.argv.slice(2), {
...Cli.defaultContext,
})
.then((status) => {
process.exit(status)
})
.catch((e) => {
console.error(e)
process.exit(1)
})
/**
*
* @usage
*
* ```ts
* const cli = new NapiCli()
*
* cli.build({
* cwd: '/path/to/your/project',
* })
* ```
*/
export class NapiCli {
artifacts = collectArtifacts
new = newProject
build = buildProject
createNpmDirs = createNpmDirs
prePublish = prePublish
rename = renameProject
universalize = universalizeBinaries
version = version
}

View file

@ -1,258 +0,0 @@
export const createJsBinding = (
localName: string,
pkgName: string,
) => `/* tslint:disable */
/* eslint-disable */
/* prettier-ignore */
/* auto-generated by NAPI-RS */
const { existsSync, readFileSync } = require('fs')
const { join } = require('path')
const { platform, arch } = process
let nativeBinding = null
let localFileExisted = false
let loadError = null
function isMusl() {
// For Node 10
if (!process.report || typeof process.report.getReport !== 'function') {
try {
const lddPath = require('child_process').execSync('which ldd').toString().trim()
return readFileSync(lddPath, 'utf8').includes('musl')
} catch (e) {
return true
}
} else {
const { glibcVersionRuntime } = process.report.getReport().header
return !glibcVersionRuntime
}
}
switch (platform) {
case 'android':
switch (arch) {
case 'arm64':
localFileExisted = existsSync(join(__dirname, '${localName}.android-arm64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.android-arm64.node')
} else {
nativeBinding = require('${pkgName}-android-arm64')
}
} catch (e) {
loadError = e
}
break
case 'arm':
localFileExisted = existsSync(join(__dirname, '${localName}.android-arm-eabi.node'))
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.android-arm-eabi.node')
} else {
nativeBinding = require('${pkgName}-android-arm-eabi')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(\`Unsupported architecture on Android \${arch}\`)
}
break
case 'win32':
switch (arch) {
case 'x64':
localFileExisted = existsSync(
join(__dirname, '${localName}.win32-x64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.win32-x64-msvc.node')
} else {
nativeBinding = require('${pkgName}-win32-x64-msvc')
}
} catch (e) {
loadError = e
}
break
case 'ia32':
localFileExisted = existsSync(
join(__dirname, '${localName}.win32-ia32-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.win32-ia32-msvc.node')
} else {
nativeBinding = require('${pkgName}-win32-ia32-msvc')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, '${localName}.win32-arm64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.win32-arm64-msvc.node')
} else {
nativeBinding = require('${pkgName}-win32-arm64-msvc')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(\`Unsupported architecture on Windows: \${arch}\`)
}
break
case 'darwin':
localFileExisted = existsSync(join(__dirname, '${localName}.darwin-universal.node'))
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.darwin-universal.node')
} else {
nativeBinding = require('${pkgName}-darwin-universal')
}
break
} catch {}
switch (arch) {
case 'x64':
localFileExisted = existsSync(join(__dirname, '${localName}.darwin-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.darwin-x64.node')
} else {
nativeBinding = require('${pkgName}-darwin-x64')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, '${localName}.darwin-arm64.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.darwin-arm64.node')
} else {
nativeBinding = require('${pkgName}-darwin-arm64')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(\`Unsupported architecture on macOS: \${arch}\`)
}
break
case 'freebsd':
if (arch !== 'x64') {
throw new Error(\`Unsupported architecture on FreeBSD: \${arch}\`)
}
localFileExisted = existsSync(join(__dirname, '${localName}.freebsd-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.freebsd-x64.node')
} else {
nativeBinding = require('${pkgName}-freebsd-x64')
}
} catch (e) {
loadError = e
}
break
case 'linux':
switch (arch) {
case 'x64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, '${localName}.linux-x64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.linux-x64-musl.node')
} else {
nativeBinding = require('${pkgName}-linux-x64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, '${localName}.linux-x64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.linux-x64-gnu.node')
} else {
nativeBinding = require('${pkgName}-linux-x64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, '${localName}.linux-arm64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.linux-arm64-musl.node')
} else {
nativeBinding = require('${pkgName}-linux-arm64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, '${localName}.linux-arm64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.linux-arm64-gnu.node')
} else {
nativeBinding = require('${pkgName}-linux-arm64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm':
localFileExisted = existsSync(
join(__dirname, '${localName}.linux-arm-gnueabihf.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./${localName}.linux-arm-gnueabihf.node')
} else {
nativeBinding = require('${pkgName}-linux-arm-gnueabihf')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(\`Unsupported architecture on Linux: \${arch}\`)
}
break
default:
throw new Error(\`Unsupported OS: \${platform}, architecture: \${arch}\`)
}
if (!nativeBinding) {
if (loadError) {
throw loadError
}
throw new Error(\`Failed to load native binding\`)
}
`

View file

@ -1,9 +0,0 @@
export const createCargoConfig = (enableLinuxArm8Musl: boolean) => {
const result: string[] = []
if (enableLinuxArm8Musl) {
result.push(`[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-musl-gcc"
rustflags = ["-C", "target-feature=-crt-static"]`)
}
return result.join('\n')
}

View file

@ -1,19 +0,0 @@
export const createCargoContent = (name: string) => `[package]
edition = "2021"
name = "${name.replace('@', '').replace('/', '_').toLowerCase()}"
version = "0.0.0"
[lib]
crate-type = ["cdylib"]
[dependencies]
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
napi = { version = "NAPI_VERSION", default-features = false, features = ["napi4"] }
napi-derive = "NAPI_DERIVE_VERSION"
[build-dependencies]
napi-build = "NAPI_BUILD_VERSION"
[profile.release]
lto = true
`

View file

@ -1,230 +0,0 @@
import { writeFileSync, mkdirSync } from 'fs'
import { join } from 'path'
import { Command, Option } from 'clipanion'
import * as chalk from 'colorette'
import inquirer from 'inquirer'
import { CreateNpmDirCommand } from '../create-npm-dir'
import { debugFactory } from '../debug'
import { DefaultPlatforms } from '../parse-triple'
import { spawn } from '../spawn'
import { createCargoContent } from './cargo'
import { createCargoConfig } from './cargo-config'
import { createGithubActionsCIYml } from './ci-yml'
import { GitIgnore } from './gitignore-template'
import { LibRs } from './lib-rs'
import { NPMIgnoreFiles } from './npmignore'
import { createPackageJson } from './package'
const NAME_PROMOTE_NAME = 'Package name'
const DIR_PROMOTE_NAME = 'Dir name'
const ENABLE_GITHUB_ACTIONS_PROMOTE_NAME = 'Enable github actions'
const debug = debugFactory('create')
const BUILD_RS = `extern crate napi_build;
fn main() {
napi_build::setup();
}
`
const SupportedPlatforms: string[] = [
'aarch64-apple-darwin',
'aarch64-linux-android',
'aarch64-unknown-linux-gnu',
'aarch64-unknown-linux-musl',
'aarch64-pc-windows-msvc',
'armv7-unknown-linux-gnueabihf',
'x86_64-apple-darwin',
'x86_64-pc-windows-msvc',
'x86_64-unknown-linux-gnu',
'x86_64-unknown-linux-musl',
'x86_64-unknown-freebsd',
'i686-pc-windows-msvc',
'armv7-linux-androideabi',
'universal-apple-darwin',
]
export class NewProjectCommand extends Command {
static usage = Command.Usage({
description: 'Create a new project from scratch',
})
static paths = [['new']]
name?: string = Option.String({
name: '-n,--name',
required: false,
})
dirname?: string = Option.String({
name: '-d,--dirname',
required: false,
})
targets?: string[] = Option.Array('--targets,-t')
dryRun = Option.Boolean(`--dry-run`, false)
enableGithubActions?: boolean = Option.Boolean(`--enable-github-actions`)
async execute() {
await this.getName()
if (!this.dirname) {
const [scope, name] = this.name?.split('/') ?? []
const defaultProjectDir = name ?? scope
const dirAnswer = await inquirer.prompt({
type: 'input',
name: DIR_PROMOTE_NAME,
default: defaultProjectDir,
})
this.dirname = dirAnswer[DIR_PROMOTE_NAME]
}
if (!this.targets) {
const { targets } = await inquirer.prompt([
{
type: 'checkbox',
name: 'targets',
message: 'Choose targets you want to support',
default: DefaultPlatforms.map((p) => p.raw),
choices: SupportedPlatforms,
},
])
if (!targets.length) {
throw new TypeError('At least choose one target')
}
this.targets = targets
}
if (this.enableGithubActions === undefined) {
const answer = await inquirer.prompt([
{
type: 'confirm',
name: ENABLE_GITHUB_ACTIONS_PROMOTE_NAME,
message: 'Enable github actions?',
default: true,
choices: SupportedPlatforms,
},
])
this.enableGithubActions = answer[ENABLE_GITHUB_ACTIONS_PROMOTE_NAME]
}
debug(`Running command: ${chalk.green('[${command}]')}`)
if (!this.dryRun) {
mkdirSync(join(process.cwd(), this.dirname!), {
recursive: true,
})
mkdirSync(join(process.cwd(), this.dirname!, 'src'), {
recursive: true,
})
}
const [s, pkgName] = this.name!.split('/')
const binaryName = pkgName ?? s
this.writeFile('Cargo.toml', createCargoContent(this.name!))
this.writeFile('.npmignore', NPMIgnoreFiles)
this.writeFile('build.rs', BUILD_RS)
this.writeFile(
'package.json',
JSON.stringify(
createPackageJson(this.name!, binaryName, this.targets!),
null,
2,
),
)
this.writeFile('src/lib.rs', LibRs)
mkdirSync(join(process.cwd(), this.dirname!, '__test__'), {
recursive: true,
})
this.writeFile(
'__test__/index.spec.mjs',
`import test from 'ava'
import { sum } from '../index.js'
test('sum from native', (t) => {
t.is(sum(1, 2), 3)
})
`,
)
if (this.enableGithubActions) {
const githubDir = join(process.cwd(), this.dirname!, '.github')
const workflowsDir = join(githubDir, 'workflows')
if (!this.dryRun) {
mkdirSync(githubDir, { recursive: true })
mkdirSync(workflowsDir, { recursive: true })
}
this.writeFile(
join('.github', 'workflows', 'CI.yml'),
createGithubActionsCIYml(binaryName, this.targets!),
)
}
await CreateNpmDirCommand.create(
'package.json',
join(process.cwd(), this.dirname!),
join(process.cwd(), this.dirname!),
)
const enableLinuxArm8Musl = this.targets!.includes(
'aarch64-unknown-linux-musl',
)
const cargoConfig = createCargoConfig(enableLinuxArm8Musl)
if (cargoConfig.length) {
const configDir = join(process.cwd(), this.dirname!, '.cargo')
if (!this.dryRun) {
mkdirSync(configDir, { recursive: true })
this.writeFile(join('.cargo', 'config.toml'), cargoConfig)
}
}
this.writeFile(
'rustfmt.toml',
`tab_spaces = 2
edition = "2021"
`,
)
this.writeFile('.gitignore', GitIgnore)
this.writeFile('.yarnrc.yml', 'nodeLinker: node-modules')
await spawn(`yarn set version stable`, {
cwd: join(process.cwd(), this.dirname!),
})
await spawn(`yarn install`, {
cwd: join(process.cwd(), this.dirname!),
})
}
private writeFile(path: string, content: string) {
const distDir = join(process.cwd(), this.dirname!)
this.context.stdout.write(chalk.green(`Writing ${chalk.blue(path)}\n`))
if (!this.dryRun) {
writeFileSync(join(distDir, path), content)
}
}
private async getName() {
if (!this.name) {
const nameAnswer = await inquirer.prompt({
type: 'input',
name: NAME_PROMOTE_NAME,
suffix: ' (The name filed in your package.json)',
})
const name = nameAnswer[NAME_PROMOTE_NAME]
if (!name) {
await this.getName()
} else {
this.name = name
}
}
}
}

View file

@ -1,64 +0,0 @@
import { version } from '../../package.json'
import { DefaultPlatforms } from '../parse-triple'
export const createPackageJson = (
name: string,
binaryName: string,
targets: string[],
) => {
const pkgContent = {
name,
version: '0.0.0',
main: 'index.js',
types: 'index.d.ts',
napi: {
name: binaryName,
},
license: 'MIT',
devDependencies: {
'@napi-rs/cli': `^${version}`,
ava: '^5.1.1',
},
ava: {
timeout: '3m',
},
engines: {
node: '>= 10',
},
scripts: {
artifacts: 'napi artifacts',
build: 'napi build --platform --release',
'build:debug': 'napi build --platform',
prepublishOnly: 'napi prepublish -t npm',
test: 'ava',
universal: 'napi universal',
version: 'napi version',
},
}
const triples: any = {}
const defaultTargetsSupported = DefaultPlatforms.every((p) =>
targets!.includes(p.raw),
)
const isOnlyDefaultTargets =
targets.length === 3 &&
DefaultPlatforms.every((p) => targets.includes(p.raw))
if (!isOnlyDefaultTargets) {
if (!defaultTargetsSupported) {
triples.defaults = false
triples.additional = targets
} else {
triples.additional = targets.filter(
(t) => !DefaultPlatforms.map((p) => p.raw).includes(t),
)
}
}
// @ts-expect-error
pkgContent.napi.triples = triples
return pkgContent
}

View file

@ -1,242 +0,0 @@
import { existsSync, statSync } from 'fs'
import { join } from 'path'
import { Octokit } from '@octokit/rest'
import { Command, Option } from 'clipanion'
import * as chalk from 'colorette'
import { getNapiConfig } from './consts'
import { debugFactory } from './debug'
import { spawn } from './spawn'
import { updatePackageJson } from './update-package'
import { readFileAsync } from './utils'
import { VersionCommand } from './version'
const debug = debugFactory('prepublish')
interface PackageInfo {
name: string
version: string
tag: string
}
export class PrePublishCommand extends Command {
static usage = Command.Usage({
description:
'Update package.json and copy addons into per platform packages',
})
static paths = [['prepublish']]
prefix = Option.String(`-p,--prefix`, 'npm')
tagStyle: 'npm' | 'lerna' = Option.String('--tagstyle,-t', 'lerna')
configFileName?: string = Option.String('-c,--config')
isDryRun = Option.Boolean('--dry-run', false)
skipGHRelease = Option.Boolean('--skip-gh-release', false)
ghReleaseName?: string = Option.String('--gh-release-name')
existingReleaseId?: string = Option.String('--gh-release-id')
async execute() {
const {
packageJsonPath,
platforms,
version,
packageName,
binaryName,
npmClient,
} = getNapiConfig(this.configFileName)
debug(`Update optionalDependencies in [${packageJsonPath}]`)
if (!this.isDryRun) {
await VersionCommand.updatePackageJson(this.prefix, this.configFileName)
await updatePackageJson(packageJsonPath, {
optionalDependencies: platforms.reduce(
(acc: Record<string, string>, cur) => {
acc[`${packageName}-${cur.platformArchABI}`] = `${version}`
return acc
},
{},
),
})
}
const { owner, repo, pkgInfo, octokit } = this.existingReleaseId
? await this.getRepoInfo(packageName, version)
: await this.createGhRelease(packageName, version)
for (const platformDetail of platforms) {
const pkgDir = join(
process.cwd(),
this.prefix,
`${platformDetail.platformArchABI}`,
)
const filename = `${binaryName}.${platformDetail.platformArchABI}.node`
const dstPath = join(pkgDir, filename)
if (!this.isDryRun) {
if (!existsSync(dstPath)) {
console.warn(`[${chalk.yellowBright(dstPath)}] doesn't exist`)
continue
}
await spawn(`${npmClient} publish`, {
cwd: pkgDir,
env: process.env,
})
if (!this.skipGHRelease && repo && owner) {
debug(
`Start upload [${chalk.greenBright(
dstPath,
)}] to Github release, [${chalk.greenBright(pkgInfo.tag)}]`,
)
try {
const releaseId = this.existingReleaseId
? Number(this.existingReleaseId)
: (
await octokit!.repos.getReleaseByTag({
repo: repo,
owner: owner,
tag: pkgInfo.tag,
})
).data.id
const dstFileStats = statSync(dstPath)
const assetInfo = await octokit!.repos.uploadReleaseAsset({
owner: owner,
repo: repo,
name: filename,
release_id: releaseId,
mediaType: { format: 'raw' },
headers: {
'content-length': dstFileStats.size,
'content-type': 'application/octet-stream',
},
// @ts-expect-error
data: await readFileAsync(dstPath),
})
console.info(`${chalk.green(dstPath)} upload success`)
console.info(
`Download url: ${chalk.blueBright(
assetInfo.data.browser_download_url,
)}`,
)
} catch (e) {
debug(
`Param: ${JSON.stringify(
{ owner, repo, tag: pkgInfo.tag, filename: dstPath },
null,
2,
)}`,
)
console.error(e)
}
}
}
}
}
private async createGhRelease(packageName: string, version: string) {
if (this.skipGHRelease) {
return {
owner: null,
repo: null,
pkgInfo: { name: null, version: null, tag: null },
}
}
const { repo, owner, pkgInfo, octokit } = await this.getRepoInfo(
packageName,
version,
)
if (!repo || !owner) {
return {
owner: null,
repo: null,
pkgInfo: { name: null, version: null, tag: null },
}
}
if (!this.isDryRun) {
try {
await octokit.repos.createRelease({
owner,
repo,
tag_name: pkgInfo.tag,
name: this.ghReleaseName,
prerelease:
version.includes('alpha') ||
version.includes('beta') ||
version.includes('rc'),
})
} catch (e) {
debug(
`Params: ${JSON.stringify(
{ owner, repo, tag_name: pkgInfo.tag },
null,
2,
)}`,
)
console.error(e)
}
}
return { owner, repo, pkgInfo, octokit }
}
private async getRepoInfo(packageName: string, version: string) {
const headCommit = (await spawn('git log -1 --pretty=%B'))
.toString('utf8')
.trim()
const { GITHUB_REPOSITORY } = process.env
if (!GITHUB_REPOSITORY) {
return {
owner: null,
repo: null,
pkgInfo: { name: null, version: null, tag: null },
}
}
debug(`Github repository: ${GITHUB_REPOSITORY}`)
const [owner, repo] = GITHUB_REPOSITORY.split('/')
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
})
let pkgInfo: PackageInfo | undefined
if (this.tagStyle === 'lerna') {
const packagesToPublish = headCommit
.split('\n')
.map((line) => line.trim())
.filter((line, index) => line.length && index)
.map((line) => line.substring(2))
.map(this.parseTag)
pkgInfo = packagesToPublish.find(
(pkgInfo) => pkgInfo.name === packageName,
)
if (!pkgInfo) {
throw new TypeError(
`No release commit found with ${packageName}, original commit info: ${headCommit}`,
)
}
} else {
pkgInfo = {
tag: `v${version}`,
version,
name: packageName,
}
}
return { owner, repo, pkgInfo, octokit }
}
private parseTag(tag: string) {
const segments = tag.split('@')
const version = segments.pop()!
const name = segments.join('@')
return {
name,
version,
tag,
}
}
}

View file

@ -1,121 +0,0 @@
import { join } from 'path'
import { Command, Option } from 'clipanion'
import * as chalk from 'colorette'
import inquirer from 'inquirer'
import { load, dump } from 'js-yaml'
import { debugFactory } from './debug'
import { spawn } from './spawn'
import { readFileAsync, writeFileAsync } from './utils'
const debug = debugFactory('rename')
export class RenameCommand extends Command {
static paths = [['rename']]
name = Option.String('-n', {
required: false,
description: 'The new name of the project',
})
napiName = Option.String('--napi-name', {
required: false,
description: 'The new napi addon name',
})
repository = Option.String('--repository', {
required: false,
description: 'The repository of the package',
})
description = Option.String('-d,--description', {
required: false,
description: 'The description of the package',
})
cwd = Option.String({
required: false,
description: 'The working directory, default is [process.cwd()]',
})
async execute() {
const cwd = this.cwd ?? process.cwd()
const packageJson = await readFileAsync(join(cwd, 'package.json'), 'utf8')
const packageJsonData = JSON.parse(packageJson)
const name =
this.name ??
(
await inquirer.prompt({
name: 'name',
type: 'input',
suffix: chalk.dim(' name field in package.json'),
})
).name
const napiName =
this.napiName ??
(
await inquirer.prompt({
name: 'napi name',
type: 'input',
default: name.split('/')[1],
})
)['napi name']
debug('name: %s, napi name: %s', name, napiName)
packageJsonData.name = name
packageJsonData.napi.name = napiName
const repository =
this.repository ??
(
await inquirer.prompt({
name: 'repository',
type: 'input',
suffix: chalk.dim(' Leave empty to skip'),
})
).repository
if (repository) {
packageJsonData.repository = repository
}
const description =
this.description ??
(
await inquirer.prompt({
name: 'description',
type: 'input',
suffix: chalk.dim(' Leave empty to skip'),
})
).description
if (description) {
packageJsonData.description = description
}
await writeFileAsync(
join(cwd, 'package.json'),
JSON.stringify(packageJsonData, null, 2),
)
const CI = await readFileAsync(
join(cwd, '.github', 'workflows', 'CI.yml'),
'utf8',
)
const CIObject = load(CI) as any
CIObject.env.APP_NAME = napiName
await writeFileAsync(
join(cwd, '.github', 'workflows', 'CI.yml'),
dump(CIObject, {
lineWidth: 1000,
}),
)
let tomlContent = await readFileAsync(join(cwd, 'Cargo.toml'), 'utf8')
tomlContent = tomlContent.replace(
'name = "napi-package-template"',
`name = "${napiName}"`,
)
await writeFileAsync(join(cwd, 'Cargo.toml'), tomlContent)
await spawn('napi create-npm-dir -t .')
}
}

View file

@ -1,30 +0,0 @@
import { spawn as _spawn, SpawnOptionsWithoutStdio } from 'child_process'
import { debugFactory } from './debug'
const debug = debugFactory('spawn')
export function spawn(
command: string,
options: SpawnOptionsWithoutStdio = {},
): Promise<Buffer> {
const [cmd, ...args] = command.split(' ').map((s) => s.trim())
debug(`execute ${cmd} ${args.join(' ')}`)
return new Promise((resolve, reject) => {
const spawnStream = _spawn(cmd, args, { ...options, shell: true })
const chunks: Buffer[] = []
process.stdin.pipe(spawnStream.stdin)
spawnStream.stdout?.on('data', (chunk) => {
chunks.push(chunk)
})
spawnStream.stdout.pipe(process.stdout)
spawnStream.stderr.pipe(process.stderr)
spawnStream.on('close', (code) => {
if (code !== 0) {
reject()
} else {
resolve(Buffer.concat(chunks))
}
})
})
}

View file

@ -1,81 +0,0 @@
import { spawnSync } from 'child_process'
import { join } from 'path'
import { Command, Option } from 'clipanion'
import * as chalk from 'colorette'
import { getNapiConfig } from './consts'
import { debugFactory } from './debug'
import { UniArchsByPlatform } from './parse-triple'
import { fileExists } from './utils'
const debug = debugFactory('universal')
export class UniversalCommand extends Command {
static usage = Command.Usage({
description: 'Combine built binaries to universal binaries',
})
static paths = [['universal']]
sourceDir = Option.String('-d,--dir', 'artifacts')
distDir = Option.String('--dist', '.')
configFileName?: string = Option.String('-c,--config')
buildUniversal: Record<
keyof typeof UniArchsByPlatform,
(binName: string, srcFiles: string[]) => string
> = {
darwin: (binName, srcFiles) => {
const outPath = join(
this.distDir,
`${binName}.${process.platform}-universal.node`,
)
const srcPaths = srcFiles.map((f) => join(this.sourceDir, f))
spawnSync('lipo', ['-create', '-output', outPath, ...srcPaths])
return outPath
},
}
async execute() {
const { platforms, binaryName } = getNapiConfig(this.configFileName)
const targetPlatform = platforms.find(
(p) => p.platform === process.platform && p.arch === 'universal',
)
if (!targetPlatform) {
throw new TypeError(
`'universal' arch for platform '${process.platform}' not found in config!`,
)
}
const srcFiles = UniArchsByPlatform[process.platform]?.map(
(a) => `${binaryName}.${process.platform}-${a}.node`,
)
if (!srcFiles) {
throw new TypeError(
`'universal' arch for platform '${process.platform}' not supported.`,
)
}
debug(
`Looking up source binaries to combine: ${chalk.yellowBright(
srcFiles.join(', '),
)}`,
)
const srcFileLookup = await Promise.all(
srcFiles.map((f) => fileExists(join(this.sourceDir, f))),
)
const notFoundFiles = srcFiles.filter((_f, i) => !srcFileLookup[i])
if (notFoundFiles.length > 0) {
throw new TypeError(
`Some binary files were not found: ${JSON.stringify(notFoundFiles)}`,
)
}
const outPath = this.buildUniversal[process.platform](binaryName, srcFiles)
debug(`Produced universal binary: ${outPath}`)
}
}

View file

@ -1,17 +0,0 @@
import { debugFactory } from './debug'
import { writeFileAsync, fileExists } from './utils'
const debug = debugFactory('update-package')
export async function updatePackageJson(
path: string,
partial: Record<string, any>,
) {
const exists = await fileExists(path)
if (!exists) {
debug(`File not exists ${path}`)
return
}
const old = require(path)
await writeFileAsync(path, JSON.stringify({ ...old, ...partial }, null, 2))
}

View file

@ -1,23 +0,0 @@
import { readFile, writeFile, copyFile, mkdir, unlink, stat } from 'fs'
import { promisify } from 'util'
export const readFileAsync = promisify(readFile)
export const writeFileAsync = promisify(writeFile)
export const unlinkAsync = promisify(unlink)
export const copyFileAsync = promisify(copyFile)
export const mkdirAsync = promisify(mkdir)
export const statAsync = promisify(stat)
export async function fileExists(path: string) {
const exists = await statAsync(path)
.then(() => true)
.catch(() => false)
return exists
}
export function pick<O, K extends keyof O>(o: O, ...keys: K[]): Pick<O, K> {
return keys.reduce((acc, key) => {
acc[key] = o[key]
return acc
}, {} as O)
}

View file

@ -0,0 +1,201 @@
{"kind": "const", "name": "DEFAULT_COST", "js_doc": "/** This is a const */\n", "def": "export const DEFAULT_COST: number", "original_name": "DEFAULT_COST"}
{"kind": "fn", "name": "getWords", "js_doc": "", "def": "export function getWords(): Array<string>"}
{"kind": "fn", "name": "getNums", "js_doc": "/** Gets some numbers */\n", "def": "export function getNums(): Array<number>"}
{"kind": "fn", "name": "sumNums", "js_doc": "", "def": "export function sumNums(nums: Array<number>): number"}
{"kind": "fn", "name": "toJsObj", "js_doc": "", "def": "export function toJsObj(): object"}
{"kind": "fn", "name": "getNumArr", "js_doc": "", "def": "export function getNumArr(): number[]"}
{"kind": "fn", "name": "getNestedNumArr", "js_doc": "", "def": "export function getNestedNumArr(): number[][][]"}
{"kind": "fn", "name": "readFileAsync", "js_doc": "", "def": "export function readFileAsync(path: string): Promise<Buffer>"}
{"kind": "fn", "name": "asyncMultiTwo", "js_doc": "", "def": "export function asyncMultiTwo(arg: number): Promise<number>"}
{"kind": "fn", "name": "bigintAdd", "js_doc": "", "def": "export function bigintAdd(a: bigint, b: bigint): bigint"}
{"kind": "fn", "name": "createBigInt", "js_doc": "", "def": "export function createBigInt(): bigint"}
{"kind": "fn", "name": "createBigIntI64", "js_doc": "", "def": "export function createBigIntI64(): bigint"}
{"kind": "fn", "name": "bigintGetU64AsString", "js_doc": "", "def": "export function bigintGetU64AsString(bi: bigint): string"}
{"kind": "fn", "name": "bigintFromI64", "js_doc": "", "def": "export function bigintFromI64(): bigint"}
{"kind": "fn", "name": "bigintFromI128", "js_doc": "", "def": "export function bigintFromI128(): bigint"}
{"kind": "fn", "name": "getCwd", "js_doc": "", "def": "export function getCwd(callback: (arg0: string) => void): void"}
{"kind": "fn", "name": "optionEnd", "js_doc": "", "def": "export function optionEnd(callback: (arg0: string, arg1?: string | undefined | null) => void): void"}
{"kind": "fn", "name": "optionStart", "js_doc": "", "def": "export function optionStart(callback: (arg0: string | undefined | null, arg1: string) => void): void"}
{"kind": "fn", "name": "optionStartEnd", "js_doc": "", "def": "export function optionStartEnd(callback: (arg0: string | undefined | null, arg1: string, arg2?: string | undefined | null) => void): void"}
{"kind": "fn", "name": "optionOnly", "js_doc": "", "def": "export function optionOnly(callback: (arg0?: string | undefined | null) => void): void"}
{"kind": "fn", "name": "readFile", "js_doc": "/** napi = { version = 2, features = [\"serde-json\"] } */\n", "def": "export function readFile(callback: (arg0: Error | undefined, arg1?: string | undefined | null) => void): void"}
{"kind": "fn", "name": "returnJsFunction", "js_doc": "", "def": "export function returnJsFunction(): (...args: any[]) => any"}
{"kind": "fn", "name": "callbackReturnPromise", "js_doc": "", "def": "export function callbackReturnPromise<T>(functionInput: () => T | Promise<T>, callback: (err: Error | null, result: T) => void): T | Promise<T>"}
{"kind": "fn", "name": "captureErrorInCallback", "js_doc": "", "def": "export function captureErrorInCallback(cb1: () => void, cb2: (arg0: Error) => void): void"}
{"kind": "struct", "name": "Animal", "js_doc": "/**\n * `constructor` option for `struct` requires all fields to be public,\n * otherwise tag impl fn as constructor\n * #[napi(constructor)]\n */\n", "def": "/** Kind of animal */\nreadonly kind: Kind", "original_name": "Animal"}
{"kind": "impl", "name": "Animal", "js_doc": "", "def": "/** This is the constructor */\n constructor(kind: Kind, name: string)\n/** This is a factory method */\nstatic withKind(kind: Kind): Animal\nget name(): string\nset name(name: string)\nget type(): Kind\nset type(kind: Kind)\n/**\n * This is a\n * multi-line comment\n * with an emoji \uD83D\uDE80\n */\n whoami(): string\n/** This is static... */\nstatic getDogKind(): Kind\n/**\n * Here are some characters and character sequences\n * that should be escaped correctly:\n * \\[]{}/\\:\"\"{\n * }\n */\n returnOtherClass(): Dog\n returnOtherClassWithCustomConstructor(): Bird\n overrideIndividualArgOnMethod(normalTy: string, overriddenTy: {n: string}): Bird"}
{"kind": "struct", "name": "Dog", "js_doc": "", "def": "name: string\nconstructor(name: string)", "original_name": "Dog"}
{"kind": "struct", "name": "Bird", "js_doc": "", "def": "name: string", "original_name": "Bird"}
{"kind": "impl", "name": "Bird", "js_doc": "", "def": " constructor(name: string)\n getCount(): number\n getNameAsync(): Promise<string>"}
{"kind": "struct", "name": "Blake2BHasher", "js_doc": "/** Smoking test for type generation */\n", "def": "", "original_name": "Blake2bHasher"}
{"kind": "impl", "name": "Blake2BHasher", "js_doc": "", "def": "static withKey(key: Blake2bKey): Blake2BHasher"}
{"kind": "impl", "name": "Blake2BHasher", "js_doc": "", "def": " update(data: Buffer): void"}
{"kind": "struct", "name": "Blake2BKey", "js_doc": "", "def": "", "original_name": "Blake2bKey"}
{"kind": "struct", "name": "Context", "js_doc": "", "def": "maybeNeed?: boolean\nbuffer: Uint8Array", "original_name": "Context"}
{"kind": "impl", "name": "Context", "js_doc": "", "def": " constructor()\nstatic withData(data: string): Context\nstatic withBuffer(buf: Uint8Array): Context\n method(): string"}
{"kind": "struct", "name": "AnimalWithDefaultConstructor", "js_doc": "", "def": "name: string\nkind: number\nconstructor(name: string, kind: number)", "original_name": "AnimalWithDefaultConstructor"}
{"kind": "struct", "name": "NinjaTurtle", "js_doc": "", "def": "name: string", "original_name": "NinjaTurtle"}
{"kind": "impl", "name": "NinjaTurtle", "js_doc": "", "def": "static isInstanceOf(value: unknown): boolean\n/** Create your ninja turtle! \uD83D\uDC22 */\nstatic newRaph(): NinjaTurtle\n getMaskColor(): string\n getName(): string\n returnThis(this: this): this"}
{"kind": "struct", "name": "Assets", "js_doc": "", "def": "", "original_name": "JsAssets"}
{"kind": "impl", "name": "Assets", "js_doc": "", "def": " constructor()\n get(id: number): JsAsset | null"}
{"kind": "struct", "name": "Asset", "js_doc": "", "def": "", "original_name": "JsAsset"}
{"kind": "impl", "name": "Asset", "js_doc": "", "def": " constructor()\nget filePath(): number"}
{"kind": "struct", "name": "Optional", "js_doc": "", "def": "", "original_name": "Optional"}
{"kind": "impl", "name": "Optional", "js_doc": "", "def": "static optionEnd(required: string, optional?: string | undefined | null): string\nstatic optionStart(optional: string | undefined | null, required: string): string\nstatic optionStartEnd(optional1: string | undefined | null, required: string, optional2?: string | undefined | null): string\nstatic optionOnly(optional?: string | undefined | null): string"}
{"kind": "interface", "name": "ObjectFieldClassInstance", "js_doc": "", "def": "bird: Bird", "original_name": "ObjectFieldClassInstance"}
{"kind": "fn", "name": "createObjectWithClassField", "js_doc": "", "def": "export function createObjectWithClassField(): ObjectFieldClassInstance"}
{"kind": "fn", "name": "receiveObjectWithClassField", "js_doc": "", "def": "export function receiveObjectWithClassField(object: ObjectFieldClassInstance): Bird"}
{"kind": "struct", "name": "NotWritableClass", "js_doc": "", "def": "name: string\nconstructor(name: string)", "original_name": "NotWritableClass"}
{"kind": "impl", "name": "NotWritableClass", "js_doc": "", "def": " setName(name: string): void"}
{"kind": "struct", "name": "CustomFinalize", "js_doc": "", "def": "", "original_name": "CustomFinalize"}
{"kind": "impl", "name": "CustomFinalize", "js_doc": "", "def": " constructor(width: number, height: number)"}
{"kind": "struct", "name": "Width", "js_doc": "", "def": "value: number\nconstructor(value: number)", "original_name": "Width"}
{"kind": "fn", "name": "plusOne", "js_doc": "", "def": "export function plusOne(this: Width): number"}
{"kind": "struct", "name": "ClassWithFactory", "js_doc": "", "def": "name: string", "original_name": "ClassWithFactory"}
{"kind": "impl", "name": "ClassWithFactory", "js_doc": "", "def": "static withName(name: string): ClassWithFactory\n setName(name: string): this"}
{"kind": "fn", "name": "dateToNumber", "js_doc": "", "def": "export function dateToNumber(input: Date): number"}
{"kind": "fn", "name": "chronoDateToMillis", "js_doc": "", "def": "export function chronoDateToMillis(input: Date): number"}
{"kind": "fn", "name": "chronoDateAdd1Minute", "js_doc": "", "def": "export function chronoDateAdd1Minute(input: Date): Date"}
{"kind": "interface", "name": "Dates", "js_doc": "", "def": "start: Date\nend?: Date", "original_name": "Dates"}
{"kind": "fn", "name": "eitherStringOrNumber", "js_doc": "", "def": "export function eitherStringOrNumber(input: string | number): number"}
{"kind": "fn", "name": "returnEither", "js_doc": "", "def": "export function returnEither(input: number): string | number"}
{"kind": "fn", "name": "either3", "js_doc": "", "def": "export function either3(input: string | number | boolean): number"}
{"kind": "interface", "name": "Obj", "js_doc": "", "def": "v: string | number", "original_name": "Obj"}
{"kind": "fn", "name": "either4", "js_doc": "", "def": "export function either4(input: string | number | boolean | Obj): number"}
{"kind": "struct", "name": "JsClassForEither", "js_doc": "", "def": "", "original_name": "JsClassForEither"}
{"kind": "impl", "name": "JsClassForEither", "js_doc": "", "def": " constructor()"}
{"kind": "struct", "name": "AnotherClassForEither", "js_doc": "", "def": "", "original_name": "AnotherClassForEither"}
{"kind": "impl", "name": "AnotherClassForEither", "js_doc": "", "def": " constructor()"}
{"kind": "fn", "name": "receiveClassOrNumber", "js_doc": "", "def": "export function receiveClassOrNumber(either: number | JsClassForEither): number"}
{"kind": "fn", "name": "receiveMutClassOrNumber", "js_doc": "", "def": "export function receiveMutClassOrNumber(either: number | JsClassForEither): number"}
{"kind": "fn", "name": "receiveDifferentClass", "js_doc": "", "def": "export function receiveDifferentClass(either: JsClassForEither | AnotherClassForEither): number"}
{"kind": "fn", "name": "returnEitherClass", "js_doc": "", "def": "export function returnEitherClass(input: number): number | JsClassForEither"}
{"kind": "fn", "name": "eitherFromOption", "js_doc": "", "def": "export function eitherFromOption(): JsClassForEither | undefined"}
{"kind": "interface", "name": "A", "js_doc": "", "def": "foo: number", "original_name": "A"}
{"kind": "interface", "name": "B", "js_doc": "", "def": "bar: number", "original_name": "B"}
{"kind": "interface", "name": "C", "js_doc": "", "def": "baz: number", "original_name": "C"}
{"kind": "fn", "name": "eitherFromObjects", "js_doc": "", "def": "export function eitherFromObjects(input: A | B | C): string"}
{"kind": "fn", "name": "eitherBoolOrFunction", "js_doc": "", "def": "export function eitherBoolOrFunction(input: boolean | ((...args: any[]) => any)): void"}
{"kind": "fn", "name": "promiseInEither", "js_doc": "", "def": "export function promiseInEither(input: number | Promise<number>): Promise<boolean>"}
{"kind": "enum", "name": "Kind", "js_doc": "/** default enum values are continuos i32s start from 0 */\n", "def": "/** Barks */\nDog = 0,\n /** Kills birds */\nCat = 1,\n /** Tasty */\nDuck = 2", "original_name": "Kind"}
{"kind": "enum", "name": "Empty", "js_doc": "", "def": "", "original_name": "Empty"}
{"kind": "enum", "name": "CustomNumEnum", "js_doc": "/** You could break the step and for an new continuous value. */\n", "def": "One = 1,\n Two = 2,\n Three = 3,\n Four = 4,\n Six = 6,\n Eight = 8,\n Nine = 9,\n Ten = 10", "original_name": "CustomNumEnum"}
{"kind": "fn", "name": "enumToI32", "js_doc": "", "def": "export function enumToI32(e: CustomNumEnum): number"}
{"kind": "fn", "name": "throwError", "js_doc": "", "def": "export function throwError(): void"}
{"kind": "fn", "name": "panic", "js_doc": "", "def": "export function panic(): void"}
{"kind": "fn", "name": "receiveString", "js_doc": "", "def": "export function receiveString(s: string): string"}
{"kind": "fn", "name": "customStatusCode", "js_doc": "", "def": "export function customStatusCode(): void"}
{"kind": "fn", "name": "createExternal", "js_doc": "", "def": "export function createExternal(size: number): ExternalObject<number>"}
{"kind": "fn", "name": "createExternalString", "js_doc": "", "def": "export function createExternalString(content: string): ExternalObject<string>"}
{"kind": "fn", "name": "getExternal", "js_doc": "", "def": "export function getExternal(external: ExternalObject<number>): number"}
{"kind": "fn", "name": "mutateExternal", "js_doc": "", "def": "export function mutateExternal(external: ExternalObject<number>, newVal: number): void"}
{"kind": "fn", "name": "validateArray", "js_doc": "", "def": "export function validateArray(arr: Array<number>): number"}
{"kind": "fn", "name": "validateBuffer", "js_doc": "", "def": "export function validateBuffer(b: Buffer): number"}
{"kind": "fn", "name": "validateTypedArray", "js_doc": "", "def": "export function validateTypedArray(input: Uint8Array): number"}
{"kind": "fn", "name": "validateBigint", "js_doc": "", "def": "export function validateBigint(input: bigint): bigint"}
{"kind": "fn", "name": "validateBoolean", "js_doc": "", "def": "export function validateBoolean(i: boolean): boolean"}
{"kind": "fn", "name": "validateDate", "js_doc": "", "def": "export function validateDate(d: Date): number"}
{"kind": "fn", "name": "validateDateTime", "js_doc": "", "def": "export function validateDateTime(d: Date): number"}
{"kind": "fn", "name": "validateExternal", "js_doc": "", "def": "export function validateExternal(e: ExternalObject<number>): number"}
{"kind": "fn", "name": "validateFunction", "js_doc": "", "def": "export function validateFunction(cb: () => number): number"}
{"kind": "fn", "name": "validateHashMap", "js_doc": "", "def": "export function validateHashMap(input: Record<string, number>): number"}
{"kind": "fn", "name": "validateNull", "js_doc": "", "def": "export function validateNull(i: null): boolean"}
{"kind": "fn", "name": "validateUndefined", "js_doc": "", "def": "export function validateUndefined(i: undefined): boolean"}
{"kind": "fn", "name": "validateNumber", "js_doc": "", "def": "export function validateNumber(i: number): number"}
{"kind": "fn", "name": "validatePromise", "js_doc": "", "def": "export function validatePromise(p: Promise<number>): Promise<number>"}
{"kind": "fn", "name": "validateString", "js_doc": "", "def": "export function validateString(s: string): string"}
{"kind": "fn", "name": "validateSymbol", "js_doc": "", "def": "export function validateSymbol(s: symbol): boolean"}
{"kind": "fn", "name": "validateOptional", "js_doc": "", "def": "export function validateOptional(input1?: string | undefined | null, input2?: boolean | undefined | null): boolean"}
{"kind": "fn", "name": "returnUndefinedIfInvalid", "js_doc": "", "def": "export function returnUndefinedIfInvalid(input: boolean): boolean"}
{"kind": "fn", "name": "returnUndefinedIfInvalidPromise", "js_doc": "", "def": "export function returnUndefinedIfInvalidPromise(input: Promise<boolean>): Promise<boolean>"}
{"kind": "fn", "name": "tsRename", "js_doc": "", "def": "export function tsRename(a: { foo: number }): string[]"}
{"kind": "fn", "name": "overrideIndividualArgOnFunction", "js_doc": "", "def": "export function overrideIndividualArgOnFunction(notOverridden: string, f: () => string, notOverridden2: number): string"}
{"kind": "fn", "name": "overrideIndividualArgOnFunctionWithCbArg", "js_doc": "", "def": "export function overrideIndividualArgOnFunctionWithCbArg(callback: (town: string, name?: string | undefined | null) => string, notOverridden: number): object"}
{"kind": "struct", "name": "Fib", "js_doc": "", "def": "", "original_name": "Fib"}
{"kind": "impl", "name": "Fib", "js_doc": "", "def": "[Symbol.iterator](): Iterator<number, void, number>"}
{"kind": "impl", "name": "Fib", "js_doc": "", "def": " constructor()"}
{"kind": "struct", "name": "Fib2", "js_doc": "", "def": "", "original_name": "Fib2"}
{"kind": "impl", "name": "Fib2", "js_doc": "", "def": "[Symbol.iterator](): Iterator<number, void, number>"}
{"kind": "impl", "name": "Fib2", "js_doc": "", "def": "static create(seed: number): Fib2"}
{"kind": "struct", "name": "Fib3", "js_doc": "", "def": "current: number\nnext: number\nconstructor(current: number, next: number)", "original_name": "Fib3"}
{"kind": "impl", "name": "Fib3", "js_doc": "", "def": "[Symbol.iterator](): Iterator<number, void, number>"}
{"kind": "const", "name": "ALIGNMENT", "js_doc": "", "def": "export const ALIGNMENT: number", "original_name": "ALIGNMENT", "js_mod": "xxh3"}
{"kind": "fn", "name": "xxh3_64", "js_doc": "", "def": "export function xxh3_64(input: Buffer): bigint", "js_mod": "xxh3"}
{"kind": "fn", "name": "xxh128", "js_doc": "/** xxh128 function */\n", "def": "export function xxh128(input: Buffer): bigint", "js_mod": "xxh3"}
{"kind": "struct", "name": "Xxh3", "js_doc": "/** Xxh3 class */\n", "def": "", "original_name": "Xxh3", "js_mod": "xxh3"}
{"kind": "impl", "name": "Xxh3", "js_doc": "", "def": " constructor()\n/** update */\n update(input: Buffer): void\n digest(): bigint", "js_mod": "xxh3"}
{"kind": "fn", "name": "xxh2Plus", "js_doc": "", "def": "export function xxh2Plus(a: number, b: number): number", "js_mod": "xxh2"}
{"kind": "fn", "name": "xxh3Xxh64Alias", "js_doc": "", "def": "export function xxh3Xxh64Alias(input: Buffer): bigint", "js_mod": "xxh2"}
{"kind": "fn", "name": "xxh64Alias", "js_doc": "", "def": "export function xxh64Alias(input: Buffer): bigint"}
{"kind": "fn", "name": "getMapping", "js_doc": "", "def": "export function getMapping(): Record<string, number>"}
{"kind": "fn", "name": "sumMapping", "js_doc": "", "def": "export function sumMapping(nums: Record<string, number>): number"}
{"kind": "fn", "name": "mapOption", "js_doc": "", "def": "export function mapOption(val?: number | undefined | null): number | null"}
{"kind": "fn", "name": "returnNull", "js_doc": "", "def": "export function returnNull(): null"}
{"kind": "fn", "name": "returnUndefined", "js_doc": "", "def": "export function returnUndefined(): void"}
{"kind": "fn", "name": "add", "js_doc": "", "def": "export function add(a: number, b: number): number"}
{"kind": "fn", "name": "fibonacci", "js_doc": "", "def": "export function fibonacci(n: number): number"}
{"kind": "fn", "name": "listObjKeys", "js_doc": "", "def": "export function listObjKeys(obj: object): Array<string>"}
{"kind": "fn", "name": "createObj", "js_doc": "", "def": "export function createObj(): object"}
{"kind": "fn", "name": "getGlobal", "js_doc": "", "def": "export function getGlobal(): typeof global"}
{"kind": "fn", "name": "getUndefined", "js_doc": "", "def": "export function getUndefined(): void"}
{"kind": "fn", "name": "getNull", "js_doc": "", "def": "export function getNull(): null"}
{"kind": "interface", "name": "AllOptionalObject", "js_doc": "", "def": "name?: string\nage?: number", "original_name": "AllOptionalObject"}
{"kind": "fn", "name": "receiveAllOptionalObject", "js_doc": "", "def": "export function receiveAllOptionalObject(obj?: AllOptionalObject | undefined | null): void"}
{"kind": "enum", "name": "ALIAS", "js_doc": "", "def": "A = 0,\n B = 1", "original_name": "AliasedEnum"}
{"kind": "interface", "name": "AliasedStruct", "js_doc": "", "def": "a: ALIAS\nb: number", "original_name": "StructContainsAliasedEnum"}
{"kind": "fn", "name": "fnReceivedAliased", "js_doc": "", "def": "export function fnReceivedAliased(s: AliasedStruct, e: ALIAS): void"}
{"kind": "interface", "name": "StrictObject", "js_doc": "", "def": "name: string", "original_name": "StrictObject"}
{"kind": "fn", "name": "receiveStrictObject", "js_doc": "", "def": "export function receiveStrictObject(strictObject: StrictObject): void"}
{"kind": "fn", "name": "getStrFromObject", "js_doc": "", "def": "export function getStrFromObject(): void"}
{"kind": "interface", "name": "TsTypeChanged", "js_doc": "", "def": "typeOverride: object\ntypeOverrideOptional?: object", "original_name": "TsTypeChanged"}
{"kind": "fn", "name": "createObjWithProperty", "js_doc": "", "def": "export function createObjWithProperty(): { value: ArrayBuffer, get getter(): number }"}
{"kind": "fn", "name": "getterFromObj", "js_doc": "", "def": "export function getterFromObj(): number"}
{"kind": "interface", "name": "ObjectOnlyFromJs", "js_doc": "", "def": "count: number\ncallback: (err: Error | null, value: number) => any", "original_name": "ObjectOnlyFromJs"}
{"kind": "fn", "name": "receiveObjectOnlyFromJs", "js_doc": "", "def": "export function receiveObjectOnlyFromJs(obj: { count: number, callback: (err: Error | null, count: number) => void }): void"}
{"kind": "fn", "name": "asyncPlus100", "js_doc": "", "def": "export function asyncPlus100(p: Promise<number>): Promise<number>"}
{"kind": "struct", "name": "JsRepo", "js_doc": "", "def": "", "original_name": "JsRepo"}
{"kind": "impl", "name": "JsRepo", "js_doc": "", "def": " constructor(dir: string)\n remote(): JsRemote"}
{"kind": "struct", "name": "JsRemote", "js_doc": "", "def": "", "original_name": "JsRemote"}
{"kind": "impl", "name": "JsRemote", "js_doc": "", "def": " name(): string"}
{"kind": "struct", "name": "CssRuleList", "js_doc": "", "def": "", "original_name": "CSSRuleList"}
{"kind": "impl", "name": "CssRuleList", "js_doc": "", "def": " getRules(): Array<string>\nget parentStyleSheet(): CSSStyleSheet\nget name(): string | null"}
{"kind": "struct", "name": "CssStyleSheet", "js_doc": "", "def": "", "original_name": "CSSStyleSheet"}
{"kind": "struct", "name": "AnotherCssStyleSheet", "js_doc": "", "def": "", "original_name": "AnotherCSSStyleSheet"}
{"kind": "impl", "name": "AnotherCssStyleSheet", "js_doc": "", "def": "get rules(): CssRuleList"}
{"kind": "impl", "name": "CssStyleSheet", "js_doc": "", "def": " constructor(name: string, rules: Array<string>)\nget rules(): CssRuleList\n anotherCssStyleSheet(): AnotherCssStyleSheet"}
{"kind": "interface", "name": "PackageJson", "js_doc": "/** This is an interface for package.json */\n", "def": "name: string\n/** The version of the package */\nversion: string\ndependencies?: Record<string, any>\ndevDependencies?: Record<string, any>", "original_name": "PackageJson"}
{"kind": "fn", "name": "readPackageJson", "js_doc": "", "def": "export function readPackageJson(): PackageJson"}
{"kind": "fn", "name": "getPackageJsonName", "js_doc": "", "def": "export function getPackageJsonName(packageJson: PackageJson): string"}
{"kind": "fn", "name": "testSerdeRoundtrip", "js_doc": "", "def": "export function testSerdeRoundtrip(data: any): any"}
{"kind": "fn", "name": "contains", "js_doc": "", "def": "export function contains(source: string, target: string): boolean"}
{"kind": "fn", "name": "concatStr", "js_doc": "", "def": "export function concatStr(s: string): string"}
{"kind": "fn", "name": "concatUtf16", "js_doc": "", "def": "export function concatUtf16(s: string): string"}
{"kind": "fn", "name": "concatLatin1", "js_doc": "", "def": "export function concatLatin1(s: string): string"}
{"kind": "fn", "name": "roundtripStr", "js_doc": "", "def": "export function roundtripStr(s: string): string"}
{"kind": "fn", "name": "setSymbolInObj", "js_doc": "", "def": "export function setSymbolInObj(symbol: symbol): object"}
{"kind": "fn", "name": "createSymbol", "js_doc": "", "def": "export function createSymbol(): symbol"}
{"kind": "impl", "name": "DelaySum", "js_doc": "", "def": ""}
{"kind": "fn", "name": "withoutAbortController", "js_doc": "", "def": "export function withoutAbortController(a: number, b: number): Promise<number>"}
{"kind": "fn", "name": "withAbortController", "js_doc": "", "def": "export function withAbortController(a: number, b: number, signal: AbortSignal): Promise<number>"}
{"kind": "fn", "name": "callThreadsafeFunction", "js_doc": "", "def": "export function callThreadsafeFunction(callback: (...args: any[]) => any): void"}
{"kind": "fn", "name": "threadsafeFunctionThrowError", "js_doc": "", "def": "export function threadsafeFunctionThrowError(cb: (...args: any[]) => any): void"}
{"kind": "fn", "name": "threadsafeFunctionFatalMode", "js_doc": "", "def": "export function threadsafeFunctionFatalMode(cb: (...args: any[]) => any): void"}
{"kind": "fn", "name": "threadsafeFunctionFatalModeError", "js_doc": "", "def": "export function threadsafeFunctionFatalModeError(cb: (...args: any[]) => any): void"}
{"kind": "fn", "name": "threadsafeFunctionClosureCapture", "js_doc": "", "def": "export function threadsafeFunctionClosureCapture(func: (...args: any[]) => any): void"}
{"kind": "fn", "name": "tsfnCallWithCallback", "js_doc": "", "def": "export function tsfnCallWithCallback(func: (...args: any[]) => any): void"}
{"kind": "fn", "name": "tsfnAsyncCall", "js_doc": "", "def": "export function tsfnAsyncCall(func: (...args: any[]) => any): Promise<void>"}
{"kind": "fn", "name": "acceptThreadsafeFunction", "js_doc": "", "def": "export function acceptThreadsafeFunction(func: (err: Error | null, value: number) => any): void"}
{"kind": "fn", "name": "acceptThreadsafeFunctionFatal", "js_doc": "", "def": "export function acceptThreadsafeFunctionFatal(func: (value: number) => any): void"}
{"kind": "fn", "name": "acceptThreadsafeFunctionTupleArgs", "js_doc": "", "def": "export function acceptThreadsafeFunctionTupleArgs(func: (err: Error | null, arg0: number, arg1: boolean, arg2: string) => any): void"}
{"kind": "fn", "name": "getBuffer", "js_doc": "", "def": "export function getBuffer(): Buffer"}
{"kind": "fn", "name": "appendBuffer", "js_doc": "", "def": "export function appendBuffer(buf: Buffer): Buffer"}
{"kind": "fn", "name": "getEmptyBuffer", "js_doc": "", "def": "export function getEmptyBuffer(): Buffer"}
{"kind": "fn", "name": "convertU32Array", "js_doc": "", "def": "export function convertU32Array(input: Uint32Array): Array<number>"}
{"kind": "fn", "name": "createExternalTypedArray", "js_doc": "", "def": "export function createExternalTypedArray(): Uint32Array"}
{"kind": "fn", "name": "mutateTypedArray", "js_doc": "", "def": "export function mutateTypedArray(input: Float32Array): void"}
{"kind": "fn", "name": "derefUint8Array", "js_doc": "", "def": "export function derefUint8Array(a: Uint8Array, b: Uint8ClampedArray): number"}
{"kind": "fn", "name": "bufferPassThrough", "js_doc": "", "def": "export function bufferPassThrough(buf: Buffer): Promise<Buffer>"}
{"kind": "fn", "name": "arrayBufferPassThrough", "js_doc": "", "def": "export function arrayBufferPassThrough(buf: Uint8Array): Promise<Uint8Array>"}
{"kind": "impl", "name": "AsyncBuffer", "js_doc": "", "def": ""}
{"kind": "fn", "name": "asyncReduceBuffer", "js_doc": "", "def": "export function asyncReduceBuffer(buf: Buffer): Promise<number>"}
{"kind": "fn", "name": "runScript", "js_doc": "", "def": "export function runScript(script: string): unknown"}

View file

@ -0,0 +1,110 @@
# Snapshot report for `src/utils/__tests__/target.spec.ts`
The actual snapshot is saved in `target.spec.ts.snap`.
Generated by [AVA](https://avajs.dev).
## should parse triple correctly
> Snapshot 1
[
{
abi: null,
arch: 'arm64',
platform: 'darwin',
platformArchABI: 'darwin-arm64',
triple: 'aarch64-apple-darwin',
},
{
abi: null,
arch: 'arm64',
platform: 'android',
platformArchABI: 'android-arm64',
triple: 'aarch64-linux-android',
},
{
abi: 'gnu',
arch: 'arm64',
platform: 'linux',
platformArchABI: 'linux-arm64-gnu',
triple: 'aarch64-unknown-linux-gnu',
},
{
abi: 'musl',
arch: 'arm64',
platform: 'linux',
platformArchABI: 'linux-arm64-musl',
triple: 'aarch64-unknown-linux-musl',
},
{
abi: 'msvc',
arch: 'arm64',
platform: 'win32',
platformArchABI: 'win32-arm64-msvc',
triple: 'aarch64-pc-windows-msvc',
},
{
abi: null,
arch: 'x64',
platform: 'darwin',
platformArchABI: 'darwin-x64',
triple: 'x86_64-apple-darwin',
},
{
abi: 'msvc',
arch: 'x64',
platform: 'win32',
platformArchABI: 'win32-x64-msvc',
triple: 'x86_64-pc-windows-msvc',
},
{
abi: 'gnu',
arch: 'x64',
platform: 'linux',
platformArchABI: 'linux-x64-gnu',
triple: 'x86_64-unknown-linux-gnu',
},
{
abi: 'musl',
arch: 'x64',
platform: 'linux',
platformArchABI: 'linux-x64-musl',
triple: 'x86_64-unknown-linux-musl',
},
{
abi: null,
arch: 'x64',
platform: 'freebsd',
platformArchABI: 'freebsd-x64',
triple: 'x86_64-unknown-freebsd',
},
{
abi: 'msvc',
arch: 'ia32',
platform: 'win32',
platformArchABI: 'win32-ia32-msvc',
triple: 'i686-pc-windows-msvc',
},
{
abi: 'gnueabihf',
arch: 'arm',
platform: 'linux',
platformArchABI: 'linux-arm-gnueabihf',
triple: 'armv7-unknown-linux-gnueabihf',
},
{
abi: 'eabi',
arch: 'arm',
platform: 'android',
platformArchABI: 'android-arm-eabi',
triple: 'armv7-linux-androideabi',
},
{
abi: null,
arch: 'universal',
platform: 'darwin',
platformArchABI: 'darwin-universal',
triple: 'universal-apple-darwin',
},
]

View file

@ -0,0 +1,618 @@
# Snapshot report for `src/utils/__tests__/typegen.spec.ts`
The actual snapshot is saved in `typegen.spec.ts.snap`.
Generated by [AVA](https://avajs.dev).
## should ident string correctly
> original ident is 0
`␊
/**␊
* should keep␊
* class A {␊
* foo = () => {}␊
* bar = () => {}␊
* }␊
*/␊
class A {␊
foo() {␊
a = b␊
}␊
bar = () => {␊
}␊
boz = 1␊
}␊
namespace B {␊
namespace C {␊
type D = A␊
}␊
}␊
`
> original ident is 2
`␊
/**␊
* should keep␊
* class A {␊
* foo = () => {}␊
* bar = () => {}␊
* }␊
*/␊
class A {␊
foo() {␊
a = b␊
}␊
bar = () => {␊
}␊
boz = 1␊
}␊
namespace B {␊
namespace C {␊
type D = A␊
}␊
}␊
`
## should process type def correctly
> Snapshot 1
`␊
export class ExternalObject<T> {␊
readonly '': {␊
readonly '': unique symbol␊
[K: symbol]: T␊
}␊
}␊
/**␊
* \`constructor\` option for \`struct\` requires all fields to be public,␊
* otherwise tag impl fn as constructor␊
* #[napi(constructor)]␊
*/␊
export class Animal {␊
/** Kind of animal */␊
readonly kind: Kind␊
/** This is the constructor */␊
constructor(kind: Kind, name: string)␊
/** This is a factory method */␊
static withKind(kind: Kind): Animal␊
get name(): string␊
set name(name: string)␊
get type(): Kind␊
set type(kind: Kind)␊
/**␊
* This is a␊
* multi-line comment␊
* with an emoji 🚀␊
*/␊
whoami(): string␊
/** This is static... */␊
static getDogKind(): Kind␊
/**␊
* Here are some characters and character sequences␊
* that should be escaped correctly:␊
* \\[]{}/\\:""{␊
* }␊
*/␊
returnOtherClass(): Dog␊
returnOtherClassWithCustomConstructor(): Bird␊
overrideIndividualArgOnMethod(normalTy: string, overriddenTy: {n: string}): Bird␊
}␊
export class AnimalWithDefaultConstructor {␊
name: string␊
kind: number␊
constructor(name: string, kind: number)␊
}␊
export class AnotherClassForEither {␊
constructor()␊
}␊
export class AnotherCssStyleSheet {␊
get rules(): CssRuleList␊
}␊
export type AnotherCSSStyleSheet = AnotherCssStyleSheet␊
export class Asset {␊
constructor()␊
get filePath(): number␊
}␊
export type JsAsset = Asset␊
export class Assets {␊
constructor()␊
get(id: number): JsAsset | null␊
}␊
export type JsAssets = Assets␊
export class Bird {␊
name: string␊
constructor(name: string)␊
getCount(): number␊
getNameAsync(): Promise<string>
}␊
/** Smoking test for type generation */␊
export class Blake2BHasher {␊
static withKey(key: Blake2bKey): Blake2BHasher␊
update(data: Buffer): void␊
}␊
export type Blake2bHasher = Blake2BHasher␊
export class Blake2BKey {␊
}␊
export type Blake2bKey = Blake2BKey␊
export class ClassWithFactory {␊
name: string␊
static withName(name: string): ClassWithFactory␊
setName(name: string): this␊
}␊
export class Context {␊
maybeNeed?: boolean␊
buffer: Uint8Array␊
constructor()␊
static withData(data: string): Context␊
static withBuffer(buf: Uint8Array): Context␊
method(): string␊
}␊
export class CssRuleList {␊
getRules(): Array<string>
get parentStyleSheet(): CSSStyleSheet␊
get name(): string | null␊
}␊
export type CSSRuleList = CssRuleList␊
export class CssStyleSheet {␊
constructor(name: string, rules: Array<string>)␊
get rules(): CssRuleList␊
anotherCssStyleSheet(): AnotherCssStyleSheet␊
}␊
export type CSSStyleSheet = CssStyleSheet␊
export class CustomFinalize {␊
constructor(width: number, height: number)␊
}␊
export class Dog {␊
name: string␊
constructor(name: string)␊
}␊
export class Fib {␊
[Symbol.iterator](): Iterator<number, void, number>
constructor()␊
}␊
export class Fib2 {␊
[Symbol.iterator](): Iterator<number, void, number>
static create(seed: number): Fib2␊
}␊
export class Fib3 {␊
current: number␊
next: number␊
constructor(current: number, next: number)␊
[Symbol.iterator](): Iterator<number, void, number>
}␊
export class JsClassForEither {␊
constructor()␊
}␊
export class JsRemote {␊
name(): string␊
}␊
export class JsRepo {␊
constructor(dir: string)␊
remote(): JsRemote␊
}␊
export class NinjaTurtle {␊
name: string␊
static isInstanceOf(value: unknown): boolean␊
/** Create your ninja turtle! 🐢 */␊
static newRaph(): NinjaTurtle␊
getMaskColor(): string␊
getName(): string␊
returnThis(this: this): this␊
}␊
export class NotWritableClass {␊
name: string␊
constructor(name: string)␊
setName(name: string): void␊
}␊
export class Optional {␊
static optionEnd(required: string, optional?: string | undefined | null): string␊
static optionStart(optional: string | undefined | null, required: string): string␊
static optionStartEnd(optional1: string | undefined | null, required: string, optional2?: string | undefined | null): string␊
static optionOnly(optional?: string | undefined | null): string␊
}␊
export class Width {␊
value: number␊
constructor(value: number)␊
}␊
export interface A {␊
foo: number␊
}␊
export function acceptThreadsafeFunction(func: (err: Error | null, value: number) => any): void␊
export function acceptThreadsafeFunctionFatal(func: (value: number) => any): void␊
export function acceptThreadsafeFunctionTupleArgs(func: (err: Error | null, arg0: number, arg1: boolean, arg2: string) => any): void␊
export function add(a: number, b: number): number␊
export const enum ALIAS {␊
A = 0,␊
B = 1␊
}␊
export interface AliasedStruct {␊
a: ALIAS␊
b: number␊
}␊
export interface AllOptionalObject {␊
name?: string␊
age?: number␊
}␊
export function appendBuffer(buf: Buffer): Buffer␊
export function arrayBufferPassThrough(buf: Uint8Array): Promise<Uint8Array>
export function asyncMultiTwo(arg: number): Promise<number>
export function asyncPlus100(p: Promise<number>): Promise<number>
export function asyncReduceBuffer(buf: Buffer): Promise<number>
export interface B {␊
bar: number␊
}␊
export function bigintAdd(a: bigint, b: bigint): bigint␊
export function bigintFromI128(): bigint␊
export function bigintFromI64(): bigint␊
export function bigintGetU64AsString(bi: bigint): string␊
export function bufferPassThrough(buf: Buffer): Promise<Buffer>
export interface C {␊
baz: number␊
}␊
export function callbackReturnPromise<T>(functionInput: () => T | Promise<T>, callback: (err: Error | null, result: T) => void): T | Promise<T>
export function callThreadsafeFunction(callback: (...args: any[]) => any): void␊
export function captureErrorInCallback(cb1: () => void, cb2: (arg0: Error) => void): void␊
export function chronoDateAdd1Minute(input: Date): Date␊
export function chronoDateToMillis(input: Date): number␊
export function concatLatin1(s: string): string␊
export function concatStr(s: string): string␊
export function concatUtf16(s: string): string␊
export function contains(source: string, target: string): boolean␊
export function convertU32Array(input: Uint32Array): Array<number>
export function createBigInt(): bigint␊
export function createBigIntI64(): bigint␊
export function createExternal(size: number): ExternalObject<number>
export function createExternalString(content: string): ExternalObject<string>
export function createExternalTypedArray(): Uint32Array␊
export function createObj(): object␊
export function createObjectWithClassField(): ObjectFieldClassInstance␊
export function createObjWithProperty(): { value: ArrayBuffer, get getter(): number }␊
export function createSymbol(): symbol␊
/** You could break the step and for an new continuous value. */␊
export const enum CustomNumEnum {␊
One = 1,␊
Two = 2,␊
Three = 3,␊
Four = 4,␊
Six = 6,␊
Eight = 8,␊
Nine = 9,␊
Ten = 10␊
}␊
export function customStatusCode(): void␊
export interface Dates {␊
start: Date␊
end?: Date␊
}␊
export function dateToNumber(input: Date): number␊
/** This is a const */␊
export const DEFAULT_COST: number␊
export function derefUint8Array(a: Uint8Array, b: Uint8ClampedArray): number␊
export function either3(input: string | number | boolean): number␊
export function either4(input: string | number | boolean | Obj): number␊
export function eitherBoolOrFunction(input: boolean | ((...args: any[]) => any)): void␊
export function eitherFromObjects(input: A | B | C): string␊
export function eitherFromOption(): JsClassForEither | undefined␊
export function eitherStringOrNumber(input: string | number): number␊
export const enum Empty {␊
}␊
export function enumToI32(e: CustomNumEnum): number␊
export function fibonacci(n: number): number␊
export function fnReceivedAliased(s: AliasedStruct, e: ALIAS): void␊
export function getBuffer(): Buffer␊
export function getCwd(callback: (arg0: string) => void): void␊
export function getEmptyBuffer(): Buffer␊
export function getExternal(external: ExternalObject<number>): number␊
export function getGlobal(): typeof global␊
export function getMapping(): Record<string, number>
export function getNestedNumArr(): number[][][]␊
export function getNull(): null␊
export function getNumArr(): number[]␊
/** Gets some numbers */␊
export function getNums(): Array<number>
export function getPackageJsonName(packageJson: PackageJson): string␊
export function getStrFromObject(): void␊
export function getterFromObj(): number␊
export function getUndefined(): void␊
export function getWords(): Array<string>
/** default enum values are continuos i32s start from 0 */␊
export const enum Kind {␊
/** Barks */␊
Dog = 0,␊
/** Kills birds */␊
Cat = 1,␊
/** Tasty */␊
Duck = 2␊
}␊
export function listObjKeys(obj: object): Array<string>
export function mapOption(val?: number | undefined | null): number | null␊
export function mutateExternal(external: ExternalObject<number>, newVal: number): void␊
export function mutateTypedArray(input: Float32Array): void␊
export interface Obj {␊
v: string | number␊
}␊
export interface ObjectFieldClassInstance {␊
bird: Bird␊
}␊
export interface ObjectOnlyFromJs {␊
count: number␊
callback: (err: Error | null, value: number) => any␊
}␊
export function optionEnd(callback: (arg0: string, arg1?: string | undefined | null) => void): void␊
export function optionOnly(callback: (arg0?: string | undefined | null) => void): void␊
export function optionStart(callback: (arg0: string | undefined | null, arg1: string) => void): void␊
export function optionStartEnd(callback: (arg0: string | undefined | null, arg1: string, arg2?: string | undefined | null) => void): void␊
export function overrideIndividualArgOnFunction(notOverridden: string, f: () => string, notOverridden2: number): string␊
export function overrideIndividualArgOnFunctionWithCbArg(callback: (town: string, name?: string | undefined | null) => string, notOverridden: number): object␊
/** This is an interface for package.json */␊
export interface PackageJson {␊
name: string␊
/** The version of the package */␊
version: string␊
dependencies?: Record<string, any>
devDependencies?: Record<string, any>
}␊
export function panic(): void␊
export function plusOne(this: Width): number␊
export function promiseInEither(input: number | Promise<number>): Promise<boolean>
/** napi = { version = 2, features = ["serde-json"] } */␊
export function readFile(callback: (arg0: Error | undefined, arg1?: string | undefined | null) => void): void␊
export function readFileAsync(path: string): Promise<Buffer>
export function readPackageJson(): PackageJson␊
export function receiveAllOptionalObject(obj?: AllOptionalObject | undefined | null): void␊
export function receiveClassOrNumber(either: number | JsClassForEither): number␊
export function receiveDifferentClass(either: JsClassForEither | AnotherClassForEither): number␊
export function receiveMutClassOrNumber(either: number | JsClassForEither): number␊
export function receiveObjectOnlyFromJs(obj: { count: number, callback: (err: Error | null, count: number) => void }): void␊
export function receiveObjectWithClassField(object: ObjectFieldClassInstance): Bird␊
export function receiveStrictObject(strictObject: StrictObject): void␊
export function receiveString(s: string): string␊
export function returnEither(input: number): string | number␊
export function returnEitherClass(input: number): number | JsClassForEither␊
export function returnJsFunction(): (...args: any[]) => any␊
export function returnNull(): null␊
export function returnUndefined(): void␊
export function returnUndefinedIfInvalid(input: boolean): boolean␊
export function returnUndefinedIfInvalidPromise(input: Promise<boolean>): Promise<boolean>
export function roundtripStr(s: string): string␊
export function runScript(script: string): unknown␊
export function setSymbolInObj(symbol: symbol): object␊
export interface StrictObject {␊
name: string␊
}␊
export function sumMapping(nums: Record<string, number>): number␊
export function sumNums(nums: Array<number>): number␊
export function testSerdeRoundtrip(data: any): any␊
export function threadsafeFunctionClosureCapture(func: (...args: any[]) => any): void␊
export function threadsafeFunctionFatalMode(cb: (...args: any[]) => any): void␊
export function threadsafeFunctionFatalModeError(cb: (...args: any[]) => any): void␊
export function threadsafeFunctionThrowError(cb: (...args: any[]) => any): void␊
export function throwError(): void␊
export function toJsObj(): object␊
export function tsfnAsyncCall(func: (...args: any[]) => any): Promise<void>
export function tsfnCallWithCallback(func: (...args: any[]) => any): void␊
export function tsRename(a: { foo: number }): string[]␊
export interface TsTypeChanged {␊
typeOverride: object␊
typeOverrideOptional?: object␊
}␊
export function validateArray(arr: Array<number>): number␊
export function validateBigint(input: bigint): bigint␊
export function validateBoolean(i: boolean): boolean␊
export function validateBuffer(b: Buffer): number␊
export function validateDate(d: Date): number␊
export function validateDateTime(d: Date): number␊
export function validateExternal(e: ExternalObject<number>): number␊
export function validateFunction(cb: () => number): number␊
export function validateHashMap(input: Record<string, number>): number␊
export function validateNull(i: null): boolean␊
export function validateNumber(i: number): number␊
export function validateOptional(input1?: string | undefined | null, input2?: boolean | undefined | null): boolean␊
export function validatePromise(p: Promise<number>): Promise<number>
export function validateString(s: string): string␊
export function validateSymbol(s: symbol): boolean␊
export function validateTypedArray(input: Uint8Array): number␊
export function validateUndefined(i: undefined): boolean␊
export function withAbortController(a: number, b: number, signal: AbortSignal): Promise<number>
export function withoutAbortController(a: number, b: number): Promise<number>
export function xxh64Alias(input: Buffer): bigint␊
export namespace xxh2 {␊
export function xxh2Plus(a: number, b: number): number␊
export function xxh3Xxh64Alias(input: Buffer): bigint␊
}␊
export namespace xxh3 {␊
/** Xxh3 class */␊
export class Xxh3 {␊
constructor()␊
/** update */␊
update(input: Buffer): void␊
digest(): bigint␊
}␊
export const ALIGNMENT: number␊
/** xxh128 function */␊
export function xxh128(input: Buffer): bigint␊
export function xxh3_64(input: Buffer): bigint␊
}␊
`

View file

@ -0,0 +1,20 @@
# Snapshot report for `src/utils/__tests__/version.spec.ts`
The actual snapshot is saved in `version.spec.ts.snap`.
Generated by [AVA](https://avajs.dev).
## should generate correct napi engine requirement
> Snapshot 1
[
'>= 8.6.0',
'>= 8.10.0 && < 9 || >= 9.3.0',
'>= 6.14.2 && < 7 || >= 8.11.2 && < 9 || >= 9.11.0',
'>= 10.16.0 && < 11 || >= 11.8.0',
'>= 10.17.0 && < 11 || >= 12.11.0',
'>= 10.20.0 && < 11 || >= 12.17.0 && < 13 || >= 14.0.0',
'>= 10.23.0 && < 11 || >= 12.19.0 && < 13 || >= 14.12.0',
'>= 12.22.0 && < 13 || >= 14.17.0 && < 15 || >= 15.12.0',
]

View file

@ -0,0 +1,19 @@
import os from 'os'
import test from 'ava'
import {
parseTriple,
getSystemDefaultTarget,
AVAILABLE_TARGETS,
} from '../target.js'
test('should parse triple correctly', (t) => {
t.snapshot(AVAILABLE_TARGETS.map(parseTriple))
})
test('should get system default target correctly', (t) => {
const target = getSystemDefaultTarget()
t.is(target.platform, os.platform())
})

View file

@ -0,0 +1,49 @@
import { join } from 'path'
import { fileURLToPath } from 'url'
import test from 'ava'
import { correctStringIdent, processTypeDef } from '../typegen.js'
test('should ident string correctly', (t) => {
const input = `
/**
* should keep
* class A {
* foo = () => {}
* bar = () => {}
* }
*/
class A {
foo() {
a = b
}
bar = () => {
}
boz = 1
}
namespace B {
namespace C {
type D = A
}
}
`
t.snapshot(correctStringIdent(input, 0), 'original ident is 0')
t.snapshot(correctStringIdent(input, 2), 'original ident is 2')
})
test('should process type def correctly', async (t) => {
const dts = await processTypeDef(
join(
fileURLToPath(import.meta.url),
'../',
'__fixtures__',
'napi_type_def',
),
)
t.snapshot(dts)
})

View file

@ -0,0 +1,13 @@
import test from 'ava'
import { napiEngineRequirement, NapiVersion } from '../version.js'
test('should generate correct napi engine requirement', (t) => {
t.snapshot(
(
Object.values(NapiVersion).filter(
(v) => typeof v === 'number',
) as NapiVersion[]
).map(napiEngineRequirement),
)
})

35
cli/src/utils/cargo.ts Normal file
View file

@ -0,0 +1,35 @@
import { execSync } from 'child_process'
import { debug } from './log.js'
export function tryInstallCargoBinary(name: string, bin: string) {
if (detectCargoBinary(bin)) {
debug('Cargo binary already installed: %s', name)
return
}
try {
debug('Installing cargo binary: %s', name)
execSync(`cargo install ${name}`, {
stdio: 'inherit',
})
} catch (e) {
throw new Error(`Failed to install cargo binary: ${name}`, {
cause: e,
})
}
}
function detectCargoBinary(bin: string) {
debug('Detecting cargo binary: %s', bin)
try {
execSync(`cargo help ${bin}`, {
stdio: 'ignore',
})
debug('Cargo binary detected: %s', bin)
return true
} catch (e) {
debug('Cargo binary not detected: %s', bin)
return false
}
}

Some files were not shown because too many files have changed in this diff Show more