@napi-rs/cli 3.8.6 → 3.9.1

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.
Files changed (34) hide show
  1. package/dist/cli.js +9085 -8737
  2. package/dist/index.cjs +9092 -8744
  3. package/dist/index.d.cts +37 -17
  4. package/dist/index.d.ts +37 -17
  5. package/dist/index.js +9085 -8737
  6. package/docs/build.md +36 -34
  7. package/package.json +4 -5
  8. package/src/api/__tests__/__snapshots__/templates.spec.ts.md +9 -13
  9. package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
  10. package/src/api/__tests__/build.spec.ts +284 -0
  11. package/src/api/__tests__/create-npm-dirs.spec.ts +4 -3
  12. package/src/api/__tests__/templates.spec.ts +84 -1
  13. package/src/api/build.ts +181 -7
  14. package/src/api/create-npm-dirs.ts +2 -1
  15. package/src/api/new.ts +13 -18
  16. package/src/api/pre-publish.ts +5 -10
  17. package/src/api/rename.ts +10 -21
  18. package/src/api/templates/js-binding.ts +7 -9
  19. package/src/api/templates/load-wasi-template.ts +20 -17
  20. package/src/api/templates/wasi-worker-template.ts +36 -31
  21. package/src/commands/__tests__/build.spec.ts +24 -0
  22. package/src/commands/build.ts +1 -1
  23. package/src/def/build.ts +30 -4
  24. package/src/utils/__tests__/__snapshots__/typegen.spec.ts.md +18 -28
  25. package/src/utils/__tests__/__snapshots__/typegen.spec.ts.snap +0 -0
  26. package/src/utils/__tests__/misc.spec.ts +4 -0
  27. package/src/utils/__tests__/serialize.spec.ts +55 -0
  28. package/src/utils/__tests__/target.spec.ts +221 -0
  29. package/src/utils/__tests__/typegen.spec.ts +115 -0
  30. package/src/utils/index.ts +1 -0
  31. package/src/utils/misc.ts +4 -5
  32. package/src/utils/serialize.ts +47 -0
  33. package/src/utils/target.ts +150 -1
  34. package/src/utils/typegen.ts +241 -30
@@ -7,7 +7,10 @@ import {
7
7
  createWasiBrowserBinding,
8
8
  createWasiDeferredBrowserBinding,
9
9
  } from '../templates/load-wasi-template.js'
10
- import { createWasiBrowserWorkerBinding } from '../templates/wasi-worker-template.js'
10
+ import {
11
+ createWasiBrowserWorkerBinding,
12
+ WASI_WORKER_TEMPLATE,
13
+ } from '../templates/wasi-worker-template.js'
11
14
 
12
15
  const test = ava
13
16
 
@@ -228,6 +231,36 @@ const wasiLoaderCases: Array<{ name: string; code: string }> = [
228
231
  { name: 'deferred/workerd', code: createWasiDeferredBrowserBinding('test') },
229
232
  ]
230
233
 
