Skip to main content

anodizer_core/config/
npm.rs

1use std::collections::HashMap;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use super::{StringOrBool, deserialize_string_or_bool_opt};
7
8/// Binary-distribution strategy for an [`NpmConfig`] entry.
9///
10/// `optional-deps` (the default for a Rust release) emits npm's native
11/// platform-resolution layout: one thin per-platform package whose
12/// `os`/`cpu`/`libc` selectors are derived from the built target triples,
13/// plus a metapackage that lists every platform package under
14/// `optionalDependencies` and ships a `bin` shim resolving the installed
15/// one via `require.resolve`. npm installs only the matching platform
16/// package; there is no download and no postinstall script. This is the
17/// pattern leading Rust CLIs ship binaries through npm with (biome,
18/// git-cliff).
19///
20/// `postinstall` emits a single package carrying a `postinstall.js` shim that
21/// downloads + sha256-verifies the OS/arch-matching release archive at
22/// `npm install` time — for registries or policies that disallow per-platform
23/// packages.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
25#[serde(rename_all = "kebab-case")]
26pub enum NpmMode {
27    /// Emit per-platform packages + a metapackage with `optionalDependencies`
28    /// and a `require.resolve` bin shim. npm's native `os`/`cpu`/`libc`
29    /// resolution selects the right prebuilt package — no download, no
30    /// postinstall. Default.
31    #[default]
32    OptionalDeps,
33    /// Emit a single package with a `postinstall.js` shim that downloads and
34    /// sha256-verifies the matching archive at install time.
35    Postinstall,
36}
37
38/// Credential-selection strategy for an [`NpmConfig`] entry.
39///
40/// Controls whether the publisher authenticates with a long-lived registry
41/// token (`NPM_TOKEN` / `cfg.token`) or with GitHub Actions OIDC (npm Trusted
42/// Publishing), evaluated **per published package** — in `optional-deps` mode
43/// that means the metapackage and every per-platform sub-package are decided
44/// independently, so a metapackage with a configured Trusted Publisher can use
45/// OIDC while brand-new sub-packages (which Trusted Publishing cannot create)
46/// fall back to the token.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
48#[serde(rename_all = "kebab-case")]
49pub enum NpmAuthMode {
50    /// Decide per package at publish time (default). The registry is probed for
51    /// each package's existence: a package that already exists prefers OIDC
52    /// (Trusted Publishing) when an OIDC context is present, otherwise the
53    /// token; a brand-new package always uses the token (Trusted Publishing
54    /// cannot create a package that does not yet exist) and hard-errors if no
55    /// token is set. When OIDC is chosen for an existing package and the
56    /// publish fails, `auto` retries that package with the token (if available)
57    /// and emits a loud warning that Trusted Publishing was not exercised.
58    #[default]
59    Auto,
60    /// Always authenticate with the token (`NPM_TOKEN` / `cfg.token`); never
61    /// attempt OIDC. Errors if no token is available. This is anodizer's
62    /// historical behaviour.
63    Token,
64    /// Always authenticate with OIDC (Trusted Publishing); never fall back to
65    /// the token. Errors if the OIDC request env is absent. Use when every
66    /// package in this entry has a configured Trusted Publisher and you want a
67    /// failed exchange to fail the release loudly rather than silently fall
68    /// back to a token.
69    Oidc,
70}
71
72/// NPM package registry publisher configuration.
73///
74/// In the default `optional-deps` mode anodizer emits one thin npm package
75/// per built platform (with `os`/`cpu`/`libc` selectors derived from the
76/// target triple) plus a metapackage whose `optionalDependencies` lists
77/// every platform package; npm's native resolution installs only the one
78/// matching the host. In `postinstall` mode a single package carries a
79/// `postinstall` script that downloads the matching release archive at
80/// `npm install` time. Each `npms[]` entry produces one publish.
81#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
82#[serde(default, deny_unknown_fields)]
83pub struct NpmConfig {
84    /// Unique identifier for selecting this entry from the CLI (`--id=...`).
85    pub id: Option<String>,
86
87    /// Build IDs filter: only include artifacts whose archive `id` is in this list.
88    pub ids: Option<Vec<String>>,
89
90    /// Binary-distribution strategy. `optional-deps` (default) emits npm's
91    /// native per-platform packages; `postinstall` emits a download shim.
92    #[serde(default)]
93    pub mode: NpmMode,
94
95    /// npm scope for the per-platform packages emitted in `optional-deps`
96    /// mode (e.g. `@biomejs`). The per-platform packages are named
97    /// `<scope>/<bin>-<os>-<cpu>[-<libc>]`. Required for `optional-deps`
98    /// mode; ignored in `postinstall` mode.
99    pub scope: Option<String>,
100
101    /// Metapackage name for `optional-deps` mode (e.g. `biome`). This is the
102    /// package users `npm install`; it lists every per-platform package under
103    /// `optionalDependencies` and ships the `bin` shim. Falls back to `name`
104    /// (or the crate name) when unset.
105    pub metapackage: Option<String>,
106
107    /// Command name installed by the metapackage's `bin` map (`optional-deps`
108    /// mode). Falls back to the metapackage basename when unset.
109    pub bin: Option<String>,
110
111    /// In `optional-deps` mode, emit separate per-platform packages for linux
112    /// `musl` vs `glibc` (distinguished by the npm `libc` selector). When
113    /// `false`, a single linux package per cpu is emitted with no `libc`
114    /// selector. Default `true` — musl and glibc binaries are not
115    /// interchangeable, so collapsing them risks installing the wrong one.
116    #[serde(default = "default_libc_aware")]
117    pub libc_aware: bool,
118
119    /// NPM package name (the metapackage / postinstall package). May be scoped
120    /// (`@org/foo`) or unscoped (`foo`). Falls back to the crate name when unset.
121    pub name: Option<String>,
122
123    /// Templated package description. Falls back to the project-level
124    /// `metadata.description` when unset.
125    pub description: Option<String>,
126
127    /// Templated homepage URL. Falls back to `metadata.homepage` when unset.
128    pub homepage: Option<String>,
129
130    /// NPM `keywords` list.
131    pub keywords: Option<Vec<String>>,
132
133    /// Templated SPDX license identifier (e.g. `MIT`, `Apache-2.0`).
134    /// Falls back to `metadata.license` when unset.
135    pub license: Option<String>,
136
137    /// Templated `author` field for `package.json`. Falls back to the
138    /// project's `metadata.maintainers[0]`, and then to the crate's
139    /// `Cargo.toml [package].authors[0]`, when unset.
140    pub author: Option<String>,
141
142    /// npm `engines` constraint map written verbatim into `package.json`
143    /// (e.g. `{ node: ">=18" }`). When unset, anodizer emits a sensible
144    /// default of `{ node: ">=18" }` — the floor every leading native-CLI
145    /// wrapper (esbuild, biome, swc) declares. Set to an empty map to
146    /// suppress the field entirely.
147    pub engines: Option<std::collections::BTreeMap<String, String>>,
148
149    /// Explicit npm `files` allowlist written into `package.json`. When
150    /// unset, anodizer derives it from what each package actually ships
151    /// (the per-platform binary, the metapackage `shim.js`, or the
152    /// postinstall launcher/script) plus any `extra_files` basenames. Set
153    /// to an empty list to suppress the field (npm then falls back to its
154    /// implicit inclusion rules).
155    pub files: Option<Vec<String>>,
156
157    /// npm `publishConfig.provenance` flag. When unset, anodizer emits
158    /// `true` — the npm supply-chain norm that biome and swc both set,
159    /// pairing with anodizer's signing story. Set to `false` to disable.
160    pub provenance: Option<bool>,
161
162    /// Templated repository URL. Emitted as `repository.url` in
163    /// `package.json` with `type: git`.
164    pub repository: Option<String>,
165
166    /// Templated bug tracker URL. Emitted as `bugs.url` in `package.json`.
167    pub bugs: Option<String>,
168
169    /// NPM access level for scoped packages. Accepts `"public"` /
170    /// `"restricted"`. Scoped packages on npmjs.org default to
171    /// `restricted` unless this is set to `public`.
172    pub access: Option<String>,
173
174    /// NPM dist-tag for the publish (default `latest`). Templated.
175    pub tag: Option<String>,
176
177    /// Archive format the `postinstall` script downloads
178    /// (`tgz`, `tar.gz`, `tar`, `zip`, `binary`). Default `tgz`. Only consulted
179    /// in `postinstall` mode.
180    pub format: Option<String>,
181
182    /// Override the download URL emitted into the postinstall script
183    /// (templated). When unset, anodizer derives the URL from the
184    /// release context. Only consulted in `postinstall` mode.
185    pub url_template: Option<String>,
186
187    /// Additional files to include in the published package alongside the
188    /// generated metadata. Default `["README*", "LICENSE*"]` (applied at the
189    /// `Default` pass).
190    pub extra_files: Option<Vec<String>>,
191
192    /// Template-rendered file mappings (`src` may be a glob; rendered
193    /// contents written to `dst`).
194    pub templated_extra_files: Option<Vec<NpmTemplatedExtraFile>>,
195
196    /// Free-form root-level `package.json` fields. Shallow-merged into
197    /// the generated `package.json` (config keys win over generated ones).
198    /// Useful for `mcpName`, `funding`, or other npm metadata fields
199    /// anodizer does not surface as first-class options.
200    pub extra: Option<HashMap<String, serde_json::Value>>,
201
202    /// Override the registry endpoint (default `https://registry.npmjs.org`).
203    pub registry: Option<String>,
204
205    /// Auth token for the registry. Falls back to the `NPM_TOKEN` env var
206    /// when unset. Stored in `.npmrc` as `//<registry>/:_authToken=...`
207    /// at publish time and never passed via argv.
208    pub token: Option<String>,
209
210    /// Credential-selection strategy: `auto` (default) decides per package by
211    /// probing the registry for the package's existence; `token` always uses
212    /// the token; `oidc` always uses Trusted Publishing with no token fallback.
213    /// See [`NpmAuthMode`]. Absent in existing configs resolves to `auto`.
214    #[serde(default)]
215    pub auth: NpmAuthMode,
216
217    /// Skip this publisher. Accepts bool or template string.
218    /// Accepts the legacy `disable:` spelling via serde alias for back-compat.
219    #[serde(
220        default,
221        alias = "disable",
222        deserialize_with = "deserialize_string_or_bool_opt"
223    )]
224    pub skip: Option<StringOrBool>,
225
226    /// Override whether this publisher failing should fail the overall release.
227    ///
228    /// Default: `true` — NPM is a Manager-group publisher (one-way
229    /// 72-hour unpublish window), so a failed publish aborts by default
230    /// to avoid surprising the operator with a half-released version.
231    /// Set to `false` to log failures but continue.
232    #[serde(default, skip_serializing_if = "Option::is_none")]
233    pub required: Option<bool>,
234
235    /// Template-conditional gate: when the rendered result is falsy
236    /// (`"false"` / `"0"` / `"no"` / empty), the NPM publisher entry is
237    /// skipped. Render failure hard-errors.
238    #[serde(rename = "if")]
239    pub if_condition: Option<String>,
240    /// When `true`, a triggered rollback leaves this publisher's work in
241    /// place rather than attempting to undo it. Default `false`.
242    pub retain_on_rollback: Option<bool>,
243}
244
245/// Default for [`NpmConfig::libc_aware`] — emit musl and glibc linux
246/// packages separately.
247fn default_libc_aware() -> bool {
248    true
249}
250
251impl Default for NpmConfig {
252    fn default() -> Self {
253        Self {
254            id: None,
255            ids: None,
256            mode: NpmMode::default(),
257            scope: None,
258            metapackage: None,
259            bin: None,
260            libc_aware: default_libc_aware(),
261            name: None,
262            description: None,
263            homepage: None,
264            keywords: None,
265            license: None,
266            author: None,
267            engines: None,
268            files: None,
269            provenance: None,
270            repository: None,
271            bugs: None,
272            access: None,
273            tag: None,
274            format: None,
275            url_template: None,
276            extra_files: None,
277            templated_extra_files: None,
278            extra: None,
279            registry: None,
280            token: None,
281            auth: NpmAuthMode::default(),
282            skip: None,
283            required: None,
284            if_condition: None,
285            retain_on_rollback: None,
286        }
287    }
288}
289
290/// Template-rendered file mapping for [`NpmConfig::templated_extra_files`].
291#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
292#[serde(default, deny_unknown_fields)]
293pub struct NpmTemplatedExtraFile {
294    /// Source path (may be a glob; relative to the project root).
295    pub src: String,
296    /// Destination path inside the published package.
297    pub dst: String,
298}