@napi-rs/cli 3.8.1 → 3.8.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/docs/wasi.md CHANGED
@@ -153,6 +153,28 @@ module runs inside the host process, so `wasm32` or host-OS restrictions would
153
153
  make npm reject a direct install or skip the optional dependency on otherwise
154
154
  supported hosts.
155
155
 
156
+ Because nothing gates the install, the root package does not declare the WASI
157
+ package in `optionalDependencies` when native targets are also configured. npm
158
+ evaluates every `optionalDependencies` entry independently, so a declared WASI
159
+ package is downloaded by every consumer, including the ones that already
160
+ resolved a native package and will never load the `.wasm` binary. The generated
161
+ binding loader picks WASI at require time instead, and environments without a
162
+ native package are expected to install it on demand.
163
+
164
+ When WASI is the only configured target it is the primary artifact rather than a
165
+ fallback, so it is declared by default. Set `napi.wasm.optionalDependency` to
166
+ override the default in either direction:
167
+
168
+ ```json
169
+ {
170
+ "napi": {
171
+ "wasm": {
172
+ "optionalDependency": true
173
+ }
174
+ }
175
+ }
176
+ ```
177
+
156
178
  `napi.wasm.initialMemory` is measured in 64 KiB WebAssembly pages. The regular
157
179
  Node and browser loaders retain the historical 4,000-page (250 MiB) default.
158
180
  The deferred `./workerd` loader defaults to 1,024 pages (64 MiB), leaving
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@napi-rs/cli",
3
- "version": "3.8.1",
3
+ "version": "3.8.2",
4
4
  "description": "Cli tools for napi-rs",
5
5
  "author": "LongYinan <lynweklm@gmail.com>",
6
6
  "homepage": "https://napi.rs/",