234
+ test('Node WASI loader uses an accessible host root on Android', (t) => {
235
+ const code = createWasiBinding('test', '@scope/test')
236
+ assertValidJS(t, code, 'Node WASI loader')
237
+ t.true(code.includes('const __cwd = process.cwd()'))
238
+ t.true(code.includes("process.platform === 'android' ? __cwd : __rootDir"))
239
+ t.true(code.includes('[__rootDir]: __hostRoot'))
240
+ t.true(code.includes('[__hostRoot]: __hostRoot'))
241
+ t.true(
242
+ code.includes('workerData: { hostRoot: __hostRoot, rootDir: __rootDir }'),
243
+ )
244
+ t.false(code.includes('[__rootDir]: __rootDir'))
245
+ })
246
+
247
+ test('Node WASI worker uses an accessible host root on Android', (t) => {
248
+ assertValidJS(t, WASI_WORKER_TEMPLATE, 'Node WASI worker')
249
+ t.true(
250
+ WASI_WORKER_TEMPLATE.includes(
251
+ "workerData && typeof workerData.rootDir === 'string' && workerData.rootDir",
252
+ ),
253
+ )
254
+ t.true(
255
+ WASI_WORKER_TEMPLATE.includes(
256
+ "workerData && typeof workerData.hostRoot === 'string' && workerData.hostRoot",
257
+ ),
258
+ )
259
+ t.true(WASI_WORKER_TEMPLATE.includes('[__rootDir]: __hostRoot'))
260
+ t.true(WASI_WORKER_TEMPLATE.includes('[__hostRoot]: __hostRoot'))
261
+ t.false(WASI_WORKER_TEMPLATE.includes('[__rootDir]: __rootDir'))
262
+ })
263
+
231
264
  for (const { name, code } of wasiLoaderCases) {
232
265
  test(`WASI loader waits for queued settlements before destroy: ${name}`, (t) => {
233
266
  t.true(
@@ -284,6 +317,34 @@ function initializationRollbackBody(code: string): string {
284
317
  return code.slice(deferredStart, code.indexOf('throw error', deferredStart))
285
318
  }
286
319
 
320
+ test('createCjsBinding uses one statement dialect', (t) => {
321
+ const code = createCjsBinding('test', '@scope/test', ['sum'], '1.0.0', [
322
+ 'wasm32-wasi',
323
+ ])
324
+ t.false(
325
+ code.includes('NAPI_RS_NATIVE_LIBRARY_PATH);'),
326
+ 'native library require must not use a leftover semicolon',
327
+ )
328
+ t.true(code.includes("const __napiWasiFlavors = ['wasm32-wasi']"))
329
+ t.false(code.includes('"wasm32-wasi"'))
330
+ t.true(code.includes("return require('./test.win32-x64-gnu.node')"))
331
+ const win32Gnu = code.slice(
332
+ code.indexOf("process.arch === 'x64'"),
333
+ code.indexOf("process.arch === 'ia32'"),
334
+ )
335
+ t.true(
336
+ win32Gnu.includes(" return require('./test.win32-x64-gnu.node')"),
337
+ 'win32-x64 gnu local require must be indented inside try',
338
+ )
339
+ })
340
+
341
+ test('WASI worker template matches the CJS/ESM quote and semicolon dialect', (t) => {
342
+ t.true(WASI_WORKER_TEMPLATE.includes("import fs from 'node:fs'"))
343
+ t.false(WASI_WORKER_TEMPLATE.includes('from "node:fs"'))
344
+ t.false(WASI_WORKER_TEMPLATE.includes("from 'node:fs';"))
345
+ t.true(WASI_WORKER_TEMPLATE.includes('memory: wasmMemory,'))
346
+ })
347
+
287
348
  test('createEsmBinding is Node 12 compatible', (t) => {
288
349
  const code = createEsmBinding('test', '@scope/test', ['sum'])
289
350
  assertValidJS(t, code, 'esm')
@@ -294,3 +355,25 @@ test('createEsmBinding is Node 12 compatible', (t) => {
294
355
  t.false(code.includes('?.'), 'ESM loader must not use optional chaining')
295
356
  t.false(code.includes('??'), 'ESM loader must not use nullish coalescing')
296
357
  })
358
+
359
+ test('createEsmBinding builds native addon loading on portable ESM primitives', (t) => {
360
+ const code = createEsmBinding('test', '@scope/test', ['sum'])
361
+ assertValidJS(t, code, 'esm')
362
+ t.true(
363
+ code.includes(`import { createRequire } from 'module'`),
364
+ 'ESM loader must import createRequire so native .node addons can be loaded without the CommonJS `require` global',
365
+ )
366
+ t.true(
367
+ code.includes('const require = createRequire(import.meta.url)'),
368
+ 'ESM loader must derive `require` from the current module, not rely on a CommonJS global',
369
+ )
370
+ // `@napi-rs/cli` is a valid Node package but is not running under Deno. Deno
371
+ // rejects `URL.pathname` treated as a filesystem path, which is exactly how a
372
+ // `__dirname` built from `new URL('.', import.meta.url).pathname` behaves on
373
+ // Windows (percent-encoded, forward-slash separated). The ESM native loader
374
+ // never used `__dirname` anyway, so it must not emit that construct at all.
375
+ t.false(
376
+ /new URL\(['"]\.['"], import\.meta\.url\)\.pathname/.test(code),
377
+ 'ESM loader must not treat URL.pathname as a filesystem path',
378
+ )
379
+ })
package/src/api/build.ts CHANGED
@@ -45,7 +45,10 @@ import {
45
45
  targetToEnvVar,
46
46
  tryInstallCargoBinary,
47
47
  unlinkAsync,
48
+ rustBundledWasiLibc,
49
+ wasiLibcHasNewFutexAbi,
48
50
  wasiLoaderSuffix,
51
+ wasiSdkMajorVersion,
49
52
  wasiTargetHasThreads,
50
53
  writeFileAtomic,
51
54
  withFileSystemReconciliation,
@@ -84,8 +87,13 @@ type WasiBindingMetadata = {
84
87
  bindingTypeDef?: string
85
88
  }
86
89
 
90
+ export type BuildFormat = 'esm' | 'commonjs'
91
+
87
92
  type BuildOptions = RawBuildOptions & { cargoOptions?: string[] }
88
- type ParsedBuildOptions = Omit<BuildOptions, 'cwd'> & { cwd: string }
93
+ type ParsedBuildOptions = Omit<BuildOptions, 'cwd' | 'format'> & {
94
+ cwd: string
95
+ format: BuildFormat
96
+ }
89
97
 
90
98
  export const WASI_ARTIFACT_METADATA_PREFIX = '// napi-rs-artifact-metadata:'
91
99
 
@@ -218,6 +226,83 @@ export function getTypeDefCacheFolder(options: {
218
226
  return join(options.targetDir, 'napi-rs', `${options.crateName}-${hash}`)
219
227
  }
220
228
 
229
+ /**
230
+ * Name of the emnapi archive directory built against the wasi-sdk 34 ABI.
231
+ * Only the threaded target has one: the signature change it carries lives in
232
+ * the threads-only futex code.
233
+ */
234
+ export const EMNAPI_WASI_SDK_34_LINK_DIR = 'wasm32-wasip1-threads-wasi-sdk-34'
235
+
236
+ export interface EmnapiLinkDirSelection {
237
+ /** Directory name under `emnapi/lib` whose archives should be linked. */
238
+ linkDirName: string
239
+ /** Detected wasi-sdk major version, `null` when no wasi-sdk is configured. */
240
+ wasiSdkMajor: number | null
241
+ /** `true` when the toolchain needs the wasi-sdk 34 archives. */
242
+ needsWasiSdk34: boolean
243
+ }
244
+
245
+ /**
246
+ * Pick the emnapi archive directory for a WASI build.
247
+ *
248
+ * wasi-libc removed the unused `int op` parameter from
249
+ * `__wasilibc_futex_wait_atomic_wait` / `__wasilibc_futex_wait_maybe_busy`,
250
+ * and wasi-sdk 34 is the first release shipping the 3-argument signature.
251
+ * Linking the legacy 4-argument archives against a wasi-sdk >= 34 sysroot
252
+ * makes `wasm-ld` report `function signature mismatch` and emit an invalid
253
+ * wasm module, so emnapi publishes a second archive set for the new ABI and
254
+ * the directory has to be chosen by wasi-sdk version, not by target triple.
255
+ *
256
+ * Without `WASI_SDK_PATH`, cargo links through `rust-lld` against the
257
+ * wasi-libc bundled with the Rust standard library, so that archive is
258
+ * probed instead. Rust picked up wasi-sdk 34 in rust-lang/rust#161773, which
259
+ * lands in 1.100, and a toolchain carrying it needs the new archives even
260
+ * though no wasi-sdk is configured.
261
+ *
262
+ * The legacy directory stays the default whenever the ABI cannot be
263
+ * determined, and older emnapi releases ship it alone.
264
+ */
265
+ export function selectEmnapiLinkDir(
266
+ emnapiLibDir: string,
267
+ wasiTarget: string,
268
+ hasThreads: boolean,
269
+ wasiSdkPath: string | undefined,
270
+ {
271
+ cwd,
272
+ rustWasiLibc,
273
+ }: {
274
+ // The directory Cargo is spawned in. It selects the toolchain, so the
275
+ // sysroot probe has to use it too.
276
+ cwd?: string
277
+ // Omit to resolve the Rust sysroot lazily, so a configured wasi-sdk never
278
+ // pays for a `rustc` invocation. Pass `null` to skip the probe.
279
+ rustWasiLibc?: string | null
280
+ } = {},
281
+ ): EmnapiLinkDirSelection {
282
+ const usingWasiSdk = Boolean(wasiSdkPath) && existsSync(wasiSdkPath!)
283
+ const wasiSdkMajor = usingWasiSdk ? wasiSdkMajorVersion(wasiSdkPath!) : null
284
+ // Whichever wasi-libc actually gets linked decides the ABI: the wasi-sdk
285
+ // sysroot when one is configured, otherwise the copy bundled with the Rust
286
+ // standard library. Only the threaded target has a second archive set, so
287
+ // `hasThreads` gates the probe as well as the result — a threadless build
288
+ // must not pay for a `rustc` invocation and an archive read it cannot use.
289
+ const needsWasiSdk34 =
290
+ hasThreads &&
291
+ (usingWasiSdk
292
+ ? wasiSdkMajor !== null && wasiSdkMajor >= 34
293
+ : wasiLibcHasNewFutexAbi(
294
+ rustWasiLibc === undefined
295
+ ? rustBundledWasiLibc(wasiTarget, cwd)
296
+ : rustWasiLibc,
297
+ ) === true)
298
+ const linkDirName =
299
+ needsWasiSdk34 &&
300
+ existsSync(join(emnapiLibDir, EMNAPI_WASI_SDK_34_LINK_DIR))
301
+ ? EMNAPI_WASI_SDK_34_LINK_DIR
302
+ : wasiTarget
303
+ return { linkDirName, wasiSdkMajor, needsWasiSdk34 }
304
+ }
305
+
221
306
  export function createWasiCompilerFlags(
222
307
  wasiSdkPath: string,
223
308
  wasiTarget: string,
@@ -569,12 +654,57 @@ function readWasmU32(binary: Uint8Array, start: number) {
569
654
  throw new Error('Invalid WebAssembly unsigned LEB128 value')
570
655
  }
571
656
 
657
+ type BuildFormatOptions = {
658
+ format?: string
659
+ esm?: boolean
660
+ commonjs?: boolean
661
+ }
662
+
663
+ /**
664
+ * Resolve the explicit build format and its legacy CLI/API aliases.
665
+ *
666
+ * CommonJS is the historical default. The aliases are intentionally kept
667
+ * separate from `format` because `--esm` and `--commonjs` are boolean flags,
668
+ * while `--format` takes a value.
669
+ */
670
+ export function resolveBuildFormat(options: BuildFormatOptions): BuildFormat {
671
+ let format: BuildFormat | undefined
672
+ if (options.format !== undefined) {
673
+ if (options.format === 'esm' || options.format === 'commonjs') {
674
+ format = options.format
675
+ } else {
676
+ throw new Error(
677
+ `Invalid build format ${JSON.stringify(options.format)}. Expected \`esm\` or \`commonjs\`.`,
678
+ )
679
+ }
680
+ }
681
+
682
+ if (options.esm && options.commonjs) {
683
+ throw new Error('`--esm` and `--commonjs` cannot be used together.')
684
+ }
685
+
686
+ const aliasFormat = options.esm
687
+ ? 'esm'
688
+ : options.commonjs
689
+ ? 'commonjs'
690
+ : undefined
691
+ if (format && aliasFormat && format !== aliasFormat) {
692
+ throw new Error(
693
+ `\`--format ${format}\` cannot be used with \`--${aliasFormat}\`.`,
694
+ )
695
+ }
696
+
697
+ return format ?? aliasFormat ?? 'commonjs'
698
+ }
699
+
700
+ /** Build the configured NAPI-RS crate and generate its output artifacts. */
572
701
  export async function buildProject(rawOptions: BuildOptions) {
573
702
  debug('napi build command receive options: %O', rawOptions)
574
703
 
575
704
  const options: ParsedBuildOptions = {
576
705
  dtsCache: true,
577
706
  ...rawOptions,
707
+ format: resolveBuildFormat(rawOptions),
578
708
  cwd: rawOptions.cwd ?? process.cwd(),
579
709
  }
580
710
 
@@ -1411,16 +1541,40 @@ class Builder {
1411
1541
  private setWasiEnv() {
1412
1542
  const hasThreads = wasiTargetHasThreads(this.target)
1413
1543
  const wasiTarget = hasThreads ? 'wasm32-wasip1-threads' : 'wasm32-wasip1'
1414
- const emnapi = join(require.resolve('emnapi'), '..', 'lib', wasiTarget)
1544
+ const emnapiLibDir = join(require.resolve('emnapi'), '..', 'lib')
1415
1545
  const emnapiVersion = require('emnapi/package.json').version
1546
+ const { WASI_SDK_PATH } = process.env
1547
+ // `wasiTarget` stays the real target triple, because it is what the clang
1548
+ // driver is told to build for below. The emnapi archive directory is a
1549
+ // separate choice: emnapi ships two archive sets for the threaded target,
1550
+ // one per wasi-libc futex ABI. See `selectEmnapiLinkDir`.
1551
+ const { linkDirName, wasiSdkMajor, needsWasiSdk34 } = selectEmnapiLinkDir(
1552
+ emnapiLibDir,
1553
+ wasiTarget,
1554
+ hasThreads,
1555
+ WASI_SDK_PATH,
1556
+ { cwd: this.options.cwd },
1557
+ )
1558
+ const emnapi = join(emnapiLibDir, linkDirName)
1416
1559
  // Keep this in sync with `emnapi_link_library` in `crates/build/src/wasi.rs`.
1417
1560
  const emnapiArchive = join(
1418
1561
  emnapi,
1419
1562
  hasThreads ? 'libemnapi-napi-rs-mt.a' : 'libemnapi-basic-napi-rs.a',
1420
1563
  )
1564
+ const fellBackToLegacy =
1565
+ needsWasiSdk34 && linkDirName !== EMNAPI_WASI_SDK_34_LINK_DIR
1421
1566
  if (!existsSync(emnapiArchive)) {
1422
1567
  throw new Error(
1423
- `emnapi@${emnapiVersion} is missing the ${wasiTarget} archive required by napi-rs at ${emnapiArchive}. Install emnapi v2 with support for this target.`,
1568
+ fellBackToLegacy
1569
+ ? `emnapi@${emnapiVersion} does not ship the ${EMNAPI_WASI_SDK_34_LINK_DIR} archives that wasi-sdk ${wasiSdkMajor} requires, and the ${linkDirName} archive it fell back to is missing too at ${emnapiArchive}. Upgrade emnapi to a version that ships the wasi-sdk 34 archives.`
1570
+ : needsWasiSdk34
1571
+ ? `emnapi@${emnapiVersion} is missing the ${linkDirName} archive required by napi-rs at ${emnapiArchive}. wasi-sdk ${wasiSdkMajor} needs those archives, so upgrade emnapi to a version that ships them.`
1572
+ : `emnapi@${emnapiVersion} is missing the ${linkDirName} archive required by napi-rs at ${emnapiArchive}. Install emnapi v2 with support for this target.`,
1573
+ )
1574
+ }
1575
+ if (fellBackToLegacy) {
1576
+ debug.warn(
1577
+ `emnapi@${emnapiVersion} does not ship the ${EMNAPI_WASI_SDK_34_LINK_DIR} archives that wasi-sdk ${wasiSdkMajor} requires. Falling back to ${linkDirName}, which links the legacy wasi-libc futex ABI and may fail with \`wasm-ld: function signature mismatch\`. Upgrade emnapi to fix this.`,
1424
1578
  )
1425
1579
  }
1426
1580
  this.envs.EMNAPI_LINK_DIR = emnapi
@@ -1449,7 +1603,6 @@ class Builder {
1449
1603
  `emnapi version mismatch: emnapi@${emnapiVersion}, @emnapi/core@${emnapiCoreVersion}, @emnapi/runtime@${emnapiRuntimeVersion}. Please ensure all emnapi packages are the same version.`,
1450
1604
  )
1451
1605
  }
1452
- const { WASI_SDK_PATH } = process.env
1453
1606
 
1454
1607
  if (WASI_SDK_PATH && existsSync(WASI_SDK_PATH)) {
1455
1608
  this.envs.CARGO_TARGET_WASM32_WASI_PREVIEW1_THREADS_LINKER = join(
@@ -2003,7 +2156,7 @@ class Builder {
2003
2156
  noJsBinding: this.options.noJsBinding,
2004
2157
  idents,
2005
2158
  jsBinding: this.options.jsBinding,
2006
- esm: this.options.esm,
2159
+ format: this.options.format,
2007
2160
  binaryName: this.config.binaryName,
2008
2161
  packageName: this.options.jsPackageName ?? this.config.packageName,
2009
2162
  version: process.env.npm_new_version ?? this.config.packageJson.version,
@@ -2352,7 +2505,9 @@ export interface WriteJsBindingOptions {
2352
2505
  noJsBinding?: boolean
2353
2506
  idents: string[]
2354
2507
  jsBinding?: string
2508
+ format?: BuildFormat
2355
2509
  esm?: boolean
2510
+ commonjs?: boolean
2356
2511
  binaryName: string
2357
2512
  packageName: string
2358
2513
  version: string
@@ -2366,6 +2521,7 @@ export interface WriteJsBindingOptions {
2366
2521
  wasiFlavors?: string[]
2367
2522
  }
2368
2523
 
2524
+ /** Write a platform binding loader in the requested module format. */
2369
2525
  export async function writeJsBinding(
2370
2526
  options: WriteJsBindingOptions,
2371
2527
  ): Promise<Output | undefined> {
@@ -2389,7 +2545,8 @@ export async function writeJsBinding(
2389
2545
  ? localWasiName
2390
2546
  : `./${localWasiName}`
2391
2547
 
2392
- const createBinding = options.esm ? createEsmBinding : createCjsBinding
2548
+ const createBinding =
2549
+ resolveBuildFormat(options) === 'esm' ? createEsmBinding : createCjsBinding
2393
2550
  const binding = createBinding(
2394
2551
  options.binaryName,
2395
2552
  options.packageName,
@@ -2506,10 +2663,27 @@ export declare class ExternalObject<T> {
2506
2663
 
2507
2664
  if (dts.indexOf('TypedArray') > -1) {
2508
2665
  header += `
2509
- export type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array
2666
+ export type TypedArray =
2667
+ | Int8Array
2668
+ | Uint8Array
2669
+ | Uint8ClampedArray
2670
+ | Int16Array
2671
+ | Uint16Array
2672
+ | Int32Array
2673
+ | Uint32Array
2674
+ | Float32Array
2675
+ | Float64Array
2676
+ | BigInt64Array
2677
+ | BigUint64Array
2510
2678
  `
2511
2679
  }
2512
2680
 
2681
+ if (header && !header.endsWith('\n')) {
2682
+ header += '\n'
2683
+ }
2684
+ if (header && !header.endsWith('\n\n')) {
2685
+ header += '\n'
2686
+ }
2513
2687
  dts = header + dts
2514
2688
  const dtsWithTypeImports = rewriteTypeImportReferences(
2515
2689
  header + dtsWithTypeImportMarkers,
@@ -28,6 +28,7 @@ import {
28
28
  pick,
29
29
  resolvePackageReconciliationPaths,
30
30
  restrictWasiNodeEngine,
31
+ serializeJson,
31
32
  wasiLoaderSuffix,
32
33
  wasiTargetHasThreads,
33
34
  withFileSystemReconciliation,
@@ -574,7 +575,7 @@ async function createNpmDirsUnlocked(
574
575
 
575
576
  const targetPackageJson = join(targetDir, 'package.json')
576
577
  pendingWrites.push({
577
- content: JSON.stringify(scopedPackageJson, null, 2) + '\n',
578
+ content: serializeJson(scopedPackageJson),
578
579
  destination: targetPackageJson,
579
580
  })
580
581
  if (wasmModuleTypeDef) {
package/src/api/new.ts CHANGED
@@ -4,8 +4,8 @@ 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'
8
- import { load as yamlLoad, dump as yamlDump } from 'js-yaml'
7
+ import { parse as parseToml } from '@std/toml'
8
+ import { load as yamlLoad } from 'js-yaml'
9
9
 
10
10
  import {
11
11
  applyDefaultNewOptions,
@@ -15,6 +15,9 @@ import {
15
15
  AVAILABLE_TARGETS,
16
16
  CLI_VERSION,
17
17
  debugFactory,
18
+ serializeJson,
19
+ serializeToml,
20
+ serializeYaml,
18
21
  DEFAULT_TARGETS,
19
22
  getWasiTarget,
20
23
  mkdirAsync,
@@ -347,7 +350,7 @@ async function filterTargetsInPackageJson(
347
350
  }
348
351
  }
349
352
 
350
- await fs.writeFile(filePath, JSON.stringify(packageJson, null, 2) + '\n')
353
+ await fs.writeFile(filePath, serializeJson(packageJson))
351
354
  await updateGeneratedWasiAttributes(
352
355
  path.join(path.dirname(filePath), '.gitattributes'),
353
356
  packageJson.napi.binaryName,
@@ -381,10 +384,10 @@ async function updateGeneratedWasiAttributes(
381
384
  generatedFiles.add(`${binaryName}.${suffix}-deferred.d.ts`)
382
385
  }
383
386
  }
387
+ while (lines[lines.length - 1] === '') {
388
+ lines.pop()
389
+ }
384
390
  if (generatedFiles.size > 0) {
385
- while (lines[lines.length - 1] === '') {
386
- lines.pop()
387
- }
388
391
  lines.push(
389
392
  '',
390
393
  ...[...generatedFiles].map((file) => `${file} linguist-detectable=false`),
@@ -429,7 +432,7 @@ async function updateCargoTomlTypeDef(
429
432
 
430
433
  dependencies['napi-derive'] = dependencyConfig
431
434
 
432
- await fs.writeFile(filePath, stringifyToml(cargoToml))
435
+ await fs.writeFile(filePath, serializeToml(cargoToml))
433
436
  }
434
437
 
435
438
  export async function updateCargoTomlNodeApiVersion(
@@ -475,7 +478,7 @@ export async function updateCargoTomlNodeApiVersion(
475
478
 
476
479
  dependencies.napi = dependencyConfig
477
480
 
478
- await fs.writeFile(filePath, stringifyToml(cargoToml))
481
+ await fs.writeFile(filePath, serializeToml(cargoToml))
479
482
  }
480
483
 
481
484
  async function filterTargetsInGithubActions(
@@ -683,12 +686,7 @@ async function filterTargetsInGithubActions(
683
686
  }
684
687
 
685
688
  // Write back the filtered YAML
686
- const updatedYaml = yamlDump(yaml, {
687
- lineWidth: -1,
688
- noRefs: true,
689
- sortKeys: false,
690
- })
691
- await fs.writeFile(filePath, updatedYaml)
689
+ await fs.writeFile(filePath, serializeYaml(yaml))
692
690
  }
693
691
 
694
692
  function processOptions(options: RawNewOptions) {
@@ -853,10 +851,7 @@ export async function newProject(userOptions: RawNewOptions) {
853
851
  )
854
852
  }
855
853
 
856
- await fs.writeFile(
857
- packageJsonPath,
858
- JSON.stringify(pkgJson, null, 2) + '\n',
859
- )
854
+ await fs.writeFile(packageJsonPath, serializeJson(pkgJson))
860
855
  } catch (error) {
861
856
  throw new Error(`Failed to create project: ${error}`)
862
857
  }
@@ -58,6 +58,7 @@ import {
58
58
  withFileSystemReconciliation,
59
59
  AVAILABLE_TARGETS,
60
60
  parseTriple,
61
+ serializeJson,
61
62
  type CommonPackageJsonFields,
62
63
  type FileSystemTransactionWrite,
63
64
  type RootPublisher,
@@ -2267,7 +2268,7 @@ async function registerPublicationWorkspaces(
2267
2268
  } else {
2268
2269
  manifest.workspaces = releaseWorkspaces
2269
2270
  }
2270
- await writeFileAtomic(manifestPath, JSON.stringify(manifest, null, 2))
2271
+ await writeFileAtomic(manifestPath, serializeJson(manifest))
2271
2272
  }
2272
2273
 
2273
2274
  async function createPublicationExecutionPackage(
@@ -2391,10 +2392,7 @@ async function materializeRootReleasePlan(
2391
2392
  }
2392
2393
  }
2393
2394
  syncThreadlessWasiRootFacadeManifest(updatedPackageJson, plan.packageJson)
2394
- await writeFileAtomic(
2395
- packageJsonPath,
2396
- JSON.stringify(updatedPackageJson, null, 2),
2397
- )
2395
+ await writeFileAtomic(packageJsonPath, serializeJson(updatedPackageJson))
2398
2396
 
2399
2397
  const generatedFiles = new Set(plan.facade?.generatedFiles ?? [])
2400
2398
  await Promise.all(
@@ -2492,7 +2490,7 @@ async function stageReleasePackage(
2492
2490
  const packageJsonPath = join(stagedPkgDir, 'package.json')
2493
2491
  const packageJson = JSON.parse(await readFileAsync(packageJsonPath, 'utf8'))
2494
2492
  packageJson.version = packageVersion
2495
- await writeFileAtomic(packageJsonPath, JSON.stringify(packageJson, null, 2))
2493
+ await writeFileAtomic(packageJsonPath, serializeJson(packageJson))
2496
2494
  return {
2497
2495
  pkgDir: options.pkgDir,
2498
2496
  rootDir: options.rootDir,
@@ -2705,10 +2703,7 @@ async function validateReleasePackageContents({
2705
2703
  if (updateManifest) {
2706
2704
  packageFiles.splice(0, packageFiles.length, ...declarationClosure.files)
2707
2705
  packageJson.files = packageFiles
2708
- await writeFileAtomic(
2709
- packageJsonPath,
2710
- `${JSON.stringify(packageJson, null, 2)}\n`,
2711
- )
2706
+ await writeFileAtomic(packageJsonPath, serializeJson(packageJson))
2712
2707
  }
2713
2708
 
2714
2709
  const publicFiles = new Set<string>(packageFiles)
package/src/api/rename.ts CHANGED
@@ -12,8 +12,8 @@ import {
12
12
  sep,
13
13
  } from 'node:path'
14
14
 
15
- import { parse as parseToml, stringify as stringifyToml } from '@std/toml'
16
- import { load as yamlParse, dump as yamlStringify } from 'js-yaml'
15
+ import { parse as parseToml } from '@std/toml'
16
+ import { load as yamlParse } from 'js-yaml'
17
17
  import { isNil, omitBy, pick } from 'es-toolkit'
18
18
 
19
19
  import { applyDefaultRenameOptions, type RenameOptions } from '../def/rename.js'
@@ -23,6 +23,9 @@ import {
23
23
  readFileAsync,
24
24
  resolvePackageReconciliationPaths,
25
25
  type Target,
26
+ serializeJson,
27
+ serializeToml,
28
+ serializeYaml,
26
29
  wasiLoaderSuffix,
27
30
  wasiTargetHasThreads,
28
31
  withPackageFileSystemReconciliation,
@@ -1265,10 +1268,6 @@ function updateNapiConfigRecord(record: JsonRecord, options: RenameOptions) {
1265
1268
  }
1266
1269
  }
1267
1270
 
1268
- function serializeJsonLike(content: string, value: unknown) {
1269
- return `${JSON.stringify(value, null, 2)}${content.endsWith('\n') ? '\n' : ''}`
1270
- }
1271
-
1272
1271
  function sanitizeCargoPackageName(binaryName: string) {
1273
1272
  return binaryName.replace(/[^A-Za-z0-9_]/g, '_').toLowerCase()
1274
1273
  }
@@ -1458,8 +1457,7 @@ async function renameProjectUnlocked(
1458
1457
  const plan = new RenameTransactionPlan()
1459
1458
  plan.addWrite(
1460
1459
  packageJsonPath,
1461
- serializeJsonLike(
1462
- packageJsonContent,
1460
+ serializeJson(
1463
1461
  rewritePackageManifest(
1464
1462
  packageJsonData,
1465
1463
  managedWasiRenames,
@@ -1477,11 +1475,7 @@ async function renameProjectUnlocked(
1477
1475
  throw new Error(`NAPI config must contain a JSON object: ${configPath}`)
1478
1476
  }
1479
1477
  updateNapiConfigRecord(configData, options)
1480
- plan.addWrite(
1481
- configPath,
1482
- serializeJsonLike(configContent, configData),
1483
- configMode,
1484
- )
1478
+ plan.addWrite(configPath, serializeJson(configData), configMode)
1485
1479
  }
1486
1480
 
1487
1481
  if (binaryNameChanged) {
@@ -1495,7 +1489,7 @@ async function renameProjectUnlocked(
1495
1489
  const cargoToml = parseToml(tomlContent) as any
1496
1490
  if (cargoToml.package) {
1497
1491
  cargoToml.package.name = sanitizeCargoPackageName(newName)
1498
- plan.addWrite(cargoTomlPath, stringifyToml(cargoToml), cargoTomlMode)
1492
+ plan.addWrite(cargoTomlPath, serializeToml(cargoToml), cargoTomlMode)
1499
1493
  }
1500
1494
 
1501
1495
  const workflowPath = join(projectRoot, '.github', 'workflows', 'CI.yml')
@@ -1515,11 +1509,7 @@ async function renameProjectUnlocked(
1515
1509
  workflowData.env.APP_NAME = newName
1516
1510
  plan.addWrite(
1517
1511
  canonicalWorkflowPath,
1518
- yamlStringify(workflowData, {
1519
- lineWidth: -1,
1520
- noRefs: true,
1521
- sortKeys: false,
1522
- }),
1512
+ serializeYaml(workflowData),
1523
1513
  workflowMode,
1524
1514
  )
1525
1515
  }
@@ -1567,8 +1557,7 @@ async function renameProjectUnlocked(
1567
1557
  `Managed package manifest must contain a JSON object: ${targetPackageJsonPath}`,
1568
1558
  )
1569
1559
  }
1570
- const updatedContent = serializeJsonLike(
1571
- content,
1560
+ const updatedContent = serializeJson(
1572
1561
  rewritePackageManifest(
1573
1562
  manifest,
1574
1563
  managedWasiRenames,
@@ -43,11 +43,11 @@ function createWasiFallbackChain(
43
43
  specifier,
44
44
  isPackage,
45
45
  localArtifacts,
46
- }) => ` if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === ${JSON.stringify(flavor)})) {
46
+ }) => ` if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === '${flavor}')) {
47
47
  let candidateError = null
48
48
  let candidateFailed = false
49
49
  try {
50
- candidateError = __napiWasiResolveCandidate('${specifier}', ${isPackage}, ${JSON.stringify(localArtifacts)})
50
+ candidateError = __napiWasiResolveCandidate('${specifier}', ${isPackage}, ${localArtifacts ? `[${localArtifacts.map((artifact) => `'${artifact}'`).join(', ')}]` : 'undefined'})
51
51
  candidateFailed = candidateError !== null
52
52
  if (!candidateFailed) {${
53
53
  isPackage && packageVersion
@@ -157,7 +157,6 @@ ${idents.map((ident) => `export { ${ident} }`).join('\n')}`
157
157
  return `${bindingHeader}
158
158
  import { createRequire } from 'module'
159
159
  const require = createRequire(import.meta.url)
160
- const __dirname = new URL('.', import.meta.url).pathname
161
160
 
162
161
  ${createCommonBinding(
163
162
  localName,
@@ -170,8 +169,7 @@ ${exportsCode}
170
169
  `
171
170
  }
172
171
 
173
- const bindingHeader = `// prettier-ignore
174
- /* eslint-disable */
172
+ const bindingHeader = `/* eslint-disable */
175
173
  // @ts-nocheck
176
174
  /* auto-generated by NAPI-RS */
177
175
  `
@@ -273,7 +271,7 @@ const isMuslFromChildProcess = () => {
273
271
  function requireNative() {
274
272
  if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
275
273
  try {
276
- return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH);
274
+ return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
277
275
  } catch (err) {
278
276
  loadErrors.push(err)
279
277
  }
@@ -288,9 +286,9 @@ function requireNative() {
288
286
  } else if (process.platform === 'win32') {
289
287
  if (process.arch === 'x64') {
290
288
  if ((process.config && process.config.variables && process.config.variables.shlib_suffix === 'dll.a') || (process.config && process.config.variables && process.config.variables.node_target_type === 'shared_library')) {
291
- ${requireTuple('win32-x64-gnu')}
289
+ ${requireTuple('win32-x64-gnu', 10)}
292
290
  } else {
293
- ${requireTuple('win32-x64-msvc')}
291
+ ${requireTuple('win32-x64-msvc', 10)}
294
292
  }
295
293
  } else if (process.arch === 'ia32') {
296
294
  ${requireTuple('win32-ia32-msvc')}
@@ -396,7 +394,7 @@ function createLoadErrorChain(errors) {
396
394
  //
397
395
  // NAPI_RS_WASI_FLAVOR selects one exact generated flavor and implies strict
398
396
  // WASI loading. It never crosses into another flavor or falls back to native.
399
- const __napiWasiFlavors = ${JSON.stringify(flavors)}
397
+ const __napiWasiFlavors = [${flavors.map((flavor) => `'${flavor}'`).join(', ')}]
400
398
  const __napiWasiFlavor = process.env.NAPI_RS_WASI_FLAVOR
401
399
  const __napiWasiFlavorRequested =
402
400
  typeof __napiWasiFlavor === 'string' && __napiWasiFlavor.length > 0