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