@@ -0,0 +1,106 @@
1
+ import test from 'ava'
2
+
3
+ import { resolveRootOptionalDependencies } from '../pre-publish.js'
4
+ import { parseTriple } from '../../utils/index.js'
5
+
6
+ const PACKAGE_NAME = '@scope/pkg'
7
+ const VERSION = '1.2.3'
8
+
9
+ const NATIVE_TARGETS = ['aarch64-apple-darwin', 'x86_64-unknown-linux-gnu'].map(
10
+ parseTriple,
11
+ )
12
+ const WASI_TARGET = parseTriple('wasm32-wasip1-threads')
13
+ const THREADLESS_WASI_TARGET = parseTriple('wasm32-wasip1')
14
+
15
+ const NATIVE_ENTRIES = {
16
+ [`${PACKAGE_NAME}-darwin-arm64`]: VERSION,
17
+ [`${PACKAGE_NAME}-linux-x64-gnu`]: VERSION,
18
+ }
19
+
20
+ function resolve(
21
+ targets: Parameters<typeof resolveRootOptionalDependencies>[0]['targets'],
22
+ overrides: Partial<
23
+ Parameters<typeof resolveRootOptionalDependencies>[0]
24
+ > = {},
25
+ ) {
26
+ return resolveRootOptionalDependencies({
27
+ existing: undefined,
28
+ managedPackageNames: [PACKAGE_NAME],
29
+ packageName: PACKAGE_NAME,
30
+ targets,
31
+ version: VERSION,
32
+ ...overrides,
33
+ })
34
+ }
35
+
36
+ test('omits the WASI package when native targets are configured', (t) => {
37
+ // The WASI binary is a require-time fallback, not something npm should
38
+ // install on hosts that already resolved a native package.
39
+ t.deepEqual(resolve([...NATIVE_TARGETS, WASI_TARGET]), NATIVE_ENTRIES)
40
+ })
41
+
42
+ test('omits the threadless WASI package too', (t) => {
43
+ t.deepEqual(
44
+ resolve([...NATIVE_TARGETS, THREADLESS_WASI_TARGET]),
45
+ NATIVE_ENTRIES,
46
+ )
47
+ })
48
+
49
+ test('declares the WASI package when it is the only target', (t) => {
50
+ // With no native package to fall back from, WASI is the primary artifact.
51
+ t.deepEqual(resolve([WASI_TARGET]), {
52
+ [`${PACKAGE_NAME}-wasm32-wasi`]: VERSION,
53
+ })
54
+ })
55
+
56
+ test('declares every WASI flavor when only WASI targets are configured', (t) => {
57
+ t.deepEqual(resolve([WASI_TARGET, THREADLESS_WASI_TARGET]), {
58
+ [`${PACKAGE_NAME}-wasm32-wasi`]: VERSION,
59
+ [`${PACKAGE_NAME}-wasm32-wasip1`]: VERSION,
60
+ })
61
+ })
62
+
63
+ test('wasm.optionalDependency=true opts back into declaring WASI', (t) => {
64
+ t.deepEqual(
65
+ resolve([...NATIVE_TARGETS, WASI_TARGET], {
66
+ wasm: { optionalDependency: true },
67
+ }),
68
+ {
69
+ ...NATIVE_ENTRIES,
70
+ [`${PACKAGE_NAME}-wasm32-wasi`]: VERSION,
71
+ },
72
+ )
73
+ })
74
+
75
+ test('wasm.optionalDependency=false opts out even for WASI-only builds', (t) => {
76
+ t.deepEqual(
77
+ resolve([WASI_TARGET], { wasm: { optionalDependency: false } }),
78
+ {},
79
+ )
80
+ })
81
+
82
+ test('drops a stale WASI entry left over from a previous release', (t) => {
83
+ // Consumers upgrading from a release that did declare the WASI package must
84
+ // not keep the entry, otherwise the regression survives the fix.
85
+ t.deepEqual(
86
+ resolve([...NATIVE_TARGETS, WASI_TARGET], {
87
+ existing: {
88
+ ...NATIVE_ENTRIES,
89
+ [`${PACKAGE_NAME}-wasm32-wasi`]: '1.2.2',
90
+ },
91
+ }),
92
+ NATIVE_ENTRIES,
93
+ )
94
+ })
95
+
96
+ test('preserves unmanaged optionalDependencies', (t) => {
97
+ t.deepEqual(
98
+ resolve([...NATIVE_TARGETS, WASI_TARGET], {
99
+ existing: { 'unrelated-package': '^1.0.0' },
100
+ }),
101
+ {
102
+ 'unrelated-package': '^1.0.0',
103
+ ...NATIVE_ENTRIES,
104
+ },
105
+ )
106
+ })
@@ -60,6 +60,7 @@ import {
60
60
  type CommonPackageJsonFields,
61
61
  type FileSystemTransactionWrite,
62
62
  type Target,
63
+ type UserNapiConfig,
63
64
  } from '../utils/index.js'
64
65
 
65
66
  const debug = debugFactory('pre-publish')
@@ -227,6 +228,61 @@ async function copyOwnedTemporaryPath(source: string, destination: string) {
227
228
  }
228
229
  }
229
230
 
231
+ function wasiIsOnlyTarget(targets: Target[]) {
232
+ return (
233
+ targets.length > 0 && targets.every((target) => target.platform === 'wasi')
234
+ )
235
+ }
236
+
237
+ /**
238
+ * Build the root package's `optionalDependencies` map.
239
+ *
240
+ * A WASI package is a fallback for hosts that cannot load a `.node` binary, and
241
+ * the generated binding loader selects it at require time rather than npm
242
+ * selecting it at install time. Declaring it as an `optionalDependency`
243
+ * alongside the native packages cannot express "install this only when nothing
244
+ * else matched": npm evaluates every entry independently, so every consumer
245
+ * downloads a `.wasm` binary they will never load.
246
+ *
247
+ * It is therefore only declared when WASI is the only configured target, which
248
+ * makes it the primary artifact rather than a fallback.
249
+ * `napi.wasm.optionalDependency` overrides the default in both directions.
250
+ *
251
+ * See https://github.com/rolldown/rolldown/issues/10556
252
+ */
253
+ export function resolveRootOptionalDependencies({
254
+ existing,
255
+ managedPackageNames,
256
+ packageName,
257
+ targets,
258
+ version,
259
+ wasm,
260
+ }: {
261
+ existing: unknown
262
+ managedPackageNames: Iterable<string>
263
+ packageName: string
264
+ targets: Target[]
265
+ version: string
266
+ wasm?: UserNapiConfig['wasm']
267
+ }): Record<string, unknown> {
268
+ const optionalDependencies: Record<string, unknown> = {
269
+ ...asRecord(existing),
270
+ }
271
+ for (const managedPackageName of managedPackageNames) {
272
+ for (const suffix of MANAGED_OPTIONAL_DEPENDENCY_SUFFIXES) {
273
+ delete optionalDependencies[`${managedPackageName}-${suffix}`]
274
+ }
275
+ }
276
+ const declareWasi = wasm?.optionalDependency ?? wasiIsOnlyTarget(targets)
277
+ for (const target of targets) {
278
+ if (target.platform === 'wasi' && !declareWasi) {
279
+ continue
280
+ }
281
+ optionalDependencies[`${packageName}-${target.platformArchABI}`] = version
282
+ }
283
+ return optionalDependencies
284
+ }
285
+
230
286
  export async function prePublish(userOptions: PrePublishOptions) {
231
287
  debug('Receive pre-publish options:')
232
288
  debug(' %O', userOptions)
@@ -341,9 +397,6 @@ export async function prePublish(userOptions: PrePublishOptions) {
341
397
  rootFacade,
342
398
  )
343
399
  }
