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/// NPM package registry publisher configuration.
39///
40/// In the default `optional-deps` mode anodizer emits one thin npm package
41/// per built platform (with `os`/`cpu`/`libc` selectors derived from the
42/// target triple) plus a metapackage whose `optionalDependencies` lists
43/// every platform package; npm's native resolution installs only the one
44/// matching the host. In `postinstall` mode a single package carries a
45/// `postinstall` script that downloads the matching release archive at
46/// `npm install` time. Each `npms[]` entry produces one publish.
47#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
48#[serde(default, deny_unknown_fields)]
49pub struct NpmConfig {
50 /// Unique identifier for selecting this entry from the CLI (`--id=...`).
51 pub id: Option<String>,
52
53 /// Build IDs filter: only include artifacts whose archive `id` is in this list.
54 pub ids: Option<Vec<String>>,
55
56 /// Binary-distribution strategy. `optional-deps` (default) emits npm's
57 /// native per-platform packages; `postinstall` emits a download shim.
58 #[serde(default)]
59 pub mode: NpmMode,
60
61 /// npm scope for the per-platform packages emitted in `optional-deps`
62 /// mode (e.g. `@biomejs`). The per-platform packages are named
63 /// `<scope>/<bin>-<os>-<cpu>[-<libc>]`. Required for `optional-deps`
64 /// mode; ignored in `postinstall` mode.
65 pub scope: Option<String>,
66
67 /// Metapackage name for `optional-deps` mode (e.g. `biome`). This is the
68 /// package users `npm install`; it lists every per-platform package under
69 /// `optionalDependencies` and ships the `bin` shim. Falls back to `name`
70 /// (or the crate name) when unset.
71 pub metapackage: Option<String>,
72
73 /// Command name installed by the metapackage's `bin` map (`optional-deps`
74 /// mode). Falls back to the metapackage basename when unset.
75 pub bin: Option<String>,
76
77 /// In `optional-deps` mode, emit separate per-platform packages for linux
78 /// `musl` vs `glibc` (distinguished by the npm `libc` selector). When
79 /// `false`, a single linux package per cpu is emitted with no `libc`
80 /// selector. Default `true` — musl and glibc binaries are not
81 /// interchangeable, so collapsing them risks installing the wrong one.
82 #[serde(default = "default_libc_aware")]
83 pub libc_aware: bool,
84
85 /// NPM package name (the metapackage / postinstall package). May be scoped
86 /// (`@org/foo`) or unscoped (`foo`). Falls back to the crate name when unset.
87 pub name: Option<String>,
88
89 /// Templated package description. Falls back to the project-level
90 /// `metadata.description` when unset.
91 pub description: Option<String>,
92
93 /// Templated homepage URL. Falls back to `metadata.homepage` when unset.
94 pub homepage: Option<String>,
95
96 /// NPM `keywords` list.
97 pub keywords: Option<Vec<String>>,
98
99 /// Templated SPDX license identifier (e.g. `MIT`, `Apache-2.0`).
100 /// Falls back to `metadata.license` when unset.
101 pub license: Option<String>,
102
103 /// Templated `author` field for `package.json`. Falls back to
104 /// `metadata.maintainers[0]` when unset.
105 pub author: Option<String>,
106
107 /// Templated repository URL. Emitted as `repository.url` in
108 /// `package.json` with `type: git`.
109 pub repository: Option<String>,
110
111 /// Templated bug tracker URL. Emitted as `bugs.url` in `package.json`.
112 pub bugs: Option<String>,
113
114 /// NPM access level for scoped packages. Accepts `"public"` /
115 /// `"restricted"`. Scoped packages on npmjs.org default to
116 /// `restricted` unless this is set to `public`.
117 pub access: Option<String>,
118
119 /// NPM dist-tag for the publish (default `latest`). Templated.
120 pub tag: Option<String>,
121
122 /// Archive format the `postinstall` script downloads
123 /// (`tgz`, `tar.gz`, `tar`, `zip`, `binary`). Default `tgz`. Only consulted
124 /// in `postinstall` mode.
125 pub format: Option<String>,
126
127 /// Override the download URL emitted into the postinstall script
128 /// (templated). When unset, anodizer derives the URL from the
129 /// release context. Only consulted in `postinstall` mode.
130 pub url_template: Option<String>,
131
132 /// Additional files to include in the published package alongside the
133 /// generated metadata. Default `["README*", "LICENSE*"]` (applied at the
134 /// `Default` pass).
135 pub extra_files: Option<Vec<String>>,
136
137 /// Template-rendered file mappings (`src` may be a glob; rendered
138 /// contents written to `dst`).
139 pub templated_extra_files: Option<Vec<NpmTemplatedExtraFile>>,
140
141 /// Free-form root-level `package.json` fields. Shallow-merged into
142 /// the generated `package.json`. Useful for `engines`, `mcpName`,
143 /// or other npm metadata fields anodizer does not surface.
144 pub extra: Option<HashMap<String, serde_json::Value>>,
145
146 /// Override the registry endpoint (default `https://registry.npmjs.org`).
147 pub registry: Option<String>,
148
149 /// Auth token for the registry. Falls back to the `NPM_TOKEN` env var
150 /// when unset. Stored in `.npmrc` as `//<registry>/:_authToken=...`
151 /// at publish time and never passed via argv.
152 pub token: Option<String>,
153
154 /// Skip this publisher. Accepts bool or template string.
155 /// Accepts the legacy `disable:` spelling via serde alias for back-compat.
156 #[serde(
157 default,
158 alias = "disable",
159 deserialize_with = "deserialize_string_or_bool_opt"
160 )]
161 pub skip: Option<StringOrBool>,
162
163 /// Override whether this publisher failing should fail the overall release.
164 ///
165 /// Default: `true` — NPM is a Manager-group publisher (one-way
166 /// 72-hour unpublish window), so a failed publish aborts by default
167 /// to avoid surprising the operator with a half-released version.
168 /// Set to `false` to log failures but continue.
169 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub required: Option<bool>,
171
172 /// Template-conditional gate: when the rendered result is falsy
173 /// (`"false"` / `"0"` / `"no"` / empty), the NPM publisher entry is
174 /// skipped. Render failure hard-errors.
175 #[serde(rename = "if")]
176 pub if_condition: Option<String>,
177}
178
179/// Default for [`NpmConfig::libc_aware`] — emit musl and glibc linux
180/// packages separately.
181fn default_libc_aware() -> bool {
182 true
183}
184
185impl Default for NpmConfig {
186 fn default() -> Self {
187 Self {
188 id: None,
189 ids: None,
190 mode: NpmMode::default(),
191 scope: None,
192 metapackage: None,
193 bin: None,
194 libc_aware: default_libc_aware(),
195 name: None,
196 description: None,
197 homepage: None,
198 keywords: None,
199 license: None,
200 author: None,
201 repository: None,
202 bugs: None,
203 access: None,
204 tag: None,
205 format: None,
206 url_template: None,
207 extra_files: None,
208 templated_extra_files: None,
209 extra: None,
210 registry: None,
211 token: None,
212 skip: None,
213 required: None,
214 if_condition: None,
215 }
216 }
217}
218
219/// Template-rendered file mapping for [`NpmConfig::templated_extra_files`].
220#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
221#[serde(default, deny_unknown_fields)]
222pub struct NpmTemplatedExtraFile {
223 /// Source path (may be a glob; relative to the project root).
224 pub src: String,
225 /// Destination path inside the published package.
226 pub dst: String,
227}