rman 1.0.9 → 1.0.12

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.
@@ -2,8 +2,15 @@
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "$id": "https://raw.githubusercontent.com/panates/rman/main/schemas/rmanrc.schema.json",
4
4
  "title": "rman configuration",
5
- "description": "Schema for rman's .rmanrc (JSON) and .rmanrc.yml (YAML) config files, and the \"rman\" key in package.json. See docs/api.md#configuration-rmanrc-rmanrcyml for the full reference.",
5
+ "description": "Schema for rman's .rmanrc (JSON) and .rmanrc.yml (YAML) config files, and the \"rman\" key in package.json. See docs/api.md#configuration-rmanrc-rmanrcyml for the full reference. Any string value may embed ${{ ... }} expressions - real JavaScript, evaluated per package against pkg, repository, env and semver - so one declaration at the root can still say something package-specific (\"../../coverage/${{ pkg.basename }}\", \"panates/${{ pkg.basename }}:${{ semver.major(pkg.version) }}\"). A string that is nothing but one expression keeps that value's own type. A bare {{ ... }} is left alone, so a command carrying someone else's template survives untouched.",
6
6
  "type": "object",
7
+ "additionalProperties": false,
8
+ "patternProperties": {
9
+ "^\\[.+\\]$": {
10
+ "$ref": "#",
11
+ "description": "Config for the packages this selector names, rather than for the package of the directory declaring it - \"[*]\" for every package in the repository, \"[*-dialect]\" for a glob over package names, \"[pkg-a]\" for one. In YAML the quotes are REQUIRED: a bare [*] parses as a flow sequence and * as an alias indicator. Precedence, lowest first: \"[*]\", then other selectors in declaration order, then the package's own unmarked config."
12
+ }
13
+ },
7
14
  "properties": {
8
15
  "$schema": {
9
16
  "type": "string",
@@ -66,15 +73,24 @@
66
73
  "default": "release-*",
67
74
  "description": "Root-level. Tag naming the repository's own release, as opposed to the per-package/group tags \"changelog.tagPattern\" names - only created when the root is on a calendar version (a repo with more than one version line). Must NOT match any package's \"changelog.tagPattern\", or that package's changelog boundary resolves to the repository release instead of its own."
68
75
  },
69
- "script": {
76
+ "stampDockerfile": {
77
+ "type": "boolean",
78
+ "default": true,
79
+ "description": "Per-package cascaded. Keep this package's Dockerfile \"org.opencontainers.image.version\" label in step with the version being written, in the same commit as the bump. The label's value is by specification the version of the packaged software, so there is only one correct value for it. Only ever rewrites a label the Dockerfile already declares (never inserts one), and reads the same path \"publish --target docker\" builds from (\"publish.docker.dockerfile\")."
80
+ },
81
+ "stamp": {
70
82
  "$ref": "#/definitions/stringOrStringArray",
71
- "description": "Command(s) to run as this package's own \"version\" npm-lifecycle step, when its package.json does not define one itself."
83
+ "description": "Source files whose \"version\" constant is rewritten to the version being written, in the same commit as the bump - paths relative to the package's own directory (e.g. [\"src/constants.ts\"]). Per-package cascaded; a listed file a package doesn't have is a silent no-op. Stamps the source rather than the build output, so anything running from source reports the real version and git records it."
72
84
  },
73
- "preScript": {
85
+ "before": {
74
86
  "$ref": "#/definitions/stringOrStringArray",
75
87
  "description": "Command(s) to run as this package's own \"preversion\" step, when its package.json does not define one itself."
76
88
  },
77
- "postScript": {
89
+ "exec": {
90
+ "$ref": "#/definitions/stringOrStringArray",
91
+ "description": "Command(s) to run as this package's own \"version\" npm-lifecycle step, when its package.json does not define one itself."
92
+ },
93
+ "after": {
78
94
  "$ref": "#/definitions/stringOrStringArray",
79
95
  "description": "Command(s) to run as this package's own \"postversion\" step, when its package.json does not define one itself."
80
96
  }
@@ -131,11 +147,11 @@
131
147
  },
132
148
  "publish": {
133
149
  "type": "object",
134
- "description": "Options for the \"publish\" command/PublishService, DockerPublishService and GithubReleaseService. Per-package cascaded.",
150
+ "description": "Options for the \"publish\" command/PublishService and DockerPublishService. Per-package cascaded.",
135
151
  "additionalProperties": false,
136
152
  "properties": {
137
153
  "target": {
138
- "description": "Where \"publish\" releases this package to. Default [\"npm\"]. A package that only ships Docker images (typically also \"private\": true) sets [\"docker\"]; a standalone app shipped as GitHub Release assets - or deployed elsewhere, with the release only recording that it shipped - sets [\"github\"]; any combination works. Each target answers \"is this version already out there?\" against its own registry: npm via \"npm view\", docker via \"docker manifest inspect\", github via the release for that version's tag.",
154
+ "description": "Which registry \"publish\" ships this package to. Default [\"npm\"]. A package that only ships Docker images (typically also \"private\": true) sets [\"docker\"]; both works too. Each target answers \"is this version already out there?\" against its own registry: npm via \"npm view\", docker via \"docker manifest inspect\". This is strictly about package distribution - the repository's GitHub Release is not a target here, see \"githubRelease\" and the \"github-release\" command.",
139
155
  "default": [
140
156
  "npm"
141
157
  ],
@@ -151,14 +167,14 @@
151
167
  }
152
168
  ]
153
169
  },
170
+ "directory": {
171
+ "type": "string",
172
+ "description": "Where this package's publishable output lives, relative to its own directory (e.g. \"build\"). Per-package cascaded, so a root \"[*]\" block can say it once instead of repeating publishConfig.directory in every package.json - which still wins when a package declares it. Publishing from such a directory means the manifest there is GENERATED by publish, from the package's own, with devDependencies, non-install scripts, \"private\" and publishConfig.directory removed and \"workspace:\" ranges resolved. There is nothing to configure about it."
173
+ },
154
174
  "docker": {
155
175
  "$ref": "#/definitions/dockerPublishConfig",
156
176
  "description": "Required once \"docker\" is one of this package's \"publish.target\"s."
157
177
  },
158
- "github": {
159
- "$ref": "#/definitions/githubPublishConfig",
160
- "description": "Optional even when \"github\" is one of this package's \"publish.target\"s - every required fact already has a sensible default source."
161
- },
162
178
  "skip": {
163
179
  "type": "boolean",
164
180
  "default": false,
@@ -166,39 +182,40 @@
166
182
  }
167
183
  }
168
184
  },
185
+ "githubRelease": {
186
+ "$ref": "#/definitions/githubReleaseConfig",
187
+ "description": "Details of the repository's GitHub Release (\"github-release\"/GithubReleaseService). Entirely optional - every required fact already has a sensible default source. Nothing here decides whether a release is cut: a release records that the repository shipped, so it is always cut. \"assets\" is per-package cascaded; the rest is root-level only."
188
+ },
169
189
  "run": {
170
190
  "type": "object",
171
- "description": "Per-script options for \"run\"/\"build\"/\"test\"/RunService, keyed by npm script name (e.g. \"build\", \"lint\", \"test\").",
191
+ "description": "Per-script options for \"run\"/\"build\"/\"test\"/RunService, keyed by npm script name (e.g. \"build\", \"lint\", \"test\"). A bare string (or array of them) is shorthand for { \"exec\": ... } - \"test\": \"mocha\" and \"test\": { \"exec\": \"mocha\" } mean exactly the same thing.",
172
192
  "additionalProperties": {
173
- "$ref": "#/definitions/runScriptConfig"
193
+ "oneOf": [
194
+ {
195
+ "$ref": "#/definitions/stringOrStringArray"
196
+ },
197
+ {
198
+ "$ref": "#/definitions/runScriptConfig"
199
+ }
200
+ ]
174
201
  }
175
202
  },
176
- "packages": {
177
- "type": "object",
178
- "description": "Per-package overrides, keyed by the in-repo package's own name.",
179
- "additionalProperties": {
180
- "type": "object",
181
- "additionalProperties": false,
182
- "properties": {
183
- "dependencies": {
184
- "description": "Extra in-repo \"dependencies\" not present in this package's real package.json, purely for rman's own dependency graph (topo-sort, --deps/--dependents, run's task scheduling). An array defaults each entry's range to \"*\"; an object gives an explicit name -> range map.",
185
- "oneOf": [
186
- {
187
- "type": "array",
188
- "items": {
189
- "type": "string"
190
- }
191
- },
192
- {
193
- "type": "object",
194
- "additionalProperties": {
195
- "type": "string"
196
- }
197
- }
198
- ]
203
+ "dependencies": {
204
+ "description": "In-repo packages this one depends on beyond what its real package.json declares, purely for rman's own dependency graph (topo-sort, --deps/--dependents, run's task scheduling). An array defaults each entry's range to \"*\"; an object gives an explicit name -> range map. Declare it from the root through a selector (\"[pkg-a]\": { \"dependencies\": [...] }) or in the package's own .rmanrc.",
205
+ "oneOf": [
206
+ {
207
+ "type": "array",
208
+ "items": {
209
+ "type": "string"
210
+ }
211
+ },
212
+ {
213
+ "type": "object",
214
+ "additionalProperties": {
215
+ "type": "string"
199
216
  }
200
217
  }
201
- }
218
+ ]
202
219
  }
203
220
  },
204
221
  "definitions": {
@@ -230,8 +247,7 @@
230
247
  "type": "string",
231
248
  "enum": [
232
249
  "npm",
233
- "docker",
234
- "github"
250
+ "docker"
235
251
  ]
236
252
  },
237
253
  "dockerPublishConfig": {
@@ -286,7 +302,7 @@
286
302
  }
287
303
  }
288
304
  },
289
- "githubPublishConfig": {
305
+ "githubReleaseConfig": {
290
306
  "type": "object",
291
307
  "additionalProperties": false,
292
308
  "properties": {
@@ -295,7 +311,7 @@
295
311
  "items": {
296
312
  "type": "string"
297
313
  },
298
- "description": "Files to attach to the release, as glob patterns relative to the package's own directory (e.g. [\"dist/*.tar.gz\"]). A release with no assets is still valid."
314
+ "description": "Files to attach to the release, as glob patterns relative to the package's own directory (e.g. [\"dist/*.tar.gz\"]). Read from every package, since one release covers the whole source tree. A release with no assets is still valid."
299
315
  },
300
316
  "repository": {
301
317
  "type": "string",
@@ -353,22 +369,22 @@
353
369
  "type": "string",
354
370
  "description": "Conditional-execution expression: atoms \"changed\"/\"dirty\"/\"committed\" (optionally \"= <hash>\" or \"= {ENV_VAR}\"), combined with and/or/not/(...). E.g. \"changed\", \"(changed or dirty) and not committed\"."
355
371
  },
356
- "script": {
372
+ "before": {
357
373
  "$ref": "#/definitions/stringOrStringArray",
358
- "description": "Command(s) to run as this package's own script, when its package.json does not define one for this script name."
374
+ "description": "Command(s) to run as this package's own \"pre<script>\" hook, when its package.json does not define one."
359
375
  },
360
- "preScript": {
376
+ "exec": {
361
377
  "$ref": "#/definitions/stringOrStringArray",
362
- "description": "Command(s) to run as this package's own \"pre<script>\" hook, when its package.json does not define one."
378
+ "description": "Command(s) to run as this package's own script, when its package.json does not define one for this script name."
363
379
  },
364
- "postScript": {
380
+ "after": {
365
381
  "$ref": "#/definitions/stringOrStringArray",
366
382
  "description": "Command(s) to run as this package's own \"post<script>\" hook, when its package.json does not define one."
367
383
  },
368
384
  "override": {
369
385
  "type": "boolean",
370
386
  "default": false,
371
- "description": "When true, \"script\"/\"preScript\"/\"postScript\" above replace the package's own package.json definition even when it already has one."
387
+ "description": "When true, \"before\"/\"exec\"/\"after\" above replace the package's own package.json definition even when it already has one."
372
388
  }
373
389
  }
374
390
  }
@@ -11,7 +11,7 @@ export declare namespace GithubReleaseService {
11
11
  /** Uncommitted local changes anywhere in the repository make the release `'skip'` instead of
12
12
  * `'error'` - same as `version`/`publish`'s other targets. */
13
13
  ignoreDirty?: boolean;
14
- /** `owner/repo` override - otherwise the root's own `publish.github.repository`, falling back
14
+ /** `owner/repo` override - otherwise the root's own `githubRelease.repository`, falling back
15
15
  * to the `origin` remote's URL. */
16
16
  repository?: string;
17
17
  }
@@ -31,13 +31,14 @@ export declare namespace GithubReleaseService {
31
31
  reason?: string;
32
32
  }
33
33
  /**
34
- * Computes what `publish --target github` *would* do - **one** release per run, or none.
34
+ * Computes what `github-release` *would* do - **one** release per run.
35
35
  *
36
36
  * A GitHub Release is a property of the repository, not of a package: the tag covers the whole
37
- * source tree, so everything that shipped under it belongs in it. That makes the `"github"`
38
- * target a repository-level opt-in (typically in the root `.rmanrc`, alongside `"npm"`); it's
39
- * honored as soon as *any* package resolves it, since a per-package release would have to invent
40
- * a tag no package owns.
37
+ * source tree, so everything that shipped under it belongs in it. There is deliberately **no
38
+ * opt-in**: it isn't a place a package ships to (that's `publish.target`, which is about
39
+ * registries - npm, Docker Hub, GitHub Packages), it's the repository's own record that a
40
+ * release happened, and a repository always wants that record. Re-running is harmless - an
41
+ * existing release for the tag reads `'up-to-date'`.
41
42
  *
42
43
  * The release is identified by the repository's own version (the root's - see
43
44
  * `VersionService`'s `buildRootEntry`): its release tag when that version is a calendar one, and
@@ -52,9 +53,9 @@ export declare namespace GithubReleaseService {
52
53
  */
53
54
  function getPlan(repository: Repository, options?: Options, deps?: Deps): Promise<Entry[]>;
54
55
  /**
55
- * Creates the repository's GitHub Release, then uploads whatever `publish.github.assets` globs
56
- * match. The body covers **every** package that shipped under this release - not just the ones
57
- * naming `"github"` as a target - since the tag covers all of their code either way.
56
+ * Creates the repository's GitHub Release, then uploads whatever `githubRelease.assets` globs
57
+ * match, across every package. The body covers **every** package that shipped under this
58
+ * release, since the tag covers all of their code either way.
58
59
  *
59
60
  * Each package's notes are bounded by the *previous repository release*, and headed with that
60
61
  * package's own version, so a repo whose packages sit on different version lines still reads
@@ -9,13 +9,14 @@ import { ChangelogService } from './changelog.service.js';
9
9
  export var GithubReleaseService;
10
10
  (function (GithubReleaseService) {
11
11
  /**
12
- * Computes what `publish --target github` *would* do - **one** release per run, or none.
12
+ * Computes what `github-release` *would* do - **one** release per run.
13
13
  *
14
14
  * A GitHub Release is a property of the repository, not of a package: the tag covers the whole
15
- * source tree, so everything that shipped under it belongs in it. That makes the `"github"`
16
- * target a repository-level opt-in (typically in the root `.rmanrc`, alongside `"npm"`); it's
17
- * honored as soon as *any* package resolves it, since a per-package release would have to invent
18
- * a tag no package owns.
15
+ * source tree, so everything that shipped under it belongs in it. There is deliberately **no
16
+ * opt-in**: it isn't a place a package ships to (that's `publish.target`, which is about
17
+ * registries - npm, Docker Hub, GitHub Packages), it's the repository's own record that a
18
+ * release happened, and a repository always wants that record. Re-running is harmless - an
19
+ * existing release for the tag reads `'up-to-date'`.
19
20
  *
20
21
  * The release is identified by the repository's own version (the root's - see
21
22
  * `VersionService`'s `buildRootEntry`): its release tag when that version is a calendar one, and
@@ -30,18 +31,15 @@ export var GithubReleaseService;
30
31
  */
31
32
  async function getPlan(repository, options = {}, deps = {}) {
32
33
  const root = repository.rootPackage;
33
- const wanted = [root, ...repository.getPackages()].some(pkg => targetsGithub(pkg) && !pkg.config.publish?.skip);
34
- if (!wanted)
35
- return [];
36
34
  const git = new GitHelper({ cwd: repository.dirname });
37
35
  const base = { package: root, version: root.version };
38
- const repo = options.repository ?? root.config?.publish?.github?.repository ?? repoFromRemoteUrl(await git.remoteUrl());
36
+ const repo = options.repository ?? root.config?.githubRelease?.repository ?? repoFromRemoteUrl(await git.remoteUrl());
39
37
  if (!repo) {
40
38
  return [
41
39
  {
42
40
  ...base,
43
41
  status: 'error',
44
- reason: 'cannot resolve "owner/repo" - set "publish.github.repository" or an "origin" remote',
42
+ reason: 'cannot resolve "owner/repo" - set "githubRelease.repository" or an "origin" remote',
45
43
  },
46
44
  ];
47
45
  }
@@ -91,9 +89,9 @@ export var GithubReleaseService;
91
89
  }
92
90
  GithubReleaseService.getPlan = getPlan;
93
91
  /**
94
- * Creates the repository's GitHub Release, then uploads whatever `publish.github.assets` globs
95
- * match. The body covers **every** package that shipped under this release - not just the ones
96
- * naming `"github"` as a target - since the tag covers all of their code either way.
92
+ * Creates the repository's GitHub Release, then uploads whatever `githubRelease.assets` globs
93
+ * match, across every package. The body covers **every** package that shipped under this
94
+ * release, since the tag covers all of their code either way.
97
95
  *
98
96
  * Each package's notes are bounded by the *previous repository release*, and headed with that
99
97
  * package's own version, so a repo whose packages sit on different version lines still reads
@@ -113,7 +111,7 @@ export var GithubReleaseService;
113
111
  const release = await createOrUpdateRelease(entry.repository, entry.tag, {
114
112
  name: entry.tag,
115
113
  body,
116
- draft: !!repository.rootPackage.config?.publish?.github?.draft,
114
+ draft: !!repository.rootPackage.config?.githubRelease?.draft,
117
115
  prerelease: resolvePrerelease(repository.rootPackage, entry.version),
118
116
  });
119
117
  await uploadAssets(repository, entry.repository, release.id);
@@ -127,11 +125,6 @@ export var GithubReleaseService;
127
125
  })(GithubReleaseService || (GithubReleaseService = {}));
128
126
  const GITHUB_API = 'https://api.github.com';
129
127
  const GITHUB_UPLOADS = 'https://uploads.github.com';
130
- function targetsGithub(pkg) {
131
- const target = pkg.config.publish?.target;
132
- const targets = Array.isArray(target) ? target : target ? [target] : ['npm'];
133
- return targets.includes('github');
134
- }
135
128
  /** The tag naming this repository's release. A calendar root version means several version lines,
136
129
  * so the release needs a name of its own (`release-*`); a plain one means every package shares it,
137
130
  * and that shared version's tag already *is* the release. */
@@ -144,7 +137,7 @@ function releaseTagGlob(root) {
144
137
  return pattern.replace('{name}', root.name);
145
138
  }
146
139
  function resolvePrerelease(root, version) {
147
- const configured = root.config?.publish?.github?.prerelease;
140
+ const configured = root.config?.githubRelease?.prerelease;
148
141
  // A calendar version's time part is a semver prerelease identifier by construction - it says
149
142
  // nothing about the release being a preview, so it must not be read as one.
150
143
  return configured ?? (!isCalendarVersion(version) && !!semver.prerelease(version));
@@ -242,12 +235,12 @@ async function createOrUpdateRelease(repository, tag, fields) {
242
235
  }
243
236
  return release;
244
237
  }
245
- /** Every `publish.github.assets` glob across the repository, each resolved against its own
238
+ /** Every `githubRelease.assets` glob across the repository, each resolved against its own
246
239
  * package's directory - an app ships its artifacts from its own folder, but they all land on the
247
240
  * one release the repository cut. */
248
241
  async function uploadAssets(repository, repo, releaseId) {
249
242
  for (const pkg of [repository.rootPackage, ...repository.getPackages()]) {
250
- const patterns = pkg.config.publish?.github?.assets;
243
+ const patterns = pkg.config.githubRelease?.assets;
251
244
  if (!patterns?.length)
252
245
  continue;
253
246
  const files = await fastGlob(patterns, { cwd: pkg.dirname, absolute: true, onlyFiles: true });
@@ -1,3 +1,19 @@
1
+ /**
2
+ * Prepares the `package.json` that `npm publish` will actually read, and returns a function that
3
+ * undoes it - always call that from a `finally`, so a failed publish leaves nothing behind.
4
+ *
5
+ * Which file that is depends on where the output lives:
6
+ *
7
+ * - **Publishing the package directory itself** - its own `package.json` is the manifest, rewritten
8
+ * in place: every `"workspace:"` range becomes a real, registry-consumable one (the same
9
+ * substitution pnpm/yarn's own publish performs - see `resolveWorkspaceRange`), since `npm
10
+ * publish` reads what is on disk rather than packing from a staging tarball.
11
+ * - **Publishing a build directory** - there is no manifest there until something writes one, and
12
+ * that something is this: see `derivePublishManifest`.
13
+ *
14
+ * Returns `undefined` when there is nothing to do at all (the package directory, with no
15
+ * `"workspace:"` range in it) - no disk write, nothing to restore.
16
+ */
1
17
  import { execFile } from 'node:child_process';
2
18
  import fs from 'node:fs';
3
19
  import path from 'node:path';
@@ -8,6 +24,94 @@ import { filterPackages } from '../utils/package-filter.js';
8
24
  import { parseWorkspaceRange, resolveWorkspaceRange } from '../utils/workspace-range.js';
9
25
  import { CiService } from './ci.service.js';
10
26
  import { DEPENDENCY_KEYS } from './version.service.js';
27
+ function preparePublishManifest(pkg, publishDir, packagesByName) {
28
+ if (path.resolve(publishDir) !== path.resolve(pkg.dirname)) {
29
+ const file = path.join(publishDir, 'package.json');
30
+ const previous = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : undefined;
31
+ fs.mkdirSync(publishDir, { recursive: true });
32
+ fs.writeFileSync(file, JSON.stringify(derivePublishManifest(pkg, packagesByName), undefined, 2) + '\n', 'utf-8');
33
+ return () => {
34
+ if (previous === undefined)
35
+ fs.rmSync(file, { force: true });
36
+ else
37
+ fs.writeFileSync(file, previous, 'utf-8');
38
+ };
39
+ }
40
+ const hasWorkspaceRange = DEPENDENCY_KEYS.some(depKey => {
41
+ const deps = pkg.json[depKey];
42
+ return deps && Object.values(deps).some(v => parseWorkspaceRange(v));
43
+ });
44
+ if (!hasWorkspaceRange)
45
+ return undefined;
46
+ const original = fs.readFileSync(pkg.jsonFileName, 'utf-8');
47
+ resolveWorkspaceRanges(pkg.json, packagesByName);
48
+ pkg.writeJson();
49
+ return () => {
50
+ fs.writeFileSync(pkg.jsonFileName, original, 'utf-8');
51
+ pkg.reloadJson();
52
+ };
53
+ }
54
+ /**
55
+ * The manifest to publish from a build directory, derived from the package's own - generated here
56
+ * rather than by a build script, and deliberately with nothing to configure.
57
+ *
58
+ * Generated at *publish* time, which is the whole point: a build script writes it when the build
59
+ * runs, so bumping the version afterwards (or building before a bump) publishes a manifest that
60
+ * disagrees with the package - and the `"workspace:"` rewrite above, which only ever touched the
61
+ * package's own file, never reached the copy at all.
62
+ *
63
+ * What comes out is the package's `package.json` minus what a consumer of the tarball can neither
64
+ * see nor use:
65
+ *
66
+ * - `devDependencies` - npm never installs a dependency's own, so they are pure noise.
67
+ * - `scripts`, **except** `preinstall`/`install`/`postinstall`. Those three are the only ones a
68
+ * consumer's install actually runs, and dropping them would silently break every package that
69
+ * builds a native module on install. The rest (`build`, `test`, `prepare`, ...) never reach the
70
+ * consumer - `prepare` runs for a git dependency, which builds from the repository, not from this
71
+ * tarball.
72
+ * - `private` - rman refuses to publish a private package in the first place, so carrying the flag
73
+ * into a manifest that is being published can only be wrong.
74
+ * - `publishConfig.directory` - it pointed *here*; kept, it would point one level deeper again.
75
+ */
76
+ function derivePublishManifest(pkg, packagesByName) {
77
+ const json = structuredClone(pkg.json);
78
+ resolveWorkspaceRanges(json, packagesByName);
79
+ delete json.devDependencies;
80
+ delete json.private;
81
+ if (json.scripts) {
82
+ const kept = Object.fromEntries(Object.entries(json.scripts).filter(([name]) => CONSUMER_SCRIPTS.has(name)));
83
+ if (Object.keys(kept).length)
84
+ json.scripts = kept;
85
+ else
86
+ delete json.scripts;
87
+ }
88
+ if (json.publishConfig) {
89
+ delete json.publishConfig.directory;
90
+ if (!Object.keys(json.publishConfig).length)
91
+ delete json.publishConfig;
92
+ }
93
+ return json;
94
+ }
95
+ /** The only lifecycle scripts a consumer's `npm install` of this package runs - see
96
+ * https://docs.npmjs.com/cli/using-npm/scripts. */
97
+ const CONSUMER_SCRIPTS = new Set(['preinstall', 'install', 'postinstall']);
98
+ /** Rewrites `json`'s `"workspace:"` ranges in place, resolving each against the in-repo package it
99
+ * names. Shared by both manifest paths, so they can never disagree about the substitution. */
100
+ function resolveWorkspaceRanges(json, packagesByName) {
101
+ for (const depKey of DEPENDENCY_KEYS) {
102
+ const deps = json[depKey];
103
+ if (!deps)
104
+ continue;
105
+ for (const depName of Object.keys(deps)) {
106
+ const parsed = parseWorkspaceRange(deps[depName]);
107
+ if (!parsed)
108
+ continue;
109
+ const depPkg = packagesByName.get(depName);
110
+ if (depPkg)
111
+ deps[depName] = resolveWorkspaceRange(parsed, depPkg.version);
112
+ }
113
+ }
114
+ }
11
115
  export var PublishService;
12
116
  (function (PublishService) {
13
117
  /**
@@ -106,12 +210,10 @@ export var PublishService;
106
210
  result.push({ ...entry, status: 'error', reason: `dependency "${blocker}" failed to publish` });
107
211
  continue;
108
212
  }
109
- const restore = rewriteWorkspaceRangesForPublish(pkg, packagesByName);
213
+ const publishDir = resolvePublishDir(pkg, options.contents);
214
+ const restore = preparePublishManifest(pkg, publishDir, packagesByName);
110
215
  try {
111
- await exec(buildPublishCommand(packageManager, options), {
112
- cwd: resolvePublishDir(pkg, options.contents),
113
- stdio: 'inherit',
114
- });
216
+ await exec(buildPublishCommand(packageManager, options), { cwd: publishDir, stdio: 'inherit' });
115
217
  result.push(entry);
116
218
  }
117
219
  catch (e) {
@@ -145,13 +247,14 @@ async function defaultNpmViewVersion(name, cwd, options) {
145
247
  return undefined;
146
248
  }
147
249
  }
148
- /** npm's own native `publishConfig.directory` (if the package declares one) always wins over
149
- * `options.contents` - the package's own package.json is the more authoritative, persistent
150
- * statement of "this is where the publishable output lives", `--contents` is just a fallback for
151
- * when it doesn't declare one at all. */
250
+ /** Where the publishable output lives, most specific statement first: the package's own
251
+ * `publishConfig.directory` (npm/pnpm's native spelling, and a statement about that one package),
252
+ * then `.rmanrc "publish.directory"` (which a `"[*]"` block can say once for a whole repository
253
+ * instead of repeating in every `package.json`), then `--contents` for a single run. */
152
254
  function resolvePublishDir(pkg, contentsOverride) {
153
255
  const native = pkg.json.publishConfig?.directory;
154
- const rel = (typeof native === 'string' && native) || contentsOverride;
256
+ const configured = pkg.config?.publish?.directory;
257
+ const rel = (typeof native === 'string' && native) || configured || contentsOverride;
155
258
  return rel ? path.resolve(pkg.dirname, rel) : pkg.dirname;
156
259
  }
157
260
  function buildPublishCommand(packageManager, options) {
@@ -168,40 +271,3 @@ function buildPublishCommand(packageManager, options) {
168
271
  args.push('--userconfig', options.userconfig);
169
272
  return `${packageManager} ${args.join(' ')}`;
170
273
  }
171
- /**
172
- * Rewrites every `"workspace:"` dependency range in `pkg`'s `package.json` to a real,
173
- * registry-publishable range (the same substitution pnpm/yarn's own publish performs - see
174
- * `resolveWorkspaceRange`), just before shelling out to `npm publish` - which reads whatever is
175
- * actually on disk, unlike pnpm/yarn's own publish this never packs into a staging tarball first.
176
- * Returns a function that restores the file's original bytes (and `pkg`'s in-memory state)
177
- * verbatim; always invoke it from a `finally`, so a failed publish never leaves a rewritten
178
- * `package.json` behind. Returns `undefined` (nothing to restore, no disk write at all) when `pkg`
179
- * has no `"workspace:"` ranges to begin with.
180
- */
181
- function rewriteWorkspaceRangesForPublish(pkg, packagesByName) {
182
- const hasWorkspaceRange = DEPENDENCY_KEYS.some(depKey => {
183
- const deps = pkg.json[depKey];
184
- return deps && Object.values(deps).some(v => parseWorkspaceRange(v));
185
- });
186
- if (!hasWorkspaceRange)
187
- return undefined;
188
- const original = fs.readFileSync(pkg.jsonFileName, 'utf-8');
189
- for (const depKey of DEPENDENCY_KEYS) {
190
- const deps = pkg.json[depKey];
191
- if (!deps)
192
- continue;
193
- for (const depName of Object.keys(deps)) {
194
- const parsed = parseWorkspaceRange(deps[depName]);
195
- if (!parsed)
196
- continue;
197
- const depPkg = packagesByName.get(depName);
198
- if (depPkg)
199
- deps[depName] = resolveWorkspaceRange(parsed, depPkg.version);
200
- }
201
- }
202
- pkg.writeJson();
203
- return () => {
204
- fs.writeFileSync(pkg.jsonFileName, original, 'utf-8');
205
- pkg.reloadJson();
206
- };
207
- }
@@ -42,10 +42,15 @@ export declare namespace RunService {
42
42
  * `getScriptSteps`:
43
43
  * run:
44
44
  * build:
45
- * script: tsc -b
46
- * preScript: [node ./generate.js, node ./validate.js]
47
- * postScript: node ./copy-assets.js
45
+ * before: [node ./generate.js, node ./validate.js]
46
+ * exec: tsc -b
47
+ * after: node ./copy-assets.js
48
48
  * override: true # use these even if the package *does* define its own
49
+ *
50
+ * A bare string is shorthand for `exec`, which is by far the common case - a script that is just
51
+ * a command, with nothing to configure about how it runs:
52
+ * run:
53
+ * test: mocha # same as test: { exec: mocha }
49
54
  */
50
55
  function getConfig(pkg: Package, script: string): Record<string, unknown>;
51
56
  /**