From 179f20a7c5d2b71bc0a0825816092390291ce23d Mon Sep 17 00:00:00 2001 From: LongYinan Date: Tue, 9 Nov 2021 20:10:08 +0800 Subject: [PATCH] feat(cli): generate js binding to avoid dynamic require logic --- cli/src/build.ts | 99 ++++++++++-- cli/src/js-binding-template.ts | 216 +++++++++++++++++++++++++ cli/src/new/ci-template.ts | 2 +- cli/src/new/index.ts | 2 - cli/src/new/indexjs.ts | 15 -- examples/napi-compat-mode/package.json | 14 +- examples/napi/index.d.ts | 33 ++-- examples/napi/package.json | 12 +- examples/napi/tsconfig.json | 2 +- 9 files changed, 325 insertions(+), 70 deletions(-) create mode 100644 cli/src/js-binding-template.ts delete mode 100644 cli/src/new/indexjs.ts diff --git a/cli/src/build.ts b/cli/src/build.ts index 00d3388b..8a79f239 100644 --- a/cli/src/build.ts +++ b/cli/src/build.ts @@ -1,12 +1,13 @@ import { execSync } from 'child_process' import { join, parse, sep } from 'path' -import chalk from 'chalk' +import { Instance } from 'chalk' import { Command, Option } from 'clipanion' import toml from 'toml' import { getNapiConfig } from './consts' import { debugFactory } from './debug' +import { createJsBinding } from './js-binding-template' import { getDefaultTargetTriple, parseTriple } from './parse-triple' import { copyFileAsync, @@ -18,6 +19,7 @@ import { } from './utils' const debug = debugFactory('build') +const chalk = new Instance({ level: 1 }) export class BuildCommand extends Command { static usage = Command.Usage({ @@ -26,23 +28,57 @@ export class BuildCommand extends Command { static paths = [['build']] - appendPlatformToFilename = Option.Boolean(`--platform`, false) + 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) + isRelease = Option.Boolean(`--release`, false, { + description: `Bypass to ${chalk.green('cargo --release')}`, + }) - configFileName?: string = Option.String('--config,-c') + 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') + 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 ?? '') + targetTripleDir = Option.String('--target', process.env.RUST_TARGET ?? '', { + description: `Bypass to ${chalk.green('cargo --target')}`, + }) - features?: string = Option.String('--features') + features?: string = Option.String('--features', { + description: `Bypass to ${chalk.green('cargo --features')}`, + }) - dts?: string = Option.String('--dts') + dts?: string = Option.String('--dts', 'index.d.ts', { + description: `The filename and path of ${chalk.green( + '.d.ts', + )} file, relative to cwd`, + }) - cargoFlags = Option.String('--cargo-flags', '') + cargoFlags = Option.String('--cargo-flags', '', { + description: `All the others flag passed to ${chalk.yellow('cargo')}`, + }) - cargoCwd?: string = Option.String('--cargo-cwd') + jsBinding = Option.String('--js', 'index.js', { + description: `Path to the JS binding file, pass ${chalk.underline( + chalk.yellow('false'), + )} to disable it`, + }) + + cargoCwd?: string = Option.String('--cargo-cwd', { + description: `The cwd of ${chalk.underline( + chalk.yellow('Cargo.toml'), + )} file`, + }) destDir = Option.String({ required: false, @@ -84,7 +120,7 @@ export class BuildCommand extends Command { stdio: 'inherit', cwd, }) - const { binaryName } = getNapiConfig(this.configFileName) + const { binaryName, packageName } = getNapiConfig(this.configFileName) let dylibName = this.cargoName if (!dylibName) { let tomlContentString: string @@ -198,10 +234,18 @@ export class BuildCommand extends Command { debug(`Write binary content to [${chalk.yellowBright(distModulePath)}]`) await copyFileAsync(sourcePath, distModulePath) - await processIntermediateTypeFile( + const idents = await processIntermediateTypeFile( intermediateTypeFile, join(this.destDir ?? '.', this.dts ?? 'index.d.ts'), ) + await writeJsBinding( + binaryName, + packageName, + this.jsBinding && this.jsBinding !== 'false' + ? join(process.cwd(), this.jsBinding) + : null, + idents, + ) } } @@ -224,10 +268,14 @@ interface TypeDef { def: string } -async function processIntermediateTypeFile(source: string, target: string) { +async function processIntermediateTypeFile( + source: string, + target: string, +): Promise { + const idents: string[] = [] if (!(await existsAsync(source))) { debug(`do not find tmp type file. skip type generation`) - return + return idents } const tmpFile = await readFileAsync(source, 'utf8') @@ -244,6 +292,7 @@ async function processIntermediateTypeFile(source: string, target: string) { switch (def.kind) { case 'struct': + idents.push(def.name) classes.set(def.name, def.def) break case 'impl': @@ -253,6 +302,7 @@ async function processIntermediateTypeFile(source: string, target: string) { dts += `interface ${def.name} {\n${indentLines(def.def, 2)}\n}\n` break default: + idents.push(def.name) dts += def.def + '\n' } }) @@ -271,6 +321,7 @@ async function processIntermediateTypeFile(source: string, target: string) { await unlinkAsync(source) await writeFileAsync(target, dts, 'utf8') + return idents } function indentLines(input: string, spaces: number) { @@ -279,3 +330,23 @@ function indentLines(input: string, spaces: number) { .map((line) => ''.padEnd(spaces, ' ') + line.trim()) .join('\n') } + +async function writeJsBinding( + localName: string, + packageName: string, + distFileName: string | null, + idents: string[], +) { + if (distFileName) { + const template = createJsBinding(localName, packageName) + const declareCodes = `const { ${idents.join(', ')} } = nativeBinding\n` + const exportsCode = idents.reduce((acc, cur) => { + return `${acc}\nmodule.exports.${cur} = ${cur}` + }, '') + await writeFileAsync( + distFileName, + template + declareCodes + exportsCode, + 'utf8', + ) + } +} diff --git a/cli/src/js-binding-template.ts b/cli/src/js-binding-template.ts new file mode 100644 index 00000000..5d64aeaf --- /dev/null +++ b/cli/src/js-binding-template.ts @@ -0,0 +1,216 @@ +export const createJsBinding = ( + localName: string, + pkgName: string, +) => `const { existsSync, readFileSync } = require('fs') +const { join } = require('path') + +const { platform, arch } = process + +let nativeBinding = null +let localFileExisted = false +let isMusl = false +let loadError = null + +switch (platform) { + case 'android': + if (arch !== 'arm64') { + throw new Error(\`Unsupported architecture on Android \${arch}\`) + } + 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 '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': + 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': + isMusl = readFileSync('/usr/bin/ldd', 'utf8').includes('musl') + 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': + isMusl = readFileSync('/usr/bin/ldd', 'utf8').includes('musl') + 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\`) +} + +` diff --git a/cli/src/new/ci-template.ts b/cli/src/new/ci-template.ts index 4bf3d32c..6b9c1406 100644 --- a/cli/src/new/ci-template.ts +++ b/cli/src/new/ci-template.ts @@ -38,7 +38,7 @@ jobs: target: 'x86_64-pc-windows-msvc' - host: windows-latest build: | - export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=32; + export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=64; export CARGO_PROFILE_RELEASE_LTO=false yarn build --target i686-pc-windows-msvc yarn test diff --git a/cli/src/new/index.ts b/cli/src/new/index.ts index 9bffc2c2..ba333d99 100644 --- a/cli/src/new/index.ts +++ b/cli/src/new/index.ts @@ -12,7 +12,6 @@ import { DefaultPlatforms } from '../parse-triple' import { createCargoContent } from './cargo' import { createCargoConfig } from './cargo-config' import { createGithubActionsCIYml } from './ci-yml' -import { createIndexJs } from './indexjs' import { LibRs } from './lib-rs' import { NPMIgnoreFiles } from './npmignore' import { createPackageJson } from './package' @@ -125,7 +124,6 @@ export class NewProjectCommand extends Command { this.writeFile('Cargo.toml', createCargoContent(this.name!)) this.writeFile('.npmignore', NPMIgnoreFiles) this.writeFile('build.rs', BUILD_RS) - this.writeFile('index.js', createIndexJs(this.name!, binaryName)) this.writeFile( 'package.json', JSON.stringify( diff --git a/cli/src/new/indexjs.ts b/cli/src/new/indexjs.ts deleted file mode 100644 index c5b2d8ff..00000000 --- a/cli/src/new/indexjs.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const createIndexJs = ( - pkgName: string, - name: string, -) => `const { loadBinding } = require('@node-rs/helper') - -/** - * __dirname means load native addon from current dir - * '${name}' is the name of native addon - * the second arguments was decided by \`napi.name\` field in \`package.json\` - * the third arguments was decided by \`name\` field in \`package.json\` - * \`loadBinding\` helper will load \`${name}.[PLATFORM].node\` from \`__dirname\` first - * If failed to load addon, it will fallback to load from \`${pkgName}-[PLATFORM]\` - */ -module.exports = loadBinding(__dirname, '${name}', '${pkgName}') -` diff --git a/examples/napi-compat-mode/package.json b/examples/napi-compat-mode/package.json index 734142b6..3571e929 100644 --- a/examples/napi-compat-mode/package.json +++ b/examples/napi-compat-mode/package.json @@ -2,13 +2,13 @@ "name": "test-module", "version": "1.0.0", "scripts": { - "build": "node ../../cli/scripts/index.js build --features \"latest\"", - "build-napi3": "node ../../cli/scripts/index.js build --features \"napi3\"", - "build-aarch64": "node ../../cli/scripts/index.js build --features \"latest\" --target aarch64-unknown-linux-gnu", - "build-armv7": "node ../../cli/scripts/index.js build --features \"latest\" --target armv7-unknown-linux-gnueabihf", - "build-i686": "node ../../cli/scripts/index.js build --features \"latest\" --target i686-pc-windows-msvc", - "build-i686-release": "node ../../cli/scripts/index.js build --release --features \"latest\" --target i686-pc-windows-msvc", - "build-release": "node ../../cli/scripts/index.js build --features \"latest\" --release", + "build": "node ../../cli/scripts/index.js build --js false --features \"latest\"", + "build-napi3": "node ../../cli/scripts/index.js build --js false --features \"napi3\"", + "build-aarch64": "node ../../cli/scripts/index.js build --js false --features \"latest\" --target aarch64-unknown-linux-gnu", + "build-armv7": "node ../../cli/scripts/index.js build --js false --features \"latest\" --target armv7-unknown-linux-gnueabihf", + "build-i686": "node ../../cli/scripts/index.js build --js false --features \"latest\" --target i686-pc-windows-msvc", + "build-i686-release": "node ../../cli/scripts/index.js build --js false --release --features \"latest\" --target i686-pc-windows-msvc", + "build-release": "node ../../cli/scripts/index.js build --js false --features \"latest\" --release", "test": "node ./index.js" } } diff --git a/examples/napi/index.d.ts b/examples/napi/index.d.ts index 5352b880..ab2e701d 100644 --- a/examples/napi/index.d.ts +++ b/examples/napi/index.d.ts @@ -4,9 +4,7 @@ export function sumNums(nums: Array): number export function readFileAsync(path: string): Promise export function asyncMultiTwo(arg: number): Promise export function getCwd(callback: (arg0: string) => void): void -export function readFile( - callback: (arg0: Error | undefined, arg1: string | null) => void, -): void +export function readFile(callback: (arg0: Error | undefined, arg1: string | null) => void): void export function eitherStringOrNumber(input: string | number): number export function returnEither(input: number): string | number export function either3(input: string | number | boolean): number @@ -14,21 +12,8 @@ interface Obj { v: string | number } export function either4(input: string | number | boolean | Obj): number -export enum Kind { - Dog = 0, - Cat = 1, - Duck = 2, -} -export enum CustomNumEnum { - One = 1, - Two = 2, - Three = 3, - Four = 4, - Six = 6, - Eight = 8, - Nine = 9, - Ten = 10, -} +export enum Kind { Dog = 0, Cat = 1, Duck = 2 } +export enum CustomNumEnum { One = 1, Two = 2, Three = 3, Four = 4, Six = 6, Eight = 8, Nine = 9, Ten = 10 } export function enumToI32(e: CustomNumEnum): number export function throwError(): void export function mapOption(val: number | null): number | null @@ -49,11 +34,7 @@ export function concatStr(mutS: string): string export function concatUtf16(s: string): string export function concatLatin1(s: string): string export function withoutAbortController(a: number, b: number): Promise -export function withAbortController( - a: number, - b: number, - signal: AbortSignal, -): Promise +export function withAbortController(a: number, b: number, signal: AbortSignal): Promise export function getBuffer(): Buffer export class Animal { readonly kind: Kind @@ -65,10 +46,14 @@ export class Animal { static getDogKind(): Kind } export class Blake2BHasher { + static withKey(key: Blake2bKey): Blake2BHasher } -export class Blake2BKey {} +export class Blake2BKey { + +} export class Context { + constructor() static withData(data: string): Context method(): string diff --git a/examples/napi/package.json b/examples/napi/package.json index 2c0dc747..a08fcce1 100644 --- a/examples/napi/package.json +++ b/examples/napi/package.json @@ -4,11 +4,11 @@ "main": "./index.node", "types": "./index.d.ts", "scripts": { - "build": "node ../../cli/scripts/index.js build", - "build-aarch64": "node ../../cli/scripts/index.js build --target aarch64-unknown-linux-gnu", - "build-armv7": "node ../../cli/scripts/index.js build --target armv7-unknown-linux-gnueabihf", - "build-i686": "node ../../cli/scripts/index.js build --target i686-pc-windows-msvc", - "build-i686-release": "node ../../cli/scripts/index.js build --release --target i686-pc-windows-msvc", - "build-release": "node ../../cli/scripts/index.js build --release" + "build": "node ../../cli/scripts/index.js build --js false", + "build-aarch64": "node ../../cli/scripts/index.js build --js false --target aarch64-unknown-linux-gnu", + "build-armv7": "node ../../cli/scripts/index.js build --js false --target armv7-unknown-linux-gnueabihf", + "build-i686": "node ../../cli/scripts/index.js build --js false --target i686-pc-windows-msvc", + "build-i686-release": "node ../../cli/scripts/index.js build --js false --release --target i686-pc-windows-msvc", + "build-release": "node ../../cli/scripts/index.js build --js false --release" } } diff --git a/examples/napi/tsconfig.json b/examples/napi/tsconfig.json index f9813ffb..dead17fc 100644 --- a/examples/napi/tsconfig.json +++ b/examples/napi/tsconfig.json @@ -6,5 +6,5 @@ "rootDir": "__test__", "target": "ES2015" }, - "exclude": ["dist"] + "exclude": ["dist", "index.d.ts"] }