@napi-rs/cli 3.6.1 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@napi-rs/cli",
3
- "version": "3.6.1",
3
+ "version": "3.7.0",
4
4
  "description": "Cli tools for napi-rs",
5
5
  "author": "LongYinan <lynweklm@gmail.com>",
6
6
  "homepage": "https://napi.rs/",
@@ -66,7 +66,7 @@
66
66
  "@octokit/rest": "^22.0.1",
67
67
  "clipanion": "^4.0.0-rc.4",
68
68
  "colorette": "^2.0.20",
69
- "emnapi": "^1.9.1",
69
+ "emnapi": "^1.10.0",
70
70
  "es-toolkit": "^1.41.0",
71
71
  "js-yaml": "^4.1.0",
72
72
  "obug": "^2.0.0",
@@ -81,14 +81,14 @@
81
81
  "@types/js-yaml": "^4.0.9",
82
82
  "@types/node": "^24.10.0",
83
83
  "@types/semver": "^7.7.1",
84
- "ava": "^7.0.0",
84
+ "ava": "^8.0.0",
85
85
  "empathic": "^2.0.0",
86
86
  "env-paths": "^4.0.0",
87
- "oxc-parser": "^0.124.0",
87
+ "oxc-parser": "^0.133.0",
88
88
  "prettier": "^3.6.2",
89
- "tsdown": "^0.21.0",
89
+ "tsdown": "^0.22.0",
90
90
  "tslib": "^2.8.1",
91
- "typescript": "^5.9.3"
91
+ "typescript": "^6.0.3"
92
92
  },
93
93
  "peerDependencies": {
94
94
  "@emnapi/runtime": "^1.7.1"
@@ -105,12 +105,12 @@
105
105
  "scripts": {
106
106
  "codegen": "node --import @oxc-node/core/register ./codegen/index.ts",
107
107
  "build": "tsdown",
108
- "test": "node --import @oxc-node/core/register ../node_modules/ava/entrypoints/cli.mjs"
108
+ "test": "node --import @oxc-node/core/register ../node_modules/ava/entrypoints/cli.js"
109
109
  },
110
110
  "ava": {
111
- "extensions": {
112
- "ts": "module"
113
- },
111
+ "extensions": [
112
+ "ts"
113
+ ],
114
114
  "timeout": "2m",
115
115
  "workerThreads": false,
116
116
  "files": [
package/src/api/build.ts CHANGED
@@ -542,7 +542,7 @@ class Builder {
542
542
  require.resolve('emnapi'),
543
543
  '..',
544
544
  'lib',
545
- 'wasm32-wasi-threads',
545
+ 'wasm32-wasip1-threads',
546
546
  )
547
547
  this.envs.EMNAPI_LINK_DIR = emnapi
548
548
  const emnapiVersion = require('emnapi/package.json').version
@@ -593,15 +593,15 @@ class Builder {
593
593
  )
594
594
  this.setEnvIfNotExists(
595
595
  'TARGET_CFLAGS',
596
- `--target=wasm32-wasi-threads --sysroot=${WASI_SDK_PATH}/share/wasi-sysroot -pthread -mllvm -wasm-enable-sjlj`,
596
+ `--target=wasm32-wasip1-threads --sysroot=${WASI_SDK_PATH}/share/wasi-sysroot -pthread -mllvm -wasm-enable-sjlj`,
597
597
  )
598
598
  this.setEnvIfNotExists(
599
599
  'TARGET_CXXFLAGS',
600
- `--target=wasm32-wasi-threads --sysroot=${WASI_SDK_PATH}/share/wasi-sysroot -pthread -mllvm -wasm-enable-sjlj`,
600
+ `--target=wasm32-wasip1-threads --sysroot=${WASI_SDK_PATH}/share/wasi-sysroot -pthread -mllvm -wasm-enable-sjlj`,
601
601
  )
602
602
  this.setEnvIfNotExists(
603
603
  `TARGET_LDFLAGS`,
604
- `-fuse-ld=${WASI_SDK_PATH}/bin/wasm-ld --target=wasm32-wasi-threads`,
604
+ `-fuse-ld=${WASI_SDK_PATH}/bin/wasm-ld --target=wasm32-wasip1-threads`,
605
605
  )
606
606
  }
607
607
  }
@@ -874,6 +874,8 @@ class Builder {
874
874
  configDtsHeader: this.config.dtsHeader,
875
875
  configDtsHeaderFile: this.config.dtsHeaderFile,
876
876
  constEnum: this.options.constEnum ?? this.config.constEnum,
877
+ runtimeStringEnum:
878
+ this.options.runtimeStringEnum ?? this.config.runtimeStringEnum,
877
879
  cwd: this.options.cwd,
878
880
  })
