@napi-rs/cli 3.8.4 → 3.8.5

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.
@@ -22,16 +22,17 @@ new NapiCli().prePublish({
22
22
 
23
23
  ## Options
24
24
 
25
- | Options | CLI Options | type | required | default | description |
26
- | ------------------- | ------------------------- | ---------------- | -------- | -------------- | ------------------------------------------------------------------------------------------------------------------ |
27
- | | --help,-h | | | | get help |
28
- | cwd | --cwd | string | false | process.cwd() | The working directory of where napi command will be executed in, all other paths options are relative to this path |
29
- | configPath | --config-path,-c | string | false | | Path to `napi` config json file |
30
- | packageJsonPath | --package-json-path | string | false | 'package.json' | Path to `package.json` |
31
- | npmDir | --npm-dir,-p | string | false | 'npm' | Path to the folder where the npm packages put |
32
- | tagStyle | --tag-style,--tagstyle,-t | 'npm' \| 'lerna' | false | 'lerna' | git tag style, `npm` or `lerna` |
33
- | ghRelease | --gh-release | boolean | false | true | Whether create GitHub release |
34
- | ghReleaseName | --gh-release-name | string | false | | GitHub release name |
35
- | ghReleaseId | --gh-release-id | string | false | | Existing GitHub release id |
36
- | skipOptionalPublish | --skip-optional-publish | boolean | false | false | Whether skip optionalDependencies packages publish |
37
- | dryRun | --dry-run | boolean | false | false | Dry run without touching file system |
25
+ | Options | CLI Options | type | required | default | description |
26
+ | ------------------- | ------------------------- | ------------------------------------------- | -------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
27
+ | | --help,-h | | | | get help |
28
+ | cwd | --cwd | string | false | process.cwd() | The working directory of where napi command will be executed in, all other paths options are relative to this path |
29
+ | configPath | --config-path,-c | string | false | | Path to `napi` config json file |
30
+ | packageJsonPath | --package-json-path | string | false | 'package.json' | Path to `package.json` |
31
+ | npmDir | --npm-dir,-p | string | false | 'npm' | Path to the folder where the npm packages put |
32
+ | tagStyle | --tag-style,--tagstyle,-t | 'npm' \| 'lerna' | false | 'lerna' | git tag style, `npm` or `lerna` |
33
+ | ghRelease | --gh-release | boolean | false | true | Whether create GitHub release |
34
+ | ghReleaseName | --gh-release-name | string | false | | GitHub release name |
35
+ | ghReleaseId | --gh-release-id | string | false | | Existing GitHub release id |
36
+ | skipOptionalPublish | --skip-optional-publish | boolean | false | false | Whether skip optionalDependencies packages publish |
37
+ | rootPublisher | --root-publisher | 'npm' \| 'pnpm' \| 'yarn' \| 'yarn-classic' | false | | The package manager that publishes the root package. pnpm and yarn berry replace `exports` with `publishConfig.exports` while packing, so for them the root package validation targets the publish-effective export map. Defaults to the `napi.rootPublisher` config field, and validates both export maps when neither is set |
38
+ | dryRun | --dry-run | boolean | false | false | Dry run without touching file system |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@napi-rs/cli",
3
- "version": "3.8.4",
3
+ "version": "3.8.5",
4
4
  "description": "Cli tools for napi-rs",
5
5
  "author": "LongYinan <lynweklm@gmail.com>",
6
6
  "homepage": "https://napi.rs/",
@@ -1,8 +1,18 @@
1
+ import { mkdtempSync } from 'node:fs'
2
+ import { mkdir, rm, writeFile } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import { dirname, join } from 'node:path'
5
+
1
6
  import test from 'ava'
2
7
 
3
8
  import {
9
+ collectRootPackagePathReferences,
4
10
  parseNpmPackFiles,
5
11
  resolveRootOptionalDependencies,
12
+ resolveRootPublisher,
13
+ rootPublisherRewritesRootExports,
14
+ sniffRewritingPublisherFromUserAgent,
15
+ validateRootFacadePacklist,
6
16
  } from '../pre-publish.js'
7
17
  import { parseTriple } from '../../utils/index.js'
8
18
 
@@ -139,3 +149,412 @@ test('rejects an unexpected npm pack JSON result', (t) => {
139
149
  message: 'npm pack returned an unexpected JSON result',
140
150
  })
141
151
  })
