@napi-rs/cli 3.9.0 → 3.10.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.
Files changed (39) hide show
  1. package/README.md +17 -4
  2. package/dist/cli.js +10359 -8729
  3. package/dist/index.cjs +10381 -8751
  4. package/dist/index.d.cts +83 -16
  5. package/dist/index.d.ts +83 -16
  6. package/dist/index.js +10359 -8729
  7. package/docs/wasi.md +318 -4
  8. package/package.json +5 -6
  9. package/src/api/__tests__/__snapshots__/templates.spec.ts.md +4003 -53
  10. package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
  11. package/src/api/__tests__/build-regressions.spec.ts +210 -3
  12. package/src/api/__tests__/build.spec.ts +2333 -3
  13. package/src/api/__tests__/create-npm-dirs.spec.ts +105 -3
  14. package/src/api/__tests__/pre-publish.spec.ts +153 -1
  15. package/src/api/__tests__/templates.spec.ts +1309 -1
  16. package/src/api/build.ts +756 -30
  17. package/src/api/create-npm-dirs.ts +23 -10
  18. package/src/api/new.ts +13 -18
  19. package/src/api/pre-publish.ts +34 -11
  20. package/src/api/rename.ts +10 -21
  21. package/src/api/templates/binding-target.ts +176 -0
  22. package/src/api/templates/index.ts +1 -0
  23. package/src/api/templates/js-binding.ts +54 -10
  24. package/src/api/templates/load-wasi-template.ts +661 -58
  25. package/src/api/templates/wasi-worker-template.ts +36 -31
  26. package/src/commands/build.ts +1 -1
  27. package/src/utils/__tests__/__snapshots__/typegen.spec.ts.md +18 -28
  28. package/src/utils/__tests__/__snapshots__/typegen.spec.ts.snap +0 -0
  29. package/src/utils/__tests__/misc.spec.ts +4 -0
  30. package/src/utils/__tests__/reconciliation.spec.ts +676 -0
  31. package/src/utils/__tests__/serialize.spec.ts +55 -0
  32. package/src/utils/__tests__/target.spec.ts +221 -0
  33. package/src/utils/__tests__/typegen.spec.ts +115 -0
  34. package/src/utils/config.ts +50 -0
  35. package/src/utils/index.ts +1 -0
  36. package/src/utils/misc.ts +351 -79
  37. package/src/utils/serialize.ts +47 -0
  38. package/src/utils/target.ts +150 -1
  39. package/src/utils/typegen.ts +608 -42
package/docs/wasi.md CHANGED
@@ -40,6 +40,121 @@ Without `NAPI_RS_WASI_FLAVOR`, existing behavior is unchanged.
40
40
  lazy native fallback, while `NAPI_RS_FORCE_WASI=error` requires some generated
41
41
  WASI flavor to load.
42
42
 