879
881
 
@@ -1050,9 +1052,16 @@ export interface GenerateTypeDefOptions {
1050
1052
  configDtsHeader?: string
1051
1053
  configDtsHeaderFile?: string
1052
1054
  constEnum?: boolean
1055
+ runtimeStringEnum?: boolean
1053
1056
  cwd: string
1054
1057
  }
1055
1058
 
1059
+ /**
1060
+ * Walk the napi-derive intermediate type-def directory, render every entry
1061
+ * into TypeScript via {@link processTypeDef}, and return the concatenated
1062
+ * `.d.ts` source plus the list of identifiers to re-export from
1063
+ * `index.js`.
1064
+ */
1056
1065
  export async function generateTypeDef(
1057
1066
  options: GenerateTypeDefOptions,
1058
1067
  ): Promise<{ exports: string[]; dts: string }> {
@@ -1093,20 +1102,31 @@ export async function generateTypeDef(
1093
1102
  return { exports: [], dts: '' }
1094
1103
  }
1095
1104
 
1096
- for (const file of files) {
1097
- if (!file.isFile()) {
1098
- continue
1099
- }
1105
+ const typeDefFiles = files
1106
+ .filter((file) => file.isFile())
1107
+ .sort((a, b) => a.name.localeCompare(b.name))
1100
1108
 
1101
- const { dts: fileDts, exports: fileExports } = await processTypeDef(
1102
- join(options.typeDefDir, file.name),
1103
- options.constEnum ?? true,
1109
+ const constEnum = options.constEnum ?? true
1110
+ const runtimeStringEnum = options.runtimeStringEnum ?? false
1111
+ if (runtimeStringEnum && constEnum) {
1112
+ debug.warn(
1113
+ '`--runtime-string-enum` has no effect when `--const-enum` is enabled (the default). Pass `--no-const-enum` to activate runtime string enum emission.',
1104
1114
  )
1105
-
1106
- dts += fileDts
1107
- exports.push(...fileExports)
1108
1115
  }
1109
1116
 
1117
+ const processedTypeDefs = await Promise.all(
1118
+ typeDefFiles.map((file) =>
1119
+ processTypeDef(
1120
+ join(options.typeDefDir, file.name),
1121
+ constEnum,
1122
+ runtimeStringEnum,
1123
+ ),
1124
+ ),
1125
+ )
1126
+
1127
+ dts = processedTypeDefs.map(({ dts }) => dts).join('')
1128
+ exports = processedTypeDefs.flatMap(({ exports }) => exports)
1129
+
1110
1130
  if (dts.indexOf('ExternalObject<') > -1) {
1111
1131
  header += `
1112
1132
  export declare class ExternalObject<T> {
@@ -1,9 +1,10 @@
1
1
  import { createRequire } from 'node:module'
2
2
  import { join, resolve } from 'node:path'
3
3
 
4
- import { parse } from 'semver'
4
+ import { Comparator, Range, minVersion, subset } from 'semver'
5
5
 
6
6
  const require = createRequire(import.meta.url)
7
+ const minimumWasiNodeVersion = '>=14.0.0'
7
8
 
8
9
  import {
9
10
  applyDefaultCreateNpmDirsOptions,
@@ -156,23 +157,11 @@ export async function createNpmDirs(userOptions: CreateNpmDirsOptions) {
156
157
  `wasi-worker.mjs`,
157
158
  `wasi-worker-browser.mjs`,
158
159
  )
159
- let needRestrictNodeVersion = true
160
- if (scopedPackageJson.engines?.node) {
161
- try {
162
- const { major } = parse(scopedPackageJson.engines.node) ?? {
163
- major: 0,
164
- }
165
- if (major >= 14) {
166
- needRestrictNodeVersion = false
167
- }
168
- } catch {
169
- // ignore
170
- }
171
- }
172
- if (needRestrictNodeVersion) {
173
- scopedPackageJson.engines = {
174
- node: '>=14.0.0',
175
- }
160
+ scopedPackageJson.engines = {
161
+ ...scopedPackageJson.engines,
162
+ node: scopedPackageJson.engines?.node
163
+ ? restrictWasiNodeEngine(scopedPackageJson.engines.node)
164
+ : minimumWasiNodeVersion,
176
165
  }
177
166
  const emnapiVersion = require('emnapi/package.json').version
178
167
  scopedPackageJson.dependencies = {
@@ -206,3 +195,69 @@ function readme(packageName: string, target: Target) {
206
195
  This is the **${target.triple}** binary for \`${packageName}\`
207
196
  `
208
197
  }
198
+
199
+ function restrictWasiNodeEngine(nodeRange: string) {
200
+ try {
201
+ if (subset(nodeRange, minimumWasiNodeVersion)) {
202
+ return nodeRange
203
+ }
204
+
205
+ if (subset(minimumWasiNodeVersion, nodeRange)) {
206
+ return minimumWasiNodeVersion
207
+ }
208
+
209
+ const minimumComparator = new Comparator(minimumWasiNodeVersion)
210
+ const restrictedRangeSets = new Range(nodeRange).set
211
+ .map((comparators) =>
212
+ normalizeComparatorSet([...comparators, minimumComparator]),
213
+ )
214
+ .filter(
215
+ (candidate): candidate is string =>
216
+ candidate !== undefined && minVersion(candidate) !== null,
217
+ )
218
+
219
+ if (restrictedRangeSets.length > 0) {
220
+ return restrictedRangeSets.join(' || ')
221
+ }
222
+ } catch {
223
+ // ignore
224
+ }
225
+
226
+ return minimumWasiNodeVersion
227
+ }
228
+
229
+ function normalizeComparatorSet(comparators: Comparator[]) {
230
+ const exactMatch = comparators.find(({ operator }) => operator === '')
231
+ if (exactMatch) {
232
+ return comparators.every((comparator) => comparator.test(exactMatch.semver))
233
+ ? exactMatch.value
234
+ : undefined
235
+ }
236
+
237
+ let lowerBound: Comparator | undefined
238
+ let upperBound: Comparator | undefined
239
+
240
+ for (const comparator of comparators) {
241
+ if (comparator.operator === '>' || comparator.operator === '>=') {
242
+ if (
243
+ !lowerBound ||
244
+ comparator.semver.compare(lowerBound.semver) > 0 ||
245
+ (comparator.semver.compare(lowerBound.semver) === 0 &&
246
+ comparator.operator === '>')
247
+ ) {
248
+ lowerBound = comparator
249
+ }
250
+ } else if (comparator.operator === '<' || comparator.operator === '<=') {
251
+ if (
252
+ !upperBound ||
253
+ comparator.semver.compare(upperBound.semver) < 0 ||
254
+ (comparator.semver.compare(upperBound.semver) === 0 &&
255
+ comparator.operator === '<')
256
+ ) {
257
+ upperBound = comparator
258
+ }
259
+ }
260
+ }
261
+
262
+ return [lowerBound?.value, upperBound?.value].filter(Boolean).join(' ')
263
+ }
package/src/api/new.ts CHANGED
@@ -4,6 +4,7 @@ import { homedir } from 'node:os'
4
4
  import path from 'node:path'
5
5
  import { promises as fs } from 'node:fs'
6
6
 
7
+ import { parse as parseToml, stringify as stringifyToml } from '@std/toml'
7
8
  import { load as yamlLoad, dump as yamlDump } from 'js-yaml'
8
9
 
9
10
  import {
@@ -15,6 +16,7 @@ import {
15
16
  debugFactory,
16
17
  DEFAULT_TARGETS,
17
18
  mkdirAsync,
19
+ parseTriple,
18
20
  readdirAsync,
19
21
  statAsync,
20
22
  type SupportedPackageManager,
@@ -33,6 +35,8 @@ const TEMPLATE_REPOS = {
33
35
  pnpm: 'https://github.com/napi-rs/package-template-pnpm',
34
36
  } as const
35
37
 
38
+ const WASI_TARGET = 'wasm32-wasip1-threads'
39
+
36
40
  async function checkGitCommand(): Promise<boolean> {
37
41
  return new Promise<boolean>((resolve) => {
38
42
  const cp = exec('git --version')
@@ -125,7 +129,7 @@ async function copyDirectory(
125
129
  entry.name.endsWith('.wasi.cjs') ||
126
130
  entry.name.endsWith('wasi-worker-browser.mjs') ||
127
131
  entry.name.endsWith('wasi-worker.mjs') ||
128
- entry.name.endsWith('browser.js'))
132
+ entry.name === 'browser.js')
129
133
  ) {
130
134
  continue
131
135
  }
@@ -140,6 +144,7 @@ async function filterTargetsInPackageJson(
140
144
  ): Promise<void> {
141
145
  const content = await fs.readFile(filePath, 'utf-8')
142
146
  const packageJson = JSON.parse(content)
147
+ const includeWasiBindings = enabledTargets.includes(WASI_TARGET)
143
148
 
144
149
  // Filter napi.targets
145
150
  if (packageJson.napi?.targets) {
@@ -148,9 +153,63 @@ async function filterTargetsInPackageJson(
148
153
  )
149
154
  }
150
155
 
156
+ if (!includeWasiBindings) {
157
+ if (
158
+ packageJson.browser === 'browser.js' ||
159
+ packageJson.browser === './browser.js'
160
+ ) {
161
+ delete packageJson.browser
162
+ }
163
+
164
+ if (Array.isArray(packageJson.files)) {
165
+ packageJson.files = packageJson.files.filter(
166
+ (file: unknown) => file !== 'browser.js' && file !== './browser.js',
167
+ )
168
+ }
169
+ }
170
+
151
171
  await fs.writeFile(filePath, JSON.stringify(packageJson, null, 2) + '\n')
152
172
  }
153
173
 
174
+ async function updateCargoTomlTypeDef(
175
+ filePath: string,
176
+ enableTypeDef: boolean,
177
+ ): Promise<void> {
178
+ if (enableTypeDef) {
179
+ return
180
+ }
181
+
182
+ const content = await fs.readFile(filePath, 'utf-8')
183
+ const cargoToml = parseToml(content) as Record<string, any>
184
+ const dependencies = cargoToml.dependencies
185
+
186
+ if (!dependencies || !dependencies['napi-derive']) {
187
+ return
188
+ }
189
+
190
+ const napiDeriveDependency = dependencies['napi-derive']
191
+ const dependencyConfig =
192
+ typeof napiDeriveDependency === 'string'
193
+ ? { version: napiDeriveDependency }
194
+ : { ...napiDeriveDependency }
195
+
196
+ const existingFeatures: string[] = Array.isArray(dependencyConfig.features)
197
+ ? dependencyConfig.features.filter(
198
+ (feature: unknown): feature is string => typeof feature === 'string',
199
+ )
200
+ : []
201
+
202
+ dependencyConfig['default-features'] = false
203
+ dependencyConfig.features = [
204
+ 'strict',
205
+ ...existingFeatures.filter((feature) => feature !== 'strict'),
206
+ ].filter((feature) => feature !== 'type-def')
207
+
208
+ dependencies['napi-derive'] = dependencyConfig
209
+
210
+ await fs.writeFile(filePath, stringifyToml(cargoToml))
211
+ }
212
+
154
213
  async function filterTargetsInGithubActions(
155
214
  filePath: string,
156
215
  enabledTargets: string[],
@@ -158,13 +217,6 @@ async function filterTargetsInGithubActions(
158
217
  const content = await fs.readFile(filePath, 'utf-8')
159
218
  const yaml = yamlLoad(content) as any
160
219
 
161
- const macOSAndWindowsTargets = new Set([
162
- 'x86_64-pc-windows-msvc',
163
- 'x86_64-pc-windows-gnu',
164
- 'aarch64-pc-windows-msvc',
165
- 'x86_64-apple-darwin',
166
- ])
167
-
168
220
  const linuxTargets = new Set([
169
221
  'x86_64-unknown-linux-gnu',
170
222
  'x86_64-unknown-linux-musl',
@@ -184,6 +236,10 @@ async function filterTargetsInGithubActions(
184
236
  const hasLinuxTargets = enabledTargets.some((target) =>
185
237
  linuxTargets.has(target),
186
238
  )
239
+ const hasMacOSOrWindowsTargets = enabledTargets.some((target) => {
240
+ const platform = parseTriple(target).platform
241
+ return platform === 'darwin' || platform === 'win32'
242
+ })
187
243
 
188
244
  // Filter the matrix configurations in the build job
189
245
  if (yaml?.jobs?.build?.strategy?.matrix?.settings) {
@@ -198,7 +254,7 @@ async function filterTargetsInGithubActions(
198
254
 
199
255
  const jobsToRemove: string[] = []
200
256
 
201
- if (enabledTargets.every((target) => !macOSAndWindowsTargets.has(target))) {
257
+ if (!hasMacOSOrWindowsTargets) {
202
258
  jobsToRemove.push('test-macOS-windows-binding')
203
259
  } else {
204
260
  // Filter the matrix configurations in the test-macOS-windows-binding job
@@ -237,7 +293,7 @@ async function filterTargetsInGithubActions(
237
293
  }
238
294
  }
239
295
 
240
- if (!enabledTargets.includes('wasm32-wasip1-threads')) {
296
+ if (!enabledTargets.includes(WASI_TARGET)) {
241
297
  jobsToRemove.push('test-wasi')
242
298
  }
243
299
 
@@ -313,11 +369,9 @@ function processOptions(options: RawNewOptions) {
313
369
  const out = execSync(`rustup target list`, {
314
370
  encoding: 'utf8',
315
371
  })
316
- if (out.includes('wasm32-wasip1-threads')) {
372
+ if (out.includes(WASI_TARGET)) {
317
373
  options.targets = options.targets.map((target) =>
318
- target === 'wasm32-wasi-preview1-threads'
319
- ? 'wasm32-wasip1-threads'
320
- : target,
374
+ target === 'wasm32-wasi-preview1-threads' ? WASI_TARGET : target,
321
375
  )
322
376
  }
323
377
  }
@@ -357,7 +411,7 @@ export async function newProject(userOptions: RawNewOptions) {
357
411
  await copyDirectory(
358
412
  templatePath,
359
413
  options.path,
360
- options.targets.includes('wasm32-wasip1-threads'),
414
+ options.targets.includes(WASI_TARGET),
361
415
  )
362
416
 
363
417
  // Rename project using the rename API
@@ -367,6 +421,11 @@ export async function newProject(userOptions: RawNewOptions) {
367
421
  binaryName: getBinaryName(options.name),
368
422
  })
369
423
 
424
+ const cargoTomlPath = path.join(options.path, 'Cargo.toml')
425
+ if (existsSync(cargoTomlPath)) {
426
+ await updateCargoTomlTypeDef(cargoTomlPath, options.enableTypeDef)
427
+ }
428
+
370
429
  // Filter targets in package.json
371
430
  const packageJsonPath = path.join(options.path, 'package.json')
372
431
  if (existsSync(packageJsonPath)) {
@@ -85,6 +85,7 @@ export async function prePublish(userOptions: PrePublishOptions) {
85
85
 
86
86
  function getRepoInfo(packageName: string, version: string) {
87
87
  const headCommit = execSync('git log -1 --pretty=%B', {
88
+ cwd: options.cwd,
88
89
  encoding: 'utf-8',
89
90
  }).trim()
90
91
 
package/src/api/rename.ts CHANGED
@@ -41,6 +41,18 @@ export async function renameProject(userOptions: RenameOptions) {
41
41
  },
42
42
  )
43
43
 
44
+ if (options.repository) {
45
+ if (
46
+ packageJsonData.repository &&
47
+ typeof packageJsonData.repository === 'object' &&
48
+ !Array.isArray(packageJsonData.repository)
49
+ ) {
50
+ packageJsonData.repository.url = options.repository
51
+ } else {
52
+ packageJsonData.repository = options.repository
53
+ }
54
+ }
55
+
44
56
  if (options.configPath) {
45
57
  const configPath = resolve(options.cwd, options.configPath)
46
58
  const configContent = await readFileAsync(configPath, 'utf8')
@@ -227,23 +227,33 @@ function requireNative() {
227
227
 
228
228
  nativeBinding = requireNative()
229
229
 
230
- if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) {
230
+ // NAPI_RS_FORCE_WASI is a tri-state flag:
231
+ // unset / any other value → native binding preferred, WASI is only a fallback
232
+ // 'true' → force WASI fallback even if native loaded
233
+ // 'error' → force WASI and throw if no WASI binding is found
234
+ // Treating any non-empty string as truthy (the historical behavior) meant
235
+ // NAPI_RS_FORCE_WASI=false, NAPI_RS_FORCE_WASI=0, etc. inadvertently triggered
236
+ // the WASI path, causing ENOENT for packages shipped without a .wasi.cjs file.
237
+ const forceWasi =
238
+ process.env.NAPI_RS_FORCE_WASI === 'true' || process.env.NAPI_RS_FORCE_WASI === 'error'
239
+
240
+ if (!nativeBinding || forceWasi) {
231
241
  let wasiBinding = null
232
242
  let wasiBindingError = null
233
243
  try {
234
244
  wasiBinding = require('./${localName}.wasi.cjs')
235
245
  nativeBinding = wasiBinding
236
246
  } catch (err) {
237
- if (process.env.NAPI_RS_FORCE_WASI) {
247
+ if (forceWasi) {
238
248
  wasiBindingError = err
239
249
  }
240
250
  }
241
- if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) {
251
+ if (!nativeBinding || forceWasi) {
242
252
  try {
243
253
  wasiBinding = require('${pkgName}-wasm32-wasi')
244
254
  nativeBinding = wasiBinding
245
255
  } catch (err) {
246
- if (process.env.NAPI_RS_FORCE_WASI) {
256
+ if (forceWasi) {
247
257
  if (!wasiBindingError) {
248
258
  wasiBindingError = err
249
259
  } else {
@@ -34,7 +34,7 @@ const __wasi = new __WASI({
34
34
  : ''
35
35
 
36
36
  const workerErrorHandler = errorEvent
37
- ? ` worker.addEventListener('error', (event) => {
37
+ ? ` worker.addEventListener('message', (event) => {
38
38
  if (event.data && typeof event.data === 'object' && event.data.type === 'error') {
39
39
  window.dispatchEvent(new CustomEvent('napi-rs-worker-error', { detail: event.data }))
40
40
  }
@@ -109,10 +109,9 @@ const fs = createFsProxy(__memfsExported)`
109
109
  errorOutputs.length = 0
110
110
  }`
111
111
  : ''
112
+ const errorOutputsDecl = errorEvent ? `\nconst errorOutputs = []\n` : ''
112
113
  return `${fsImport}
113
-
114
- const errorOutputs = []
115
-
114
+ ${errorOutputsDecl}
116
115
  const handler = new MessageHandler({
117
116
  onLoad({ wasmModule, wasmMemory }) {
118
117
  ${wasiCreation}
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from 'node:child_process'
2
- import { join, resolve } from 'node:path'
2
+ import { resolve } from 'node:path'
3
3
 
4
4
  import {
5
5
  applyDefaultUniversalizeOptions,
@@ -12,20 +12,38 @@ import { UniArchsByPlatform } from '../utils/target.js'
12
12
 
13
13
  const debug = debugFactory('universalize')
14
14
 
15
+ function createDarwinUniversalBinary(inputs: string[], output: string) {
16
+ const result = spawnSync('lipo', ['-create', '-output', output, ...inputs], {
17
+ stdio: 'inherit',
18
+ })
19
+
20
+ if (result.error) {
21
+ throw result.error
22
+ }
23
+
24
+ if (result.signal) {
25
+ throw new Error(
26
+ `Failed to create universal binary '${output}': lipo terminated with signal ${result.signal}.`,
27
+ )
28
+ }
29
+
30
+ if (result.status !== 0) {
31
+ throw new Error(
32
+ `Failed to create universal binary '${output}': lipo exited with status ${result.status}.`,
33
+ )
34
+ }
35
+ }
36
+
15
37
  const universalizers: Partial<
16
38
  Record<NodeJS.Platform, (inputs: string[], output: string) => void>
17
39
  > = {
18
- darwin: (inputs, output) => {
19
- spawnSync('lipo', ['-create', '-output', output, ...inputs], {
20
- stdio: 'inherit',
21
- })
22
- },
40
+ darwin: createDarwinUniversalBinary,
23
41
  }
24
42
 
25
43
  export async function universalizeBinaries(userOptions: UniversalizeOptions) {
26
44
  const options = applyDefaultUniversalizeOptions(userOptions)
27
45
 
28
- const packageJsonPath = join(options.cwd, options.packageJsonPath)
46
+ const packageJsonPath = resolve(options.cwd, options.packageJsonPath)
29
47
 
30
48
  const config = await readNapiConfig(
31
49
  packageJsonPath,
@@ -77,5 +95,9 @@ export async function universalizeBinaries(userOptions: UniversalizeOptions) {
77
95
 
78
96
  universalizers[process.platform]?.(srcFiles, output)
79
97
 
98
+ if (!(await fileExists(output))) {
99
+ throw new Error(`Universal binary was not created: ${output}`)
100
+ }
101
+
80
102
  debug(`Produced universal binary: ${output}`)
81
103
  }
@@ -35,6 +35,7 @@ export class BuildCommand extends BaseBuildCommand {
35
35
  } catch (e) {
36
36
  debug.error(`Failed to pipe output file ${output.path} to command`)
37
37
  debug.error(e)
38
+ throw e
38
39
  }
39
40
  }
40
41
  }
@@ -11,10 +11,20 @@ import {
11
11
  DEFAULT_TARGETS,
12
12
  type TargetTriple,
13
13
  } from '../utils/index.js'
14
- import { napiEngineRequirement } from '../utils/version.js'
14
+ import {
15
+ napiEngineRequirement,
16
+ SUPPORTED_NAPI_VERSIONS,
17
+ } from '../utils/version.js'
15
18
 
16
19
  const debug = debugFactory('new')
17
20
 
21
+ export function getNapiVersionChoices() {
22
+ return SUPPORTED_NAPI_VERSIONS.map((version) => ({
23
+ name: `napi${version} (${napiEngineRequirement(version)})`,
24
+ value: version,
25
+ }))
26
+ }
27
+
18
28
  export class NewCommand extends BaseNewCommand {
19
29
  interactive = Option.Boolean('--interactive,-i', true, {
20
30
  description:
@@ -77,10 +87,7 @@ export class NewCommand extends BaseNewCommand {
77
87
  message: 'Minimum node-api version (with node version requirement)',
78
88
  loop: false,
79
89
  pageSize: 10,
80
- choices: Array.from({ length: 8 }, (_, i) => ({
81
- name: `napi${i + 1} (${napiEngineRequirement(i + 1)})`,
82
- value: i + 1,
83
- })),
90
+ choices: getNapiVersionChoices(),
84
91
  // choice index
85
92
  default: this.minNodeApiVersion - 1,
86
93
  })
package/src/def/build.ts CHANGED
@@ -55,6 +55,11 @@ export abstract class BaseBuildCommand extends Command {
55
55
  description: 'Whether generate const enum for typescript bindings',
56
56
  })
57
57
 
58
+ runtimeStringEnum?: boolean = Option.Boolean('--runtime-string-enum', {
59
+ description:
60
+ 'Emit `#[napi(string_enum)]` enums as runtime enums (`export declare enum`) under `--no-const-enum`. Default: type-only union.',
61
+ })
62
+
58
63
  jsBinding?: string = Option.String('--js', {
59
64
  description:
60
65
  'Path and filename of generated JS binding file. Only works with `--platform` flag. Relative to `--output-dir`.',
@@ -157,6 +162,7 @@ export abstract class BaseBuildCommand extends Command {
157
162
  platform: this.platform,
158
163
  jsPackageName: this.jsPackageName,
159
164
  constEnum: this.constEnum,
165
+ runtimeStringEnum: this.runtimeStringEnum,
160
166
  jsBinding: this.jsBinding,
161
167
  noJsBinding: this.noJsBinding,
162
168
  dts: this.dts,
@@ -225,6 +231,10 @@ export interface BuildOptions {
225
231
  * Whether generate const enum for typescript bindings
226
232
  */
227
233
  constEnum?: boolean
234
+ /**
235
+ * Emit `#[napi(string_enum)]` enums as runtime enums (`export declare enum`) under `--no-const-enum`. Default: type-only union.
236
+ */
237
+ runtimeStringEnum?: boolean
228
238
  /**
229
239
  * Path and filename of generated JS binding file. Only works with `--platform` flag. Relative to `--output-dir`.
230
240
  */