@napi-rs/cli 3.8.4 → 3.8.6

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.
@@ -51,6 +51,7 @@ import {
51
51
  mkdirAsync,
52
52
  restrictWasiNodeEngine,
53
53
  resolvePackageReconciliationPaths,
54
+ RootPublishers,
54
55
  wasiLoaderSuffix,
55
56
  wasiTargetHasThreads,
56
57
  writeFileAtomic,
@@ -59,6 +60,7 @@ import {
59
60
  parseTriple,
60
61
  type CommonPackageJsonFields,
61
62
  type FileSystemTransactionWrite,
63
+ type RootPublisher,
62
64
  type Target,
63
65
  type UserNapiConfig,
64
66
  } from '../utils/index.js'
@@ -312,6 +314,7 @@ export async function prePublish(userOptions: PrePublishOptions) {
312
314
  packageName,
313
315
  binaryName,
314
316
  npmClient,
317
+ rootPublisher: configuredRootPublisher,
315
318
  wasm,
316
319
  } = await readNapiConfig(
317
320
  packageJsonPath,
@@ -319,6 +322,10 @@ export async function prePublish(userOptions: PrePublishOptions) {
319
322
  ? resolve(options.cwd, options.configPath)
320
323
  : undefined,
321
324
  )
325
+ const rootPublisher = resolveRootPublisher(
326
+ options.rootPublisher,
327
+ configuredRootPublisher,
328
+ )
322
329
  const threadlessWasiTarget = targets.find(
323
330
  (target) =>
324
331
  target.platform === 'wasi' && !wasiTargetHasThreads(target),
@@ -425,6 +432,7 @@ export async function prePublish(userOptions: PrePublishOptions) {
425
432
  nodeEngine,
426
433
  facade: rootFacade,
427
434
  staleGeneratedFiles: rootFacadeReconciliation.staleGeneratedFiles,
435
+ rootPublisher,
428
436
  }
429
437
  if (preparedWorkspaceRoot) {
430
438
  const stagedTransactionRoot = join(
@@ -1573,8 +1581,101 @@ function createLegacyDeepImportExports(rootDir: string) {
1573
1581
  return exportsMap
1574
1582
  }
1575
1583
 
1576
- function collectRootPackagePathReferences(
1584
+ // pnpm and yarn berry (>= 2) rewrite the manifest they pack:
1585
+ // `publishConfig.exports` wholesale-replaces `exports` in the published
1586
+ // package.json (pnpm docs: "It is possible to override some fields in the
1587
+ // manifest before the package is packed"; verified empirically with pnpm 11
1588
+ // and yarn 4 `pack`). npm does NOT: its `publishConfig` only carries
1589
+ // publish-time configuration such as tag, registry and access
1590
+ // (https://docs.npmjs.com/cli/v11/configuring-npm/package-json#publishconfig),
1591
+ // and npm 11 packs the raw `exports` untouched. Yarn Classic (1.x) behaves
1592
+ // like npm here — verified empirically with yarn 1.22.22 `pack`, which keeps
1593
+ // the raw `exports` in the tarball manifest.
1594
+ const REWRITING_ROOT_PUBLISHERS = new Set<RootPublisher>(['pnpm', 'yarn'])
1595
+
1596
+ export function rootPublisherRewritesRootExports(
1597
+ rootPublisher: RootPublisher | undefined,
1598
+ ) {
1599
+ return (
1600
+ rootPublisher !== undefined && REWRITING_ROOT_PUBLISHERS.has(rootPublisher)
1601
+ )
1602
+ }
1603
+
1604
+ // `prePublish` publishes only the generated per-platform packages; the root
1605
+ // package is packed and published by whoever called us. Which manifest ships
1606
+ // therefore cannot be observed here, so it must be declared: `--root-publisher`
1607
+ // wins over the `napi.rootPublisher` config field, and when neither is set the
1608
+ // conservative union of both export maps is validated.
1609
+ export function resolveRootPublisher(
1610
+ optionValue: string | undefined,
1611
+ configValue: string | undefined,
1612
+ ): RootPublisher | undefined {
1613
+ const [value, source] =
1614
+ optionValue === undefined
1615
+ ? ([configValue, 'napi.rootPublisher'] as const)
1616
+ : ([optionValue, '--root-publisher'] as const)
1617
+ if (value === undefined) {
1618
+ return undefined
1619
+ }
1620
+ if (!(RootPublishers as readonly string[]).includes(value)) {
1621
+ throw new Error(
1622
+ `Unknown ${source} value "${value}". Expected one of: ${RootPublishers.join(', ')}.`,
1623
+ )
1624
+ }
1625
+ return value as RootPublisher
1626
+ }
1627
+
1628
+ // Package managers identify themselves to lifecycle scripts (e.g.
1629
+ // `prepublishOnly`) via `npm_config_user_agent`. That names the process that
1630
+ // invoked this command, which is not necessarily the root publisher, so it
1631
+ // never selects a validation branch — it only makes the failure actionable by
1632
+ // naming the option to set.
1633
+ //
1634
+ // The patterns require a complete `<name>/<version>` token — a semver-ish
1635
+ // version followed by a token boundary — so that a malformed agent such as
1636
+ // `yarn/2garbage` is not read as "yarn 2" (and `pnpm/x` not as pnpm) by
1637
+ // matching a numeric prefix. The prerelease/build tails are kept as two
1638
+ // separate optional groups on purpose: collapsing them into
1639
+ // `(?:[-+][0-9A-Za-z.-]+)*` makes the `-` ambiguous with the character class
1640
+ // and backtracks exponentially on a non-matching tail.
1641
+ const VERSION_TOKEN_TAIL = String.raw`(?:\.\d+){0,2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?(?=\s|$)`
1642
+ const PNPM_PUBLISHER_PATTERN = new RegExp(
1643
+ String.raw`(?:^|\s)pnpm\/\d+` + VERSION_TOKEN_TAIL,
1644
+ )
1645
+ const YARN_PUBLISHER_PATTERN = new RegExp(
1646
+ String.raw`(?:^|\s)yarn\/(\d+)` + VERSION_TOKEN_TAIL,
1647
+ )
1648
+
1649
+ export function sniffRewritingPublisherFromUserAgent(
1650
+ userAgent: string | undefined = process.env.npm_config_user_agent,
1651
+ ): 'pnpm' | 'yarn' | undefined {
1652
+ if (typeof userAgent !== 'string') {
1653
+ return undefined
1654
+ }
1655
+ if (PNPM_PUBLISHER_PATTERN.test(userAgent)) {
1656
+ return 'pnpm'
1657
+ }
1658
+ const yarnMajor = YARN_PUBLISHER_PATTERN.exec(userAgent)
1659
+ return yarnMajor !== null && Number(yarnMajor[1]) >= 2 ? 'yarn' : undefined
1660
+ }
1661
+
1662
+ export function rootPublisherHint(
1663
+ rootPublisher: RootPublisher | undefined,
1664
+ userAgent?: string,
1665
+ ) {
1666
+ if (rootPublisher !== undefined) {
1667
+ return ''
1668
+ }
1669
+ const sniffed = sniffRewritingPublisherFromUserAgent(userAgent)
1670
+ if (sniffed === undefined) {
1671
+ return ''
1672
+ }
1673
+ return ` This ran under ${sniffed}, which replaces "exports" with "publishConfig.exports" when it packs: if ${sniffed} also publishes the root package, set napi.rootPublisher to "${sniffed}" (or pass --root-publisher ${sniffed}) to validate the publish-effective export map instead.`
1674
+ }
1675
+
1676
+ export function collectRootPackagePathReferences(
1577
1677
  packageJson: CommonPackageJsonFields,
1678
+ rootPublisher?: RootPublisher,
1578
1679
  ) {
1579
1680
  const files = new Set<string>()
1580
1681
  addLegacyPackageFileReference(files, packageJson.main)
@@ -1588,13 +1689,24 @@ function collectRootPackagePathReferences(
1588
1689
  (packageJson as CommonPackageJsonFields & { typings?: unknown }).typings,
1589
1690
  )
1590
1691
  addLegacyPackageFileReference(files, packageJson.browser)
1591
- collectRootExportFileReferences(files, packageJson.exports)
1592
1692
 
1693
+ // Validate the export map(s) that will actually ship. Under a declared
1694
+ // manifest-rewriting publisher (pnpm/yarn), `publishConfig.exports` replaces
1695
+ // the local `exports` in the published manifest, so only the
1696
+ // publish-effective map matters: the local map may reference
1697
+ // development-only sources (e.g. `dev: ./src/*.ts` conditions) that are
1698
+ // never packed. Under npm and Yarn Classic — or when no publisher is
1699
+ // declared — the raw `exports` ships as-is, so validate both maps.
1593
1700
  const publishConfig = asRecord(packageJson.publishConfig)
1594
- if (
1701
+ const hasPublishConfigExports =
1595
1702
  publishConfig &&
1596
1703
  Object.prototype.hasOwnProperty.call(publishConfig, 'exports')
1597
- ) {
1704
+ const rawExportsReplacedOnPublish =
1705
+ hasPublishConfigExports && rootPublisherRewritesRootExports(rootPublisher)
1706
+ if (!rawExportsReplacedOnPublish) {
1707
+ collectRootExportFileReferences(files, packageJson.exports)
1708
+ }
1709
+ if (hasPublishConfigExports) {
1598
1710
  collectRootExportFileReferences(files, publishConfig.exports)
1599
1711
  }
1600
1712
  return [...files]
@@ -1717,9 +1829,10 @@ export function parseNpmPackFiles(output: string) {
1717
1829
  )
1718
1830
  }
1719
1831
 
1720
- function validateRootFacadePacklist(
1832
+ export function validateRootFacadePacklist(
1721
1833
  rootDir: string,
1722
1834
  rootPackageFiles: string[],
1835
+ rootPublisher?: RootPublisher,
1723
1836
  ) {
1724
1837
  const paths = validateRootPackagePaths(rootDir, rootPackageFiles)
1725
1838
  const packedFiles = readNpmPackFiles(rootDir, 'threadless WASI root package')
@@ -1736,7 +1849,7 @@ function validateRootFacadePacklist(
1736
1849
  .map(({ path }) => path)
1737
1850
  if (missingPaths.length > 0) {
1738
1851
  throw new Error(
1739
- `The threadless WASI root package references paths omitted by npm pack: ${missingPaths.join(', ')}. Add them to package.json "files" or remove the matching .npmignore rules.`,
1852
+ `The threadless WASI root package references paths omitted by npm pack: ${missingPaths.join(', ')}. Add them to package.json "files" or remove the matching .npmignore rules.${rootPublisherHint(rootPublisher)}`,
1740
1853
  )
1741
1854
  }
1742
1855
  }
@@ -1747,6 +1860,7 @@ interface RootReleaseMaterializationPlan {
1747
1860
  nodeEngine?: string
1748
1861
  facade?: ThreadlessWasiRootFacade
1749
1862
  staleGeneratedFiles: string[]
1863
+ rootPublisher?: RootPublisher
1750
1864
  }
1751
1865
 
1752
1866
  function pathIsWithin(root: string, path: string) {
@@ -2218,7 +2332,10 @@ async function stageRootReleasePlan(
2218
2332
  await cp(packageJsonPath, join(stagedRootDir, 'package.json'))
2219
2333
  if (plan.facade) {
2220
2334
  const packedFiles = readNpmPackFiles(rootDir, 'root package')
2221
- const referencedFiles = collectRootPackagePathReferences(plan.packageJson)
2335
+ const referencedFiles = collectRootPackagePathReferences(
2336
+ plan.packageJson,
2337
+ plan.rootPublisher,
2338
+ )
2222
2339
  for (const file of new Set([
2223
2340
  ...packedFiles,
2224
2341
  ...referencedFiles,
@@ -2240,10 +2357,17 @@ async function stageRootReleasePlan(
2240
2357
  }
2241
2358
  await materializeRootReleasePlan(stagedRootDir, plan)
2242
2359
  if (plan.facade) {
2243
- validateRootFacadePacklist(stagedRootDir, [
2244
- ...plan.facade.generatedFiles,
2245
- ...collectRootPackagePathReferences(plan.packageJson),
2246
- ])
2360
+ validateRootFacadePacklist(
2361
+ stagedRootDir,
2362
+ [
2363
+ ...plan.facade.generatedFiles,
2364
+ ...collectRootPackagePathReferences(
2365
+ plan.packageJson,
2366
+ plan.rootPublisher,
2367
+ ),
2368
+ ],
2369
+ plan.rootPublisher,
2370
+ )
2247
2371
  }
2248
2372
  }
2249
2373
 
@@ -47,6 +47,14 @@ export abstract class BasePrePublishCommand extends Command {
47
47
  description: 'Whether skip optionalDependencies packages publish',
48
48
  })
49
49
 
50
+ rootPublisher?: 'npm' | 'pnpm' | 'yarn' | 'yarn-classic' = Option.String(
51
+ '--root-publisher',
52
+ {
53
+ description:
54
+ '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',
55
+ },
56
+ )
57
+
50
58
  dryRun = Option.Boolean('--dry-run', false, {
51
59
  description: 'Dry run without touching file system',
52
60
  })
@@ -62,6 +70,7 @@ export abstract class BasePrePublishCommand extends Command {
62
70
  ghReleaseName: this.ghReleaseName,
63
71
  ghReleaseId: this.ghReleaseId,
64
72
  skipOptionalPublish: this.skipOptionalPublish,
73
+ rootPublisher: this.rootPublisher,
65
74
  dryRun: this.dryRun,
66
75
  }
67
76
  }
@@ -119,6 +128,10 @@ export interface PrePublishOptions {
119
128
  * @default false
120
129
  */
121
130
  skipOptionalPublish?: boolean
131
+ /**
132
+ * 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
133
+ */
134
+ rootPublisher?: 'npm' | 'pnpm' | 'yarn' | 'yarn-classic'
122
135
  /**
123
136
  * Dry run without touching file system
124
137
  *
@@ -16,6 +16,17 @@ export type SupportedTestFramework = ValueOfConstArray<
16
16
  typeof SupportedTestFrameworks
17
17
  >
18
18
 
19
+ /**
20
+ * Package managers that may publish the **root** package, distinguished by
21
+ * whether they rewrite the manifest while packing it.
22
+ *
23
+ * `yarn` means yarn berry (>= 2); Yarn Classic (1.x) packs the raw manifest
24
+ * like npm does and is spelled `yarn-classic`.
25
+ */
26
+ export const RootPublishers = ['npm', 'pnpm', 'yarn', 'yarn-classic'] as const
27
+
28
+ export type RootPublisher = ValueOfConstArray<typeof RootPublishers>
29
+
19
30
  export interface UserNapiConfig {
20
31
  /**
21
32
  * Name of the binary to be generated, default to `index`
@@ -37,6 +48,23 @@ export interface UserNapiConfig {
37
48
  */
38
49
  npmClient?: string
39
50
 
51
+ /**
52
+ * The package manager that publishes the **root** package.
53
+ *
54
+ * `napi pre-publish` publishes only the generated per-platform packages; the
55
+ * root package is packed and published by the caller, so this cannot be
56
+ * inferred reliably. pnpm and yarn berry replace `exports` with
57
+ * `publishConfig.exports` while packing, so under those publishers
58
+ * `napi pre-publish` validates only the publish-effective export map and the
59
+ * local `exports` may point at development-only sources. npm and Yarn
60
+ * Classic ship `exports` untouched.
61
+ *
62
+ * Unset — the default — validates both maps, which is correct for every
63
+ * publisher but rejects development-only conditions such as
64
+ * `"dev": "./src/index.ts"`.
65
+ */
66
+ rootPublisher?: RootPublisher
67
+
40
68
  /**
41
69
  * Whether generate const enum for typescript bindings
42
70
  */
@@ -182,7 +210,12 @@ export type NapiConfig = Required<
182
210
  > &
183
211
  Pick<
184
212
  UserNapiConfig,
185
- 'wasm' | 'dtsHeader' | 'dtsHeaderFile' | 'constEnum' | 'runtimeStringEnum'
213
+ | 'wasm'
214
+ | 'dtsHeader'
215
+ | 'dtsHeaderFile'
216
+ | 'constEnum'
217
+ | 'runtimeStringEnum'
218
+ | 'rootPublisher'
186
219
  > & {
187
220
  targets: Target[]
188
221
  packageJson: CommonPackageJsonFields