344
- const optionalDependencies = {
345
- ...asRecord(packageJson.optionalDependencies),
346
- }
347
400
  const managedPackageNames = new Set([packageName])
348
401
  for (const flavorPackage of rootFacadeReconciliation.managedFlavorPackages) {
349
402
  for (const suffix of MANAGED_OPTIONAL_DEPENDENCY_SUFFIXES) {
@@ -353,22 +406,19 @@ export async function prePublish(userOptions: PrePublishOptions) {
353
406
  }
354
407
  }
355
408
  }
356
- for (const managedPackageName of managedPackageNames) {
357
- for (const suffix of MANAGED_OPTIONAL_DEPENDENCY_SUFFIXES) {
358
- delete optionalDependencies[`${managedPackageName}-${suffix}`]
359
- }
360
- }
361
- for (const target of targets) {
362
- optionalDependencies[`${packageName}-${target.platformArchABI}`] =
363
- packageJson.version
364
- }
365
- const nodeEngine =
366
- targets.length > 0 &&
367
- targets.every((target) => target.platform === 'wasi')
368
- ? restrictWasiNodeEngine(
369
- packageJson.engines?.node ?? MINIMUM_WASI_NODE_VERSION,
370
- )
371
- : undefined
409
+ const optionalDependencies = resolveRootOptionalDependencies({
410
+ existing: packageJson.optionalDependencies,
411
+ managedPackageNames,
412
+ packageName,
413
+ targets,
414
+ version: packageJson.version,
415
+ wasm,
416
+ })
417
+ const nodeEngine = wasiIsOnlyTarget(targets)
418
+ ? restrictWasiNodeEngine(
419
+ packageJson.engines?.node ?? MINIMUM_WASI_NODE_VERSION,
420
+ )
421
+ : undefined
372
422
  const rootReleasePlan: RootReleaseMaterializationPlan = {
373
423
  packageJson: reconciledPackageJson,
374
424
  optionalDependencies,
@@ -72,10 +72,29 @@ export interface UserNapiConfig {
72
72
  */
73
73
  maximumMemory?: number
74
74
 
75
+ /**
76
+ * Whether the generated `<packageName>-wasm32-wasi` package is declared as
77
+ * an `optionalDependency` of the root package.
78
+ *
79
+ * When native targets are configured the WASI package is a fallback for
80
+ * hosts that cannot load a `.node` binary, so declaring it would make every
81
+ * consumer download the `.wasm` binary they will never load. It is
82
+ * therefore omitted by default and is expected to be installed on demand by
83
+ * the environments that need it.
84
+ *
85
+ * When WASI is the only configured target it is the primary artifact and is
86
+ * declared by default.
87
+ *
88
+ * Set this explicitly to override either default.
89
+ *
90
+ * @default true when every configured target is WASI, false otherwise
91
+ */
92
+ optionalDependency?: boolean
93
+
75
94
  /**
76
95
  * Browser wasm binding configuration
77
96
  */
78
- browser: {
97
+ browser?: {
79
98
  /**
80
99
  * Whether to use fs module in browser
81
100
  */