152
+
153
+ // pnpm and yarn berry wholesale-replace `exports` with `publishConfig.exports`
154
+ // in the published manifest, so under those publishers the root facade
155
+ // validation must target the effective map: local-only conditions like
156
+ // `dev: ./src/*.ts` never reach the registry and must not fail the packlist
157
+ // check. npm and Yarn Classic keep the raw `exports` untouched (publishConfig
158
+ // is publish-time configuration only), so both maps must stay valid there.
159
+ //
160
+ // `prePublish` does not publish the root package, so the publisher is declared
161
+ // explicitly (`--root-publisher` / `napi.rootPublisher`). `npm_config_user_agent`
162
+ // names the process that invoked this command, which may differ from the root
163
+ // publisher, so it never selects a branch — it only enriches the failure.
164
+ const PNPM_USER_AGENT = 'pnpm/11.9.0 npm/? node/v24.19.0 darwin arm64'
165
+ const YARN_USER_AGENT = 'yarn/4.6.0 npm/? node/v24.19.0 darwin arm64'
166
+ const YARN_CLASSIC_USER_AGENT = 'yarn/1.22.22 npm/? node/v24.19.0 darwin arm64'
167
+ const NPM_USER_AGENT = 'npm/11.6.2 node/v24.19.0 darwin arm64 workspaces/false'
168
+ // A truncated version token must not be read as a major version: an agent we
169
+ // cannot parse names no package manager at all.
170
+ const MALFORMED_YARN_USER_AGENT = 'yarn/2garbage npm/? node/v24.19.0'
171
+ const MALFORMED_PNPM_USER_AGENT = 'pnpm/2garbage npm/? node/v24.19.0'
172
+ const PNPM_HINT =
173
+ ' This ran under pnpm, which replaces "exports" with "publishConfig.exports" when it packs: if pnpm also publishes the root package, set napi.rootPublisher to "pnpm" (or pass --root-publisher pnpm) to validate the publish-effective export map instead.'
174
+
175
+ function withNpmUserAgent<T>(agent: string | undefined, fn: () => T): T {
176
+ const previous = process.env.npm_config_user_agent
177
+ if (agent === undefined) {
178
+ delete process.env.npm_config_user_agent
179
+ } else {
180
+ process.env.npm_config_user_agent = agent
181
+ }
182
+ try {
183
+ return fn()
184
+ } finally {
185
+ if (previous === undefined) {
186
+ delete process.env.npm_config_user_agent
187
+ } else {
188
+ process.env.npm_config_user_agent = previous
189
+ }
190
+ }
191
+ }
192
+
193
+ const EFFECTIVE_EXPORTS_MANIFEST = {
194
+ name: PACKAGE_NAME,
195
+ version: VERSION,
196
+ main: './dist/index.cjs',
197
+ types: './dist/index.d.ts',
198
+ files: ['dist'],
199
+ exports: {
200
+ '.': {
201
+ dev: './src/index.ts',
202
+ types: './dist/index.d.ts',
203
+ default: './dist/index.cjs',
204
+ },
205
+ './helper': {
206
+ dev: './src/helper.ts',
207
+ default: './dist/helper.cjs',
208
+ },
209
+ },
210
+ publishConfig: {
211
+ exports: {
212
+ '.': {
213
+ types: './dist/index.d.ts',
214
+ default: './dist/index.cjs',
215
+ },
216
+ './helper': './dist/helper.cjs',
217
+ },
218
+ },
219
+ }
220
+
221
+ async function createStagedRoot(
222
+ manifest: Record<string, unknown>,
223
+ stagedFiles: string[],
224
+ ) {
225
+ const rootDir = mkdtempSync(join(tmpdir(), 'napi-pre-publish-spec-'))
226
+ await writeFile(join(rootDir, 'package.json'), JSON.stringify(manifest))
227
+ for (const file of stagedFiles) {
228
+ await mkdir(join(rootDir, dirname(file)), { recursive: true })
229
+ await writeFile(join(rootDir, file), '// content\n')
230
+ }
231
+ return rootDir
232
+ }
233
+
234
+ test('only declared pnpm and yarn publishers rewrite the root exports', (t) => {
235
+ t.true(rootPublisherRewritesRootExports('pnpm'))
236
+ t.true(rootPublisherRewritesRootExports('yarn'))
237
+ t.false(rootPublisherRewritesRootExports('npm'))
238
+ t.false(rootPublisherRewritesRootExports('yarn-classic'))
239
+ t.false(rootPublisherRewritesRootExports(undefined))
240
+ })
241
+
242
+ test('resolves the root publisher from the CLI option over the config field', (t) => {
243
+ t.is(resolveRootPublisher(undefined, undefined), undefined)
244
+ t.is(resolveRootPublisher(undefined, 'pnpm'), 'pnpm')
245
+ t.is(resolveRootPublisher('npm', 'pnpm'), 'npm')
246
+ t.is(resolveRootPublisher('yarn-classic', undefined), 'yarn-classic')
247
+ t.throws(() => resolveRootPublisher('pnpm@10', undefined), {
248
+ message:
249
+ 'Unknown --root-publisher value "pnpm@10". Expected one of: npm, pnpm, yarn, yarn-classic.',
250
+ })
251
+ t.throws(() => resolveRootPublisher(undefined, 'berry'), {
252
+ message:
253
+ 'Unknown napi.rootPublisher value "berry". Expected one of: npm, pnpm, yarn, yarn-classic.',
254
+ })
255
+ })
256
+
257
+ test('sniffs rewriting package managers from the user agent', (t) => {
258
+ t.is(sniffRewritingPublisherFromUserAgent(PNPM_USER_AGENT), 'pnpm')
259
+ t.is(sniffRewritingPublisherFromUserAgent(YARN_USER_AGENT), 'yarn')
260
+ t.is(
261
+ sniffRewritingPublisherFromUserAgent('yarn/2.4.3 npm/? node/v24.19.0'),
262
+ 'yarn',
263
+ )
264
+ t.is(sniffRewritingPublisherFromUserAgent(YARN_CLASSIC_USER_AGENT), undefined)
265
+ t.is(
266
+ sniffRewritingPublisherFromUserAgent('yarn/ npm/? node/v24.19.0'),
267
+ undefined,
268
+ )
269
+ t.is(sniffRewritingPublisherFromUserAgent(NPM_USER_AGENT), undefined)
270
+ t.is(
271
+ sniffRewritingPublisherFromUserAgent('bun/1.2.0 node/v24.19.0'),
272
+ undefined,
273
+ )
274
+ // An explicit `undefined` argument falls back to the ambient environment,
275
+ // so pin the environment to "absent" for the no-agent case.
276
+ withNpmUserAgent(undefined, () => {
277
+ t.is(sniffRewritingPublisherFromUserAgent(), undefined)
278
+ })
279
+ })
280
+
281
+ test('requires a complete version token to sniff a rewriting publisher', (t) => {
282
+ // `yarn/2garbage` is not yarn 2 and `pnpm/x` is not pnpm: matching only the
283
+ // numeric prefix (or the bare name) would name the wrong manager in the hint.
284
+ t.is(
285
+ sniffRewritingPublisherFromUserAgent(MALFORMED_YARN_USER_AGENT),
286
+ undefined,
287
+ )
288
+ t.is(sniffRewritingPublisherFromUserAgent('yarn/2garbage'), undefined)
289
+ t.is(sniffRewritingPublisherFromUserAgent('yarn/4.6.0.1 npm/?'), undefined)
290
+ t.is(
291
+ sniffRewritingPublisherFromUserAgent(MALFORMED_PNPM_USER_AGENT),
292
+ undefined,
293
+ )
294
+ t.is(
295
+ sniffRewritingPublisherFromUserAgent('pnpm/x npm/? node/v24.19.0'),
296
+ undefined,
297
+ )
298
+ t.is(sniffRewritingPublisherFromUserAgent('pnpm/'), undefined)
299
+ // Well-formed prerelease and build metadata tails still parse.
300
+ t.is(
301
+ sniffRewritingPublisherFromUserAgent('yarn/4.10.0-git.20250101 npm/?'),
302
+ 'yarn',
303
+ )
304
+ t.is(
305
+ sniffRewritingPublisherFromUserAgent('pnpm/10.15.1+sha512.ab npm/?'),
306
+ 'pnpm',
307
+ )
308
+ })
309
+
310
+ test('collects publishConfig.exports only for a declared rewriting publisher', (t) => {
311
+ const effectiveOnly = ['dist/helper.cjs', 'dist/index.cjs', 'dist/index.d.ts']
312
+ for (const rootPublisher of ['pnpm', 'yarn'] as const) {
313
+ t.deepEqual(
314
+ collectRootPackagePathReferences(
315
+ EFFECTIVE_EXPORTS_MANIFEST,
316
+ rootPublisher,
317
+ ).sort(),
318
+ effectiveOnly,
319
+ )
320
+ }
321
+ })
322
+
323
+ test('collects the union of exports and publishConfig.exports for every other publisher', (t) => {
324
+ const union = [
325
+ 'dist/helper.cjs',
326
+ 'dist/index.cjs',
327
+ 'dist/index.d.ts',
328
+ 'src/helper.ts',
329
+ 'src/index.ts',
330
+ ]
331
+ for (const rootPublisher of ['npm', 'yarn-classic', undefined] as const) {
332
+ t.deepEqual(
333
+ collectRootPackagePathReferences(
334
+ EFFECTIVE_EXPORTS_MANIFEST,
335
+ rootPublisher,
336
+ ).sort(),
337
+ union,
338
+ )
339
+ }
340
+ })
341
+
342
+ test('never lets the ambient user agent widen or narrow the validated map', (t) => {
343
+ const union = [
344
+ 'dist/helper.cjs',
345
+ 'dist/index.cjs',
346
+ 'dist/index.d.ts',
347
+ 'src/helper.ts',
348
+ 'src/index.ts',
349
+ ]
350
+ const effectiveOnly = ['dist/helper.cjs', 'dist/index.cjs', 'dist/index.d.ts']
351
+ for (const agent of [
352
+ PNPM_USER_AGENT,
353
+ YARN_USER_AGENT,
354
+ NPM_USER_AGENT,
355
+ YARN_CLASSIC_USER_AGENT,
356
+ MALFORMED_YARN_USER_AGENT,
357
+ MALFORMED_PNPM_USER_AGENT,
358
+ undefined,
359
+ ]) {
360
+ withNpmUserAgent(agent, () => {
361
+ // No declared publisher: always the conservative union, even under pnpm.
362
+ t.deepEqual(
363
+ collectRootPackagePathReferences(EFFECTIVE_EXPORTS_MANIFEST).sort(),
364
+ union,
365
+ )
366
+ // A declared publisher wins over whatever the agent claims.
367
+ t.deepEqual(
368
+ collectRootPackagePathReferences(
369
+ EFFECTIVE_EXPORTS_MANIFEST,
370
+ 'npm',
371
+ ).sort(),
372
+ union,
373
+ )
374
+ t.deepEqual(
375
+ collectRootPackagePathReferences(
376
+ EFFECTIVE_EXPORTS_MANIFEST,
377
+ 'pnpm',
378
+ ).sort(),
379
+ effectiveOnly,
380
+ )
381
+ })
382
+ }
383
+ })
384
+
385
+ test('falls back to raw exports without publishConfig.exports', (t) => {
386
+ const manifest = {
387
+ ...EFFECTIVE_EXPORTS_MANIFEST,
388
+ publishConfig: { access: 'public' },
389
+ }
390
+ const rawRefs = [
391
+ 'dist/helper.cjs',
392
+ 'dist/index.cjs',
393
+ 'dist/index.d.ts',
394
+ 'src/helper.ts',
395
+ 'src/index.ts',
396
+ ]
397
+ for (const rootPublisher of ['pnpm', 'npm', undefined] as const) {
398
+ t.deepEqual(
399
+ collectRootPackagePathReferences(manifest, rootPublisher).sort(),
400
+ rawRefs,
401
+ )
402
+ }
403
+ })
404
+
405
+ test('accepts dev-only export conditions replaced by publishConfig.exports under a declared pnpm publisher', async (t) => {
406
+ // The raw `exports` references src/*.ts files that are neither on disk nor
407
+ // in the packlist; under a rewriting publisher only the publish-effective
408
+ // references must survive pack.
409
+ const rootDir = await createStagedRoot(EFFECTIVE_EXPORTS_MANIFEST, [
410
+ 'dist/index.cjs',
411
+ 'dist/index.d.ts',
412
+ 'dist/helper.cjs',
413
+ ])
414
+ t.teardown(() => rm(rootDir, { recursive: true, force: true }))
415
+
416
+ t.notThrows(() =>
417
+ validateRootFacadePacklist(
418
+ rootDir,
419
+ collectRootPackagePathReferences(EFFECTIVE_EXPORTS_MANIFEST, 'pnpm'),
420
+ 'pnpm',
421
+ ),
422
+ )
423
+ })
424
+
425
+ test('rejects dev-only raw export references when no root publisher is declared', async (t) => {
426
+ // Nobody declared a rewriting publisher, so the raw `exports` may ship: the
427
+ // dev sources exist on disk but `files: ['dist']` keeps them out of the
428
+ // packlist. Running under npm adds no hint.
429
+ const rootDir = await createStagedRoot(EFFECTIVE_EXPORTS_MANIFEST, [
430
+ 'dist/index.cjs',
431
+ 'dist/index.d.ts',
432
+ 'dist/helper.cjs',
433
+ 'src/index.ts',
434
+ 'src/helper.ts',
435
+ ])
436
+ t.teardown(() => rm(rootDir, { recursive: true, force: true }))
437
+
438
+ for (const agent of [NPM_USER_AGENT, YARN_CLASSIC_USER_AGENT, undefined]) {
439
+ t.throws(
440
+ () =>
441
+ withNpmUserAgent(agent, () =>
442
+ validateRootFacadePacklist(
443
+ rootDir,
444
+ collectRootPackagePathReferences(EFFECTIVE_EXPORTS_MANIFEST),
445
+ ),
446
+ ),
447
+ {
448
+ message:
449
+ 'The threadless WASI root package references paths omitted by npm pack: src/index.ts, src/helper.ts. Add them to package.json "files" or remove the matching .npmignore rules.',
450
+ },
451
+ )
452
+ }
453
+ // Declaring npm explicitly keeps the same failure, still without a hint.
454
+ t.throws(
455
+ () =>
456
+ withNpmUserAgent(PNPM_USER_AGENT, () =>
457
+ validateRootFacadePacklist(
458
+ rootDir,
459
+ collectRootPackagePathReferences(EFFECTIVE_EXPORTS_MANIFEST, 'npm'),
460
+ 'npm',
461
+ ),
462
+ ),
463
+ {
464
+ message:
465
+ 'The threadless WASI root package references paths omitted by npm pack: src/index.ts, src/helper.ts. Add them to package.json "files" or remove the matching .npmignore rules.',
466
+ },
467
+ )
468
+ })
469
+
470
+ test('points an undeclared publisher at the option when a rewriting manager is running', async (t) => {
471
+ // The union branch failed while pnpm invoked us: pnpm may well be the root
472
+ // publisher, so name the option instead of leaving the user to guess.
473
+ const rootDir = await createStagedRoot(EFFECTIVE_EXPORTS_MANIFEST, [
474
+ 'dist/index.cjs',
475
+ 'dist/index.d.ts',
476
+ 'dist/helper.cjs',
477
+ 'src/index.ts',
478
+ 'src/helper.ts',
479
+ ])
480
+ t.teardown(() => rm(rootDir, { recursive: true, force: true }))
481
+
482
+ t.throws(
483
+ () =>
484
+ withNpmUserAgent(PNPM_USER_AGENT, () =>
485
+ validateRootFacadePacklist(
486
+ rootDir,
487
+ collectRootPackagePathReferences(EFFECTIVE_EXPORTS_MANIFEST),
488
+ ),
489
+ ),
490
+ {
491
+ message: `The threadless WASI root package references paths omitted by npm pack: src/index.ts, src/helper.ts. Add them to package.json "files" or remove the matching .npmignore rules.${PNPM_HINT}`,
492
+ },
493
+ )
494
+ // Yarn berry gets its own name; a malformed agent gets no hint at all.
495
+ const yarnError = t.throws(() =>
496
+ withNpmUserAgent(YARN_USER_AGENT, () =>
497
+ validateRootFacadePacklist(
498
+ rootDir,
499
+ collectRootPackagePathReferences(EFFECTIVE_EXPORTS_MANIFEST),
500
+ ),
501
+ ),
502
+ )
503
+ t.true(yarnError!.message.includes('set napi.rootPublisher to "yarn"'))
504
+ const malformedError = t.throws(() =>
505
+ withNpmUserAgent(MALFORMED_PNPM_USER_AGENT, () =>
506
+ validateRootFacadePacklist(
507
+ rootDir,
508
+ collectRootPackagePathReferences(EFFECTIVE_EXPORTS_MANIFEST),
509
+ ),
510
+ ),
511
+ )
512
+ t.false(malformedError!.message.includes('napi.rootPublisher'))
513
+ })
514
+
515
+ test('still rejects publishConfig.exports references omitted by npm pack', async (t) => {
516
+ const manifest = {
517
+ ...EFFECTIVE_EXPORTS_MANIFEST,
518
+ // Restrict the packlist so dist/helper.cjs exists on disk but is not
519
+ // packed: the effective exports validation must keep failing under both
520
+ // publisher kinds.
521
+ files: ['dist/index.cjs', 'dist/index.d.ts'],
522
+ }
523
+ const rootDir = await createStagedRoot(manifest, [
524
+ 'dist/index.cjs',
525
+ 'dist/index.d.ts',
526
+ 'dist/helper.cjs',
527
+ 'src/index.ts',
528
+ 'src/helper.ts',
529
+ ])
530
+ t.teardown(() => rm(rootDir, { recursive: true, force: true }))
531
+
532
+ t.throws(
533
+ () =>
534
+ withNpmUserAgent(PNPM_USER_AGENT, () =>
535
+ validateRootFacadePacklist(
536
+ rootDir,
537
+ collectRootPackagePathReferences(manifest, 'pnpm'),
538
+ 'pnpm',
539
+ ),
540
+ ),
541
+ {
542
+ message:
543
+ 'The threadless WASI root package references paths omitted by npm pack: dist/helper.cjs. Add them to package.json "files" or remove the matching .npmignore rules.',
544
+ },
545
+ )
546
+ for (const rootPublisher of ['npm', 'yarn-classic'] as const) {
547
+ t.throws(
548
+ () =>
549
+ validateRootFacadePacklist(
550
+ rootDir,
551
+ collectRootPackagePathReferences(manifest, rootPublisher),
552
+ rootPublisher,
553
+ ),
554
+ {
555
+ message:
556
+ 'The threadless WASI root package references paths omitted by npm pack: src/index.ts, src/helper.ts, dist/helper.cjs. Add them to package.json "files" or remove the matching .npmignore rules.',
557
+ },
558
+ )
559
+ }
560
+ })