43
+ ## Identifying the loaded artifact
44
+
45
+ Every generated loader exports `__napiBindingTarget`, a string naming the
46
+ artifact that actually loaded:
47
+
48
+ | value | artifact |
49
+ | ----------------- | -------------------------- |
50
+ | `'native'` | a `.node` addon |
51
+ | `'wasm32-wasi'` | the threaded WASI flavor |
52
+ | `'wasm32-wasip1'` | the threadless WASI flavor |
53
+
54
+ The WASI values are the same flavor identities `NAPI_RS_WASI_FLAVOR` accepts,
55
+ so a pinned flavor round-trips:
56
+
57
+ ```js
58
+ process.env.NAPI_RS_WASI_FLAVOR = 'wasm32-wasip1'
59
+ const binding = require('<package>')
60
+ binding.__napiBindingTarget // 'wasm32-wasip1'
61
+ ```
62
+
63
+ Remember that `wasm32-wasi` is the _threaded_ flavor; see the target aliases at
64
+ the top of this page.
65
+
66
+ The root Node.js entry sets the value from the fallback candidate it resolved,
67
+ not from anything the WASI loader reports, so it is correct even for a loader
68
+ that fails to initialize its own exports. The one exception is
69
+ `NAPI_RS_NATIVE_LIBRARY_PATH`: that override can point at a generated WASI
70
+ loader, so the root entry adopts the `__napiBindingTarget` the required module
71
+ reports and falls back to `'native'` when it reports none.
72
+
73
+ Each flavor's own loaders (the CommonJS loader, the browser loader and the
74
+ deferred `./workerd` loader) carry their own fixed flavor identity, and they
75
+ carry it on the binding object they hand out — not only as a module export. So
76
+ the browser loader's default export, `instantiate()`'s result and
77
+ `createInstance().exports` all answer `__napiBindingTarget`, which is what the
78
+ generated declarations promise:
79
+
80
+ ```js
81
+ import { instantiate } from '<package>/workerd'
82
+ const binding = await instantiate(wasmModule)
83
+ binding.__napiBindingTarget // 'wasm32-wasip1'
84
+ ```
85
+
86
+ An addon that seals or freezes its exports in a `#[napi(module_exports)]` hook
87
+ makes the loader skip the stamp on the binding object rather than fail the load,
88
+ and what survives that skip follows the entry point. The browser and the
89
+ deferred `./workerd` entries go on reporting the flavor from their module-level
90
+ `__napiBindingTarget` export; only the copy on the binding object they hand out
91
+ is missing. The CommonJS entries hand back the binding object itself as
92
+ `module.exports`, so there `__napiBindingTarget` reads `undefined` —
93
+ deliberately, because failing an otherwise successful load over a metadata
94
+ string is the worse trade.
95
+
96
+ The CommonJS loaders assign the stamp helper's return value
97
+ (`module.exports.__napiBindingTarget = __napiStampBindingTarget(...)`) rather
98
+ than calling it as a statement, so `cjs-module-lexer` — Node's CommonJS-to-ESM
99
+ named export detection — keeps seeing the name and
100
+ `import { __napiBindingTarget } from '<package>'` goes on working. On a frozen
101
+ binding the import still links; the value is `undefined`, matching the skipped
102
+ stamp.
103
+
104
+ Each loader stamps exactly once, and always in the same place: after the async
105
+ runtime hosts are installed — addon registration functions get the exports
106
+ object first, so the guard reads its final state — and inside the initialization
107
+ guard, so a conflict fails the load through the rollback rather than past it.
108
+ The CommonJS loaders additionally assign onto their own `module.exports` rather
109
+ than onto the addon's object, which leaves an addon accessor with a refusing
110
+ setter untouched; the root entry stamps before it aliases the binding, for the
111
+ same reason.
112
+
113
+ The name is reserved by the builds that emit a loader. `napi build` rejects an
114
+ export of that name it can see in the type-def metadata, but only when this
115
+ build writes a loader to carry it — a root loader (`--platform` without
116
+ `--no-js`), or a WASI flavor loader set. A plain `.node` build writes neither,
117
+ declares nothing, and is free to export the name itself. A name attached
118
+ dynamically from a `#[napi(module_exports)]` hook is invisible at build time, so
119
+ the loader rejects it at load with `ERR_NAPI_BINDING_TARGET_CONFLICT` instead of
120
+ silently overwriting it. In a WASI loader that rejection happens inside the
121
+ initialization boundary, so the conflict rolls the environment back — no
122
+ emnapi context and no `'exit'` listener survive the failed `require()`.
123
+
124
+ Use it to branch on capabilities a native addon has and a WASI build does not
125
+ (worker threads, blocking calls, host timers) without probing:
126
+
127
+ ```js
128
+ if (binding.__napiBindingTarget !== 'native') {
129
+ // running on WebAssembly
130
+ }
131
+ ```
132
+
133
+ When napi-rs type generation is enabled the export is declared in the generated
134
+ declaration files, so the check narrows in TypeScript. Which type a declaration
135
+ gives it follows the entry it types:
136
+
137
+ - The **root entry**'s declaration is a literal union of `'native'` and every
138
+ WASI flavor napi-rs can build, not only the flavors the package itself
139
+ builds. `NAPI_RS_NATIVE_LIBRARY_PATH` can point the root entry at any
140
+ generated WASI loader, so even a package that ships only a native addon can
141
+ report a WASI flavor, and a narrower union would reject comparisons the
142
+ override can actually reach.
143
+ - A **flavor's own** declaration — the CommonJS and browser `.d.cts` and the
144
+ deferred `./workerd` `.d.ts` alike — is that one flavor's exact literal. Those
145
+ loaders bake their flavor in at generation time and read no override, so a
146
+ consumer importing a fixed artifact narrows to a single value, which is what
147
+ the generated declarations promise above.
148
+
149
+ Both bullets describe an extensible binding. For a sealed or frozen addon the
150
+ root **CommonJS** entry reports `undefined` at runtime — the skipped stamp
151
+ above — which its declaration does not admit. The root ESM entry is unaffected,
152
+ because there the export is a module-level binding the loader never stamps. A
153
+ flavor's own `.d.cts` literal is not exposed either: that loader publishes its
154
+ `Symbol.dispose` implementation with `Object.defineProperty` before it stamps,
155
+ and `Object.defineProperty` throws on a non-extensible object, so a sealed or
156
+ frozen addon fails that load well before the stamp is reached.
157
+
43
158
  The root package exposes deferred workerd and Wasm entries. In a Workers
