@napi-rs/cli 3.6.1 → 3.6.2

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.6.2",
4
4
  "description": "Cli tools for napi-rs",
5
5
  "author": "LongYinan <lynweklm@gmail.com>",
6
6
  "homepage": "https://napi.rs/",
@@ -84,7 +84,7 @@
84
84
  "ava": "^7.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.125.0",
88
88
  "prettier": "^3.6.2",
89
89
  "tsdown": "^0.21.0",
90
90
  "tslib": "^2.8.1",
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
  }
@@ -1093,19 +1093,21 @@ export async function generateTypeDef(
1093
1093
  return { exports: [], dts: '' }
1094
1094
  }
1095
1095
 
1096
- for (const file of files) {
1097
- if (!file.isFile()) {
1098
- continue
1099
- }
1100
-
1101
- const { dts: fileDts, exports: fileExports } = await processTypeDef(
1102
- join(options.typeDefDir, file.name),
1103
- options.constEnum ?? true,
1104
- )
1096
+ const typeDefFiles = files
1097
+ .filter((file) => file.isFile())
1098
+ .sort((a, b) => a.name.localeCompare(b.name))
1099
+
1100
+ const processedTypeDefs = await Promise.all(
1101
+ typeDefFiles.map((file) =>
1102
+ processTypeDef(
1103
+ join(options.typeDefDir, file.name),
1104
+ options.constEnum ?? true,
1105
+ ),
1106
+ ),
1107
+ )
1105
1108
 
1106
- dts += fileDts
1107
- exports.push(...fileExports)
1108
- }
1109
+ dts = processedTypeDefs.map(({ dts }) => dts).join('')
1110
+ exports = processedTypeDefs.flatMap(({ exports }) => exports)
1109
1111
 
1110
1112
  if (dts.indexOf('ExternalObject<') > -1) {
1111
1113
  header += `
@@ -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 = 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')
@@ -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,
@@ -25,7 +25,7 @@ const universalizers: Partial<
25
25
  export async function universalizeBinaries(userOptions: UniversalizeOptions) {
26
26
  const options = applyDefaultUniversalizeOptions(userOptions)
27
27
 
28
- const packageJsonPath = join(options.cwd, options.packageJsonPath)
28
+ const packageJsonPath = resolve(options.cwd, options.packageJsonPath)
29
29
 
30
30
  const config = await readNapiConfig(
31
31
  packageJsonPath,
@@ -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/utils/misc.ts CHANGED
@@ -43,6 +43,27 @@ export function pick<O, K extends keyof O>(o: O, ...keys: K[]): Pick<O, K> {
43
43
  }, {} as O)
44
44
  }
45
45
 
46
+ function isPlainObject(value: unknown): value is Record<string, any> {
47
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
48
+ }
49
+
50
+ function mergePackageJson(
51
+ old: Record<string, any>,
52
+ partial: Record<string, any>,
53
+ ): Record<string, any> {
54
+ const merged = { ...old }
55
+
56
+ for (const [key, value] of Object.entries(partial)) {
57
+ if (isPlainObject(merged[key]) && isPlainObject(value)) {
58
+ merged[key] = mergePackageJson(merged[key], value)
59
+ } else {
60
+ merged[key] = value
61
+ }
62
+ }
63
+
64
+ return merged
65
+ }
66
+
46
67
  export async function updatePackageJson(
47
68
  path: string,
48
69
  partial: Record<string, any>,
@@ -53,7 +74,10 @@ export async function updatePackageJson(
53
74
  return
54
75
  }
55
76
  const old = JSON.parse(await readFileAsync(path, 'utf8'))
56
- await writeFileAsync(path, JSON.stringify({ ...old, ...partial }, null, 2))
77
+ await writeFileAsync(
78
+ path,
79
+ JSON.stringify(mergePackageJson(old, partial), null, 2),
80
+ )
57
81
  }
58
82
 
59
83
  export const CLI_VERSION = pkgJson.version
@@ -25,6 +25,10 @@ const NAPI_VERSION_MATRIX = new Map<NapiVersion, string>([
25
25
  [NapiVersion.Napi9, '18.17.0 | 20.3.0 | 21.1.0'],
26
26
  ])
27
27
 
28
+ export const SUPPORTED_NAPI_VERSIONS = Object.values(NapiVersion).filter(
29
+ (v): v is NapiVersion => typeof v === 'number',
30
+ )
31
+
28
32
  interface NodeVersion {
29
33
  major: number
30
34
  minor: number