44
159
  project built by Wrangler:
45
160
 
@@ -99,9 +214,13 @@ fresh singleton after cleanup rather than exports that are being destroyed. If
99
214
  the first singleton is still initializing, cleanup waits for that initialization
100
215
  to settle before destroying its context.
101
216
  `createInstance()` creates an independent instance and returns
102
- `{ exports, dispose }`; call and await the returned `dispose()` when that
103
- instance is no longer needed. It consistently returns a promise, including
104
- when emnapi cleanup completes synchronously. Independent instances are not
217
+ `{ exports, memory, memoryBytes, disposed, dispose }`; call and await the
218
+ returned `dispose()` when that instance is no longer needed. It consistently
219
+ returns a promise, including when emnapi cleanup completes synchronously.
220
+ `memoryBytes` is that instance's current linear-memory size and reads `0` once
221
+ disposal has completed — it is declared address space, not a host's
222
+ committed-memory metric, so compare it against platform telemetry rather than
223
+ treating it as a quota. Independent instances are not
105
224
  automatically disposed at
106
225
  `beforeExit`, while initializing or after success, so retained exports remain
107
226
  usable if a listener schedules more work; their cleanup ownership stays
@@ -113,7 +232,60 @@ API without a broken import of the declaration-less root package. If
113
232
  initialization fails and immediate context rollback also fails, the loader
114
233
  retains that cleanup ownership so a later `beforeExit` pass can retry it.
115
234
  `dispose()` still attempts those retained rollbacks when singleton cleanup
116
- fails, while preserving the singleton error as the primary rejection.
235
+ fails, while preserving the singleton error as the primary rejection. The
236
+ deferred loader also exports `__napiBindingTarget` (see "Identifying the loaded
237
+ artifact"), typed as its exact flavor.
238
+
239
+ A second argument to `createInstance()` selects that instance's linear memory:
240
+
241
+ ```js
242
+ const instance = await createInstance(wasmModule, {
243
+ initialMemoryPages: 1024,
244
+ maximumMemoryPages: 65536,
245
+ })
246
+ ```
247
+
248
+ or hand it one you allocated yourself:
249
+
250
+ ```js
251
+ const memory = new WebAssembly.Memory({ initial: 1024, maximum: 65536 })
252
+ const instance = await createInstance(wasmModule, { memory })
253
+ ```
254
+
255
+ `memory` and the page options are mutually exclusive. The generated
256
+ declaration models that as a union — `WasiInstanceOptions` is
257
+ `WasiCallerMemoryOptions | WasiAllocatedMemoryOptions`, each form declaring the
258
+ other form's properties as `never` — so TypeScript rejects an option bag mixing
259
+ the two before the loader throws. Page counts go straight to
260
+ `new WebAssembly.Memory`, so the engine's own bounds and messages apply. A
261
+ caller-provided `WebAssembly.Memory` must be unshared — this loader has no
262
+ threads, and shared growth does not detach, so external views handed to the
263
+ addon would silently outlive the bytes they describe — and it must come from the
264
+ **loader's own realm**. The layers underneath it (`WASI.setMemory` in
265
+ `@napi-rs/wasm-runtime`, and emnapi) identify a Memory with a realm-local
266
+ `instanceof`, so a genuine Memory built in a `node:vm` context or another frame
267
+ is rejected up front with a `TypeError` rather than failing somewhere inside
268
+ initialization.
269
+
270
+ Every Memory an instance runs on is **single-use**, the one the loader allocates
271
+ for you included: once a validated initialization attempt begins, passing the
272
+ same Memory again throws, including after that attempt fails, after the instance
273
+ is disposed, and when it is the `memory` a previous handle published. A failed
274
+ initialization may already have written into linear memory, so those bytes are
275
+ not a clean slate, and two live instances on one Memory would each overwrite the
276
+ emnapi and WASI state the other is still running on. A rejected option bag — a
277
+ foreign Memory, a shared one, `memory` together with the page options — claims
278
+ nothing, so the same Memory is still usable once the call is corrected. The
279
+ claim is tracked per evaluated loader module; two independently bundled copies
280
+ of the loader in one isolate do not see each other's claims.
281
+
282
+ `WASM_MEMORY` exports the descriptor compiled into the loader — `initialPages`,
283
+ `maximumPages`, `pageBytes`, `initialBytes`, `maximumBytes` — so a caller can
284
+ size its own Memory from it. `getDeferredRuntimeStats()` reports
285
+ `{ createdInstances, liveInstances, declaredInitialMemoryBytes }` for instances
286
+ created by that loader module evaluation, not process-wide; `liveInstances`
287
+ drops when an instance's `dispose()` resolves, so a `dispose()` that throws
288
+ leaves it counted and retryable.
117
289
 
118
290
  `Context.destroy()` is synchronous in emnapi's public contract. The deferred
119
291
  loader also contains nonconforming promise-like results defensively. Keep and
@@ -127,6 +299,30 @@ Replacement `instantiate()` calls wait for the complete public cleanup,
127
299
  including every retained failed-initialization rollback present before cleanup
128
300
  finishes.
129
301
 
302
+ Every generated loader shadows `destroy` on the emnapi context it creates, so
303
+ `napi_prepare_wasm_env_cleanup` runs before the environment stops accepting
304
+ JavaScript calls even when `destroy()` is invoked directly — by an embedder
305
+ holding the context, by a test harness, or by emnapi's `beforeExit` auto-destroy
306
+ on a host where `suppressDestroy()` is unavailable. The shadow is best-effort: a
307
+ context whose `destroy` cannot be read or redefined is used unchanged. It does
308
+ not replace `dispose()`, which additionally yields event-loop turns until
309
+ `napi_wasm_env_cleanup_pending` reports zero; a direct `destroy()` still cannot
310
+ wait for a settlement produced on another thread.
311
+
312
+ The barrier settles the promises it cancels synchronously, so a promise hook
313
+ (`node:v8` `promiseHooks`, or the `async_hooks` hook `AsyncLocalStorage`
314
+ installs) can run while it is still in flight. A `destroy()` called from such a
315
+ hook is a no-op — the frame that started the barrier destroys as soon as it
316
+ returns, and `Context.destroy()` returns `void`, so nothing observable is lost.
317
+
318
+ `dispose()` is the one frame that does not destroy the moment the barrier
319
+ returns: it yields for the settlement drain first. So a `dispose()` called from
320
+ such a hook joins the disposal already running instead of starting a second
321
+ one — every loader publishes its disposal promise before the barrier runs. A
322
+ second frame would otherwise reach the context destroyer while the first is
323
+ still parked in its drain, and the no-op above would be recorded there as a
324
+ completed destroy, leaving the context retained with its cleanup hooks unrun.
325
+
130
326
  The eager CommonJS WASI loader keeps its emnapi context alive for the process
131
327
  lifetime. Node.js can emit `beforeExit` repeatedly when a listener schedules
132
328
  more work, and cached eager exports must remain usable after every such cycle.
@@ -175,6 +371,88 @@ override the default in either direction:
175
371
  }
176
372
  ```
177
373
 
374
+ ## Shared async runtime hosts
375
+
376
+ `CurrentThread` is the only async-runtime flavor on WebAssembly, for both
377
+ `wasm32-wasip1` and `wasm32-wasip1-threads`. An addon built with the
378
+ `napi-async-runtime` crate therefore makes no progress until a JavaScript task
379
+ host publishes its runnable turns, and its timers never fire until a timer host
380
+ relays them. Set `napi.wasm.asyncRuntime` and the generated loaders install
381
+ both for you:
382
+
383
+ ```json
384
+ {
385
+ "napi": {
386
+ "wasm": {
387
+ "asyncRuntime": true
388
+ }
389
+ }
390
+ }
391
+ ```
392
+
393
+ This affects WASI output only. Native `.node` bindings run the `MultiThread`
394
+ flavor on real threads and need no JavaScript host.
395
+
396
+ With the flag on:
397
+
398
+ - The Node CommonJS and browser loaders call
399
+ `installCurrentThreadHosts(exports)` from `@napi-rs/async-runtime` right after
400
+ instantiation, and unregister both hosts before the emnapi context is
401
+ destroyed — on `dispose()`, on the initialization rollback, and at process
402
+ `exit`.
403
+ - The deferred `./workerd` loader registers one task host and one timer host
404
+ **per instance** (`registerWorkerdCurrentThreadTaskHost` /
405
+ `registerWorkerdTimerHost`) and disposes them with that instance, so
406
+ independent instances never share or cancel each other's registrations. It
407
+ imports them from the `@napi-rs/async-runtime/workerd` subpath rather than the
408
+ package root: the root entry also pulls in the Node-lane relay
409
+ (the realm-global installation registry and its timer-handle bookkeeping),
410
+ which a worker bundle never runs and which a CommonJS entry cannot be
411
+ tree-shaken out of. Both entries are equally isolate-safe — neither touches a
412
+ `node:` builtin or `process` — so the subpath is purely about bundle size.
413
+ - The generated `<packageName>-wasm32-*` packages declare
414
+ `@napi-rs/async-runtime` as a dependency. Add it to your own
415
+ `devDependencies` so local `napi build` output can load.
416
+
417
+ `napi pre-publish` requires that dependency in every WASI package whose loaders
418
+ import it, so a project that turned the flag on after scaffolding must rerun
419
+ `napi create-npm-dirs` — the root `devDependencies` entry hides a stale
420
+ manifest locally, but consumers of the published package would fail at load.
421
+
422
+ Detection is done at runtime against the instantiated module:
423
+ `@napi-rs/async-runtime` reads the seven host exports off the binding, checks
424
+ the task-host contract version (`4`), validates the reservation identity and
425
+ the liveness probe, and rolls back every registration it created if any step
426
+ fails. A binding that does not actually expose the contract therefore fails at
427
+ load with `ERR_NAPI_ASYNC_RUNTIME_BINDING_MISMATCH` rather than hanging later.
428
+
429
+ `napi build` cross-checks the flag against the same seven names up front, but it
430
+ reads them from the type definitions, so the check needs the `type-def` feature
431
+ of `napi-derive`. Without it there is no export list to check against: the build
432
+ prints a warning and leaves the verdict to the runtime detection above.
433
+
434
+ The bootstrap runs after instantiation, so it runs after `#[module_init]` and
435
+ any other Rust code that executes during module registration: the host
436
+ registration functions are exports of the binding itself and do not exist
437
+ before registration completes. Code that runs during registration must not
438
+ create timers (`sleep_until` fails loud at creation when no timer host is
439
+ registered) and cannot expect task progress until the loader has returned.
440
+ Register the runtime backend there; create tasks and timers from exports that
441
+ run later.
442
+
443
+ The flag itself is still needed because the loaders `import` the package with a
444
+ bare specifier: bundlers resolve static imports at build time, so an optional
445
+ one is not expressible. Leave it unset and every generated loader is byte-for-
446
+ byte what earlier CLI versions produced.
447
+
448
+ Only the loader's own thread is bootstrapped. WASI worker threads
449
+ (`wasi-worker.mjs`, `wasi-worker-browser.mjs`) instantiate the module with
450
+ `childThread: true` and do not register a host.
451
+
452
+ `napi build` cross-checks the flag against the addon's real export list: it
453
+ fails when the flag is set but the host exports are missing, and warns when the
454
+ exports are present but the flag is not set.
455
+
178
456
  `napi.wasm.initialMemory` is measured in 64 KiB WebAssembly pages. The regular
179
457
  Node and browser loaders retain the historical 4,000-page (250 MiB) default.
180
458
  The deferred `./workerd` loader defaults to 1,024 pages (64 MiB), leaving
@@ -182,6 +460,42 @@ headroom under workerd's 128 MiB isolate limit. An explicit
182
460
  `napi.wasm.initialMemory` value applies to every loader, so keep it within the
183
461
  target isolate's limit after measuring the addon's actual requirements.
184
462
 
463
+ `napi.wasm.threadlessInitialMemory` overrides that value for the threadless
464
+ (`wasm32-wasip1`) loaders only — the Node CJS loader, the browser loader, and
465
+ the deferred `./workerd` loader. The two flavors want different floors:
466
+
467
+ - The threaded loader allocates one `shared: true` memory and hands it to every
468
+ wasi-threads worker, so every worker stack and every thread's allocations are
469
+ carved out of it and growing it is a cross-thread event. Browser builds
470
+ pre-create `asyncWorkPoolSize + hardwareConcurrency` workers, so this is
471
+ sized for the whole pool up front.
472
+ - The threadless loader has one thread and a plain growable `ArrayBuffer`. Its
473
+ only hard floor is the module's own `env.memory` minimum — the link-time
474
+ `-zstack-size` plus static data, both fixed by `napi-build` — and it grows on
475
+ demand. That is what lets the same addon fit a host with a hard isolate cap.
476
+
477
+ ```json
478
+ {
479
+ "napi": {
480
+ "wasm": {
481
+ "initialMemory": 16384,
482
+ "threadlessInitialMemory": 1027,
483
+ "maximumMemory": 65536
484
+ }
485
+ }
486
+ }
487
+ ```
488
+
489
+ Without the key the threadless loaders keep following `napi.wasm.initialMemory`,
490
+ so nothing changes for an existing project.
491
+
492
+ Both values must be integers in `1..=65536` pages — memory32 tops out at 4 GiB,
493
+ which is also the `--max-memory` the WASI link uses — and neither may exceed
494
+ `napi.wasm.maximumMemory`. `napi build` fails the WASI target otherwise. An
495
+ `initial` _below_ the module's own `env.memory` minimum is **not** caught by the
496
+ CLI: it surfaces as a `LinkError` at instantiation, so re-measure whenever the
497
+ addon's static data or stack size grows.
498
+
185
499
  Threaded browser loaders always pre-create a pool of wasi-threads workers at
186
500
  module initialization, sized as `asyncWorkPoolSize + hardwareConcurrency`
187
501
  (logical cores, floored at 2, with a fallback for privacy-fuzzed values),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@napi-rs/cli",
3
- "version": "3.9.0",
3
+ "version": "3.10.0",
4
4
  "description": "Cli tools for napi-rs",
5
5
  "author": "LongYinan <lynweklm@gmail.com>",
6
6
  "homepage": "https://napi.rs/",
@@ -68,8 +68,8 @@
68
68
  "clipanion": "^4.0.0-rc.4",
69
69
  "colorette": "^2.0.20",
70
70
  "es-toolkit": "^1.47.0",
71
- "js-yaml": "^4.2.0",
72
- "obug": "^2.1.2",
71
+ "js-yaml": "^5.0.0",
72
+ "obug": "^3.0.0",
73
73
  "semver": "^7.8.2",
74
74
  "typanion": "^3.14.0",
75
75
  "typescript": "^6.0.3"
@@ -87,9 +87,8 @@
87
87
  "emnapi": "^2.0.0-alpha.4",
88
88
  "empathic": "^2.0.1",
89
89
  "env-paths": "^4.0.0",
90
- "oxc-parser": "^0.148.0",
91
- "prettier": "^3.8.3",
92
- "tsdown": "^0.22.2",
90
+ "oxc-parser": "^0.150.0",
91
+ "tsdown": "^0.23.0",
93
92
  "tslib": "^2.8.1"
94
93
  },
95
94
  "peerDependencies": {