rman 1.0.12 → 1.2.1

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.
Files changed (102) hide show
  1. package/README.md +90 -70
  2. package/cli.js +226 -14
  3. package/commands/build.command.js +1 -0
  4. package/commands/changed.command.js +2 -2
  5. package/commands/changelog.command.js +13 -15
  6. package/commands/config.command.js +61 -0
  7. package/commands/diff.command.js +9 -4
  8. package/commands/exec.command.js +2 -8
  9. package/commands/github-release.command.js +1 -0
  10. package/commands/info.command.d.ts +9 -0
  11. package/commands/info.command.js +12 -2
  12. package/commands/run.command.js +5 -8
  13. package/commands/test.command.js +1 -0
  14. package/commands/version.command.js +53 -14
  15. package/constants.js +1 -1
  16. package/core/config.d.ts +265 -17
  17. package/core/config.js +651 -76
  18. package/core/custom-command.d.ts +133 -0
  19. package/core/custom-command.js +99 -0
  20. package/core/extends-config.d.ts +27 -0
  21. package/core/extends-config.js +89 -0
  22. package/core/manifest.d.ts +222 -0
  23. package/core/manifest.js +150 -0
  24. package/core/merge-config.d.ts +70 -0
  25. package/core/merge-config.js +193 -0
  26. package/core/package.d.ts +73 -7
  27. package/core/package.js +86 -24
  28. package/core/plugin.d.ts +112 -0
  29. package/core/plugin.js +189 -0
  30. package/core/repository.d.ts +91 -1
  31. package/core/repository.js +277 -132
  32. package/core/resolve-target.d.ts +12 -0
  33. package/core/resolve-target.js +33 -0
  34. package/core/run-step.d.ts +75 -0
  35. package/core/run-step.js +1 -0
  36. package/core/version-scheme.d.ts +134 -0
  37. package/core/version-scheme.js +148 -0
  38. package/core/workspace.d.ts +68 -0
  39. package/core/workspace.js +83 -0
  40. package/index.d.ts +55 -8
  41. package/index.js +42 -7
  42. package/interfaces/rman-config.interface.d.ts +222 -46
  43. package/package.json +16 -7
  44. package/services/change-hash.service.d.ts +88 -0
  45. package/services/change-hash.service.js +112 -0
  46. package/services/changelog.service.d.ts +8 -13
  47. package/services/changelog.service.js +12 -11
  48. package/services/conventional-commits.service.d.ts +73 -0
  49. package/services/conventional-commits.service.js +116 -0
  50. package/services/docker-publish.service.js +1 -1
  51. package/services/exec.service.js +1 -1
  52. package/services/github-release.service.d.ts +2 -2
  53. package/services/github-release.service.js +10 -5
  54. package/services/list.service.js +5 -2
  55. package/services/run.service.d.ts +112 -6
  56. package/services/run.service.js +265 -89
  57. package/services/system-info.d.ts +22 -7
  58. package/services/system-info.js +8 -23
  59. package/services/version-plan.service.d.ts +244 -0
  60. package/services/version-plan.service.js +414 -0
  61. package/services/version.service.d.ts +102 -82
  62. package/services/version.service.js +226 -434
  63. package/services.d.ts +5 -3
  64. package/services.js +5 -3
  65. package/utils/bin-path.d.ts +59 -0
  66. package/utils/bin-path.js +82 -0
  67. package/utils/child-tracker.d.ts +16 -0
  68. package/utils/child-tracker.js +30 -0
  69. package/utils/exec.d.ts +13 -2
  70. package/utils/exec.js +17 -17
  71. package/utils/git.d.ts +9 -3
  72. package/utils/git.js +10 -2
  73. package/utils/package-filter.d.ts +33 -2
  74. package/utils/package-filter.js +47 -7
  75. package/utils/printable-config.d.ts +15 -0
  76. package/utils/printable-config.js +42 -0
  77. package/utils/release-version.js +3 -3
  78. package/utils/run-bin.d.ts +46 -0
  79. package/utils/run-bin.js +63 -0
  80. package/utils/version-stamp.d.ts +14 -6
  81. package/utils/version-stamp.js +25 -13
  82. package/commands/ci.command.js +0 -30
  83. package/commands/clean.command.d.ts +0 -3
  84. package/commands/clean.command.js +0 -36
  85. package/commands/publish.command.d.ts +0 -3
  86. package/commands/publish.command.js +0 -225
  87. package/rmanrc.schema.json +0 -392
  88. package/services/ci.service.d.ts +0 -40
  89. package/services/ci.service.js +0 -204
  90. package/services/clean.service.d.ts +0 -42
  91. package/services/clean.service.js +0 -226
  92. package/services/publish.service.d.ts +0 -79
  93. package/services/publish.service.js +0 -273
  94. package/utils/change-hash.d.ts +0 -68
  95. package/utils/change-hash.js +0 -98
  96. package/utils/conventional-commits.d.ts +0 -52
  97. package/utils/conventional-commits.js +0 -90
  98. package/utils/npm-run-path.d.ts +0 -67
  99. package/utils/npm-run-path.js +0 -63
  100. package/utils/workspace-range.d.ts +0 -17
  101. package/utils/workspace-range.js +0 -28
  102. /package/commands/{ci.command.d.ts → config.command.d.ts} +0 -0
@@ -1,42 +0,0 @@
1
- import type { Repository } from '../core/repository.js';
2
- import { type LogLevel } from '../utils/logger.js';
3
- import { type PackageFilterOptions } from '../utils/package-filter.js';
4
- export declare namespace CleanService {
5
- interface Options extends PackageFilterOptions {
6
- /** Show the live progress panel. Default true; auto-disabled when stdout isn't a TTY. */
7
- progress?: boolean;
8
- /** Report what would be removed without actually removing anything. Default false. */
9
- dryRun?: boolean;
10
- /** Clean the whole repository even when the current directory is inside a single package
11
- * (which otherwise scopes cleaning to just that package - see `Repository.currentPackage`).
12
- * Has no effect when already at the repository root, or outside any known package. */
13
- root?: boolean;
14
- /** Verbosity of the classic per-item log (only applies when the live panel is off). Falls back
15
- * to the root's `.rmanrc logLevel`, then 'info' - see `resolveRootLogLevel`. */
16
- logLevel?: LogLevel;
17
- }
18
- /**
19
- * `clean`: removes build output across every package (root included) - the built-in replacement
20
- * for `ts-cleanup`, plus whatever else `.rmanrc clean.include`/`clean.exclude` says to remove.
21
- *
22
- * For every package not opted out via its own (cascaded) `clean.skip: true`:
23
- * - deletes compiled `.js`/`.js.map`/`.d.ts` files under its `src`/`test` (see `cleanTsArtifacts`);
24
- * - deletes any `*.tsbuildinfo` incremental-build cache file anywhere in it;
25
- * - deletes anything matching its own (cascaded) `clean.include` glob(s), minus `clean.exclude`.
26
- *
27
- * `include`/`exclude` are resolved relative to *that* package's own directory - a root-level
28
- * `.rmanrc` pattern like `packages/*\/build` is naturally evaluated from the repository root
29
- * (spanning every package in one pass), while a package's own override in its own `.rmanrc`
30
- * (e.g. `include: ./cache`) only ever reaches that one package, since a package normally
31
- * overrides rather than merges with the root's value for the same key.
32
- *
33
- * Never touches `node_modules` - that's `ci`'s job, not this one.
34
- *
35
- * Run from inside a single package's own directory, it only cleans that package unless
36
- * `options.root` says otherwise (see `Repository.currentPackage`).
37
- *
38
- * Uses the same live progress panel as `run`/`build` (see `../utils/progress-panel.ts`) - unlike
39
- * `ci`, cleaning genuinely is independent per-package work, so the final per-package tally stays.
40
- */
41
- function clean(repository: Repository, options?: Options): Promise<void>;
42
- }
@@ -1,226 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import colors from 'ansi-colors';
4
- import fg from 'fast-glob';
5
- import { Logger, resolveRootLogLevel } from '../utils/logger.js';
6
- import { filterPackages } from '../utils/package-filter.js';
7
- import { ProgressPanel } from '../utils/progress-panel.js';
8
- export var CleanService;
9
- (function (CleanService) {
10
- /**
11
- * `clean`: removes build output across every package (root included) - the built-in replacement
12
- * for `ts-cleanup`, plus whatever else `.rmanrc clean.include`/`clean.exclude` says to remove.
13
- *
14
- * For every package not opted out via its own (cascaded) `clean.skip: true`:
15
- * - deletes compiled `.js`/`.js.map`/`.d.ts` files under its `src`/`test` (see `cleanTsArtifacts`);
16
- * - deletes any `*.tsbuildinfo` incremental-build cache file anywhere in it;
17
- * - deletes anything matching its own (cascaded) `clean.include` glob(s), minus `clean.exclude`.
18
- *
19
- * `include`/`exclude` are resolved relative to *that* package's own directory - a root-level
20
- * `.rmanrc` pattern like `packages/*\/build` is naturally evaluated from the repository root
21
- * (spanning every package in one pass), while a package's own override in its own `.rmanrc`
22
- * (e.g. `include: ./cache`) only ever reaches that one package, since a package normally
23
- * overrides rather than merges with the root's value for the same key.
24
- *
25
- * Never touches `node_modules` - that's `ci`'s job, not this one.
26
- *
27
- * Run from inside a single package's own directory, it only cleans that package unless
28
- * `options.root` says otherwise (see `Repository.currentPackage`).
29
- *
30
- * Uses the same live progress panel as `run`/`build` (see `../utils/progress-panel.ts`) - unlike
31
- * `ci`, cleaning genuinely is independent per-package work, so the final per-package tally stays.
32
- */
33
- async function clean(repository, options = {}) {
34
- /** Standing inside a single package's own directory scopes cleaning to just that package
35
- * (root's own artifacts included) unless `--root` asks for the whole repository anyway - a
36
- * no-op when already at the root, or outside any known package. */
37
- const cwdScope = options.root ? undefined : repository.currentPackage;
38
- const packages = repository.getPackages().filter(p => p !== repository.rootPackage);
39
- const allTargets = cwdScope ? [cwdScope] : [repository.rootPackage, ...packages];
40
- const targets = filterPackages(allTargets, options).filter(pkg => !cleanConfig(pkg).skip);
41
- const dryRun = options.dryRun ?? false;
42
- const progress = options.progress ?? true;
43
- const logger = new Logger(options.logLevel ?? resolveRootLogLevel(repository));
44
- const panel = new ProgressPanel(dryRun ? 'CLEAN (dry-run)' : 'CLEAN', !!process.stdout.isTTY && progress);
45
- panel.start();
46
- let failed = false;
47
- try {
48
- const results = await Promise.allSettled(targets.map(pkg => {
49
- const label = pkg === repository.rootPackage ? 'root' : pkg.name;
50
- return cleanPackage(pkg, panel.addItem(label), panel.enabled, dryRun, logger);
51
- }));
52
- if (results.some(r => r.status === 'rejected'))
53
- failed = true;
54
- }
55
- finally {
56
- panel.stop();
57
- }
58
- panel.printSummary();
59
- if (failed) {
60
- const err = new Error('"clean" failed');
61
- err.logged = true;
62
- throw err;
63
- }
64
- }
65
- CleanService.clean = clean;
66
- })(CleanService || (CleanService = {}));
67
- /** Directories a package's TypeScript output gets cleaned from, mirroring `ts-cleanup -s src`
68
- * and `-s test` (the only way it was ever actually invoked in this project's own scripts). */
69
- const TS_SOURCE_DIRS = ['src', 'test'];
70
- /** A package's (cascaded) `.rmanrc clean.include`/`clean.exclude`/`clean.skip` - a package that
71
- * declares its own `clean` block replaces the root's entirely for itself (same as any other
72
- * rman config key), rather than combining with it. `include`/`exclude` accept a single glob
73
- * string or an array of them. */
74
- function cleanConfig(pkg) {
75
- const cfg = pkg.config?.clean;
76
- const normalize = (v) => {
77
- if (Array.isArray(v))
78
- return v.map(String);
79
- return typeof v === 'string' && v ? [v] : [];
80
- };
81
- return { include: normalize(cfg?.include), exclude: normalize(cfg?.exclude), skip: cfg?.skip === true };
82
- }
83
- /** Deletes `file`, unless `dryRun` - either way the caller treats it as removed for reporting. */
84
- async function remove(file, dryRun) {
85
- if (!dryRun)
86
- await fs.promises.rm(file, { force: true });
87
- }
88
- /** Removes now-empty directories under `dir`, deepest first (so a parent left empty once its
89
- * only child is removed gets caught in the same pass). Also removes `dir` itself once empty
90
- * when `includeSelf` is set - used when `dir` is itself something the user asked to delete
91
- * (e.g. an `include: build` match), as opposed to a source root like `src`/`test` that should
92
- * stay even if everything inside it was cleaned out. No-op in dry-run mode: nothing was actually
93
- * deleted, so there's nothing that could have been left empty. */
94
- function pruneEmptyDirs(dir, includeSelf, dryRun) {
95
- if (dryRun || !fs.existsSync(dir))
96
- return;
97
- const entries = fg.sync('**', { cwd: dir, onlyDirectories: true, dot: true });
98
- entries.sort((a, b) => b.split(path.sep).length - a.split(path.sep).length);
99
- for (const rel of entries) {
100
- const abs = path.join(dir, rel);
101
- if (fs.existsSync(abs) && fs.readdirSync(abs).length === 0)
102
- fs.rmdirSync(abs);
103
- }
104
- if (includeSelf && fs.readdirSync(dir).length === 0)
105
- fs.rmdirSync(dir);
106
- }
107
- /**
108
- * Deletes every path matched by `include` (minus `exclude`) under `dirname` - files and
109
- * directories alike, gulp-`src()`-style. Returns what was actually removed (or, in dry-run mode,
110
- * what *would* be), relative to `dirname`, so the caller only logs what really happened.
111
- *
112
- * `exclude` protects at two levels: a pattern matching a whole `include` result directly (e.g.
113
- * `packages/pkg1/*` against a matched `packages/pkg1/build` directory) drops that result before
114
- * anything under it is touched; a finer pattern (e.g. `build/*.json`) instead protects just the
115
- * matching files *inside* an otherwise-deleted directory, leaving the rest of it gone and the
116
- * protected files (and their now non-empty parent) in place.
117
- */
118
- async function cleanGlobs(dirname, include, exclude, dryRun) {
119
- if (!include.length)
120
- return [];
121
- const matches = await fg(include, { cwd: dirname, ignore: exclude, onlyFiles: false, dot: true, absolute: true });
122
- const protectedFiles = exclude.length
123
- ? new Set(await fg(exclude, { cwd: dirname, onlyFiles: true, dot: true, absolute: true }))
124
- : new Set();
125
- const removed = [];
126
- for (const m of matches) {
127
- const stat = await fs.promises.stat(m).catch(() => undefined);
128
- if (!stat)
129
- continue;
130
- if (stat.isDirectory()) {
131
- const filesInside = await fg('**', { cwd: m, onlyFiles: true, dot: true, absolute: true });
132
- for (const f of filesInside) {
133
- if (protectedFiles.has(f))
134
- continue;
135
- await remove(f, dryRun);
136
- removed.push(path.relative(dirname, f));
137
- }
138
- pruneEmptyDirs(m, true, dryRun);
139
- }
140
- else if (!protectedFiles.has(m)) {
141
- await remove(m, dryRun);
142
- removed.push(path.relative(dirname, m));
143
- }
144
- }
145
- return removed;
146
- }
147
- /**
148
- * Removes compiled TypeScript output (`.js`, `.js.map`, `.d.ts`) sitting next to its `.ts`
149
- * source under `src`/`test` - the same job `ts-cleanup -s <dir> --all` did (the only mode this
150
- * project ever actually used it in). A `.d.ts` with no matching `.ts`/`.tsx` is left alone - it's
151
- * presumably a hand-written declaration file, not build output. Prunes directories left empty
152
- * afterward.
153
- */
154
- async function cleanTsArtifacts(dirname, dryRun) {
155
- const removed = [];
156
- for (const sub of TS_SOURCE_DIRS) {
157
- const dir = path.join(dirname, sub);
158
- if (!fs.existsSync(dir))
159
- continue;
160
- const files = await fg('**/*.{js,js.map,d.ts}', { cwd: dir, onlyFiles: true, dot: true, absolute: true });
161
- for (const f of files) {
162
- if (f.endsWith('.d.ts')) {
163
- const base = f.slice(0, -'.d.ts'.length);
164
- if (!fs.existsSync(base + '.ts') && !fs.existsSync(base + '.tsx'))
165
- continue;
166
- }
167
- await remove(f, dryRun);
168
- removed.push(path.relative(dirname, f));
169
- }
170
- pruneEmptyDirs(dir, false, dryRun);
171
- }
172
- return removed;
173
- }
174
- /**
175
- * Removes TypeScript's incremental-build cache files (`*.tsbuildinfo`) anywhere under `dirname` -
176
- * `tsc --build`/`composite`/`incremental` output that `ts-cleanup` never touched, but which can
177
- * leave a project in a stale state if a clean rebuild is expected to start from nothing. Skips
178
- * `node_modules` (dependencies may ship their own, irrelevant to this package's own build).
179
- */
180
- async function cleanTsBuildInfo(dirname, dryRun) {
181
- const files = await fg('**/*.tsbuildinfo', {
182
- cwd: dirname,
183
- ignore: ['**/node_modules/**'],
184
- onlyFiles: true,
185
- dot: true,
186
- absolute: true,
187
- });
188
- for (const f of files)
189
- await remove(f, dryRun);
190
- return files.map(f => path.relative(dirname, f));
191
- }
192
- async function cleanPackage(pkg, item, panelEnabled, dryRun, logger) {
193
- item.status = 'running';
194
- item.startedAt = Date.now();
195
- try {
196
- item.currentStep = 'ts';
197
- const tsRemoved = await cleanTsArtifacts(pkg.dirname, dryRun);
198
- const buildInfoRemoved = await cleanTsBuildInfo(pkg.dirname, dryRun);
199
- item.currentStep = 'glob';
200
- const { include, exclude } = cleanConfig(pkg);
201
- const globRemoved = await cleanGlobs(pkg.dirname, include, exclude, dryRun);
202
- const removed = [...tsRemoved, ...buildInfoRemoved, ...globRemoved];
203
- const verb = dryRun ? 'would rm' : 'rm';
204
- if (panelEnabled) {
205
- item.lastLine = removed.length
206
- ? `${dryRun ? 'would remove' : 'removed'} ${removed.length} item(s)`
207
- : 'already clean';
208
- }
209
- else if (removed.length) {
210
- for (const r of removed)
211
- logger.info(colors.yellow(verb), colors.cyan(item.name), r);
212
- }
213
- else {
214
- logger.info(colors.gray('clean'), colors.cyan(item.name));
215
- }
216
- item.status = 'success';
217
- }
218
- catch (e) {
219
- item.status = 'failed';
220
- item.log.push(e?.message ?? String(e));
221
- throw e;
222
- }
223
- finally {
224
- item.finishedAt = Date.now();
225
- }
226
- }
@@ -1,79 +0,0 @@
1
- import type { Package } from '../core/package.js';
2
- import type { Repository } from '../core/repository.js';
3
- import { type PackageFilterOptions } from '../utils/package-filter.js';
4
- import { CiService } from './ci.service.js';
5
- export declare namespace PublishService {
6
- /** Injectable registry lookup - mainly for tests, so they don't depend on network access or a
7
- * real published package. Same shape as `detectChangeHash`'s own `npmViewVersion`. */
8
- interface Deps {
9
- npmViewVersion?: (name: string, cwd: string) => Promise<string | undefined>;
10
- }
11
- interface Options extends PackageFilterOptions {
12
- /** A package with uncommitted local changes is excluded (status `'skip'`) instead of aborting
13
- * the whole plan (status `'error'`) - same as `version`'s own option. Default false. */
14
- ignoreDirty?: boolean;
15
- /** Registry to check against (and, in `applyPlan`, publish to) - `.npmrc`'s own configured
16
- * registry is used when omitted. */
17
- registry?: string;
18
- /** Path to a custom `.npmrc`, for both the registry check and the actual publish. */
19
- userconfig?: string;
20
- }
21
- interface ApplyOptions extends Options {
22
- packageManager?: CiService.PackageManager;
23
- /** `npm publish --access <access>` - required by the registry for a *new* scoped package. */
24
- access?: 'public' | 'restricted';
25
- /** `npm publish --tag <tag>` - the dist-tag this version is published under (default `latest`). */
26
- tag?: string;
27
- /** `npm publish --otp <otp>` - a 2FA one-time password, for registries that require it. */
28
- otp?: string;
29
- /** Subdirectory to publish from, relative to the package's own directory - only consulted when
30
- * the package doesn't already declare its own `package.json` `publishConfig.directory` (npm's
31
- * native mechanism for this, which always wins when present). */
32
- contents?: string;
33
- }
34
- /** One package's outcome in a publish plan - see `getPlan`. */
35
- interface Entry {
36
- package: Package;
37
- version: string;
38
- status: 'publish' | 'skip' | 'up-to-date' | 'error';
39
- /** What's currently on the registry, if anything - only set once a registry check actually ran. */
40
- registryVersion?: string;
41
- reason?: string;
42
- }
43
- /**
44
- * Computes what `publish` *would* do, across every non-private package (topological order,
45
- * dependencies before dependents) - never touches the registry to publish anything, just
46
- * queries it to decide, so it's safe to call any time, including as the plan a bare `rman
47
- * publish` shows before asking for confirmation.
48
- *
49
- * A `private: true` package is always `'skip'`ped outright. A package with uncommitted local
50
- * changes is `'error'` (aborts the whole plan) unless `options.ignoreDirty` downgrades it to
51
- * `'skip'` instead - same rule `version` uses, since publishing untracked local edits is worse
52
- * than a bad commit. Otherwise, its currently-published registry version (via `npm view`,
53
- * queried concurrently across every remaining package) decides the rest: identical to the local
54
- * `package.json` version is `'up-to-date'`; anything else (including never having been
55
- * published at all) is `'publish'`.
56
- *
57
- * Deliberately decoupled from `version`: this only ever looks at what's *currently* on disk and
58
- * on the registry, never at whether `version` was just run - so it works equally well right
59
- * after a version bump, or standing alone in a release pipeline that bumped days earlier.
60
- *
61
- * A monorepo's root package is never a candidate at all - `repository.getPackages()` already
62
- * excludes it for a real monorepo (it only doubles as "the" package in a single-package repo,
63
- * where it's a normal candidate like any other).
64
- */
65
- function getPlan(repository: Repository, options?: Options, deps?: Deps): Promise<Entry[]>;
66
- /**
67
- * Publishes every `'publish'` entry in `plan`, topological order (already `plan`'s own order -
68
- * see `getPlan`), via the configured `packageManager`'s own `publish` command. Sequential, not
69
- * concurrent: unlike `run`/`build`, a package genuinely needs its own dependencies to have
70
- * landed on the registry first, and publishing is rare enough (once per release) that the
71
- * simplicity is worth more than the parallelism `run` gets from `power-tasks`.
72
- *
73
- * If a package fails, every other still-pending entry depending on it (transitively) is marked
74
- * `'error'` too and never attempted - publishing a package whose own new dependency range points
75
- * at a version that never actually reached the registry would hand consumers a broken install.
76
- * A package's own failure doesn't stop unrelated packages elsewhere in the plan, though.
77
- */
78
- function applyPlan(repository: Repository, plan: Entry[], options?: ApplyOptions): Promise<Entry[]>;
79
- }
@@ -1,273 +0,0 @@
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
- */
17
- import { execFile } from 'node:child_process';
18
- import fs from 'node:fs';
19
- import path from 'node:path';
20
- import { promisify } from 'node:util';
21
- import { exec } from '../utils/exec.js';
22
- import { GitHelper } from '../utils/git.js';
23
- import { filterPackages } from '../utils/package-filter.js';
24
- import { parseWorkspaceRange, resolveWorkspaceRange } from '../utils/workspace-range.js';
25
- import { CiService } from './ci.service.js';
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
- }
115
- export var PublishService;
116
- (function (PublishService) {
117
- /**
118
- * Computes what `publish` *would* do, across every non-private package (topological order,
119
- * dependencies before dependents) - never touches the registry to publish anything, just
120
- * queries it to decide, so it's safe to call any time, including as the plan a bare `rman
121
- * publish` shows before asking for confirmation.
122
- *
123
- * A `private: true` package is always `'skip'`ped outright. A package with uncommitted local
124
- * changes is `'error'` (aborts the whole plan) unless `options.ignoreDirty` downgrades it to
125
- * `'skip'` instead - same rule `version` uses, since publishing untracked local edits is worse
126
- * than a bad commit. Otherwise, its currently-published registry version (via `npm view`,
127
- * queried concurrently across every remaining package) decides the rest: identical to the local
128
- * `package.json` version is `'up-to-date'`; anything else (including never having been
129
- * published at all) is `'publish'`.
130
- *
131
- * Deliberately decoupled from `version`: this only ever looks at what's *currently* on disk and
132
- * on the registry, never at whether `version` was just run - so it works equally well right
133
- * after a version bump, or standing alone in a release pipeline that bumped days earlier.
134
- *
135
- * A monorepo's root package is never a candidate at all - `repository.getPackages()` already
136
- * excludes it for a real monorepo (it only doubles as "the" package in a single-package repo,
137
- * where it's a normal candidate like any other).
138
- */
139
- async function getPlan(repository, options = {}, deps = {}) {
140
- const git = new GitHelper({ cwd: repository.dirname });
141
- const packages = filterPackages(repository.getPackages({ toposort: true }), options);
142
- const dirtyFiles = await git.listDirtyFiles({ absolute: true });
143
- const isDirty = (pkg) => dirtyFiles.some(f => !path.relative(pkg.dirname, f).startsWith('..'));
144
- const npmViewVersion = deps.npmViewVersion ?? ((name, cwd) => defaultNpmViewVersion(name, cwd, options));
145
- const entries = new Map();
146
- const toCheck = [];
147
- for (const pkg of packages) {
148
- if (pkg.config.publish?.skip) {
149
- entries.set(pkg.name, {
150
- package: pkg,
151
- version: pkg.version,
152
- status: 'skip',
153
- reason: 'excluded via .rmanrc "publish.skip"',
154
- });
155
- }
156
- else if (pkg.isPrivate) {
157
- entries.set(pkg.name, { package: pkg, version: pkg.version, status: 'skip', reason: 'private package' });
158
- }
159
- else if (isDirty(pkg)) {
160
- entries.set(pkg.name, {
161
- package: pkg,
162
- version: pkg.version,
163
- status: options.ignoreDirty ? 'skip' : 'error',
164
- reason: 'uncommitted local changes',
165
- });
166
- }
167
- else {
168
- toCheck.push(pkg);
169
- }
170
- }
171
- await Promise.all(toCheck.map(async (pkg) => {
172
- const registryVersion = await npmViewVersion(pkg.name, pkg.dirname);
173
- entries.set(pkg.name, {
174
- package: pkg,
175
- version: pkg.version,
176
- registryVersion,
177
- status: registryVersion === pkg.version ? 'up-to-date' : 'publish',
178
- reason: registryVersion ? `registry has ${registryVersion}` : 'never published',
179
- });
180
- }));
181
- return packages.map(pkg => entries.get(pkg.name));
182
- }
183
- PublishService.getPlan = getPlan;
184
- /**
185
- * Publishes every `'publish'` entry in `plan`, topological order (already `plan`'s own order -
186
- * see `getPlan`), via the configured `packageManager`'s own `publish` command. Sequential, not
187
- * concurrent: unlike `run`/`build`, a package genuinely needs its own dependencies to have
188
- * landed on the registry first, and publishing is rare enough (once per release) that the
189
- * simplicity is worth more than the parallelism `run` gets from `power-tasks`.
190
- *
191
- * If a package fails, every other still-pending entry depending on it (transitively) is marked
192
- * `'error'` too and never attempted - publishing a package whose own new dependency range points
193
- * at a version that never actually reached the registry would hand consumers a broken install.
194
- * A package's own failure doesn't stop unrelated packages elsewhere in the plan, though.
195
- */
196
- async function applyPlan(repository, plan, options = {}) {
197
- const packageManager = CiService.resolvePackageManager(repository, options.packageManager);
198
- const failed = new Set();
199
- const result = [];
200
- const packagesByName = new Map(repository.getPackages().map(p => [p.name, p]));
201
- for (const entry of plan) {
202
- if (entry.status !== 'publish') {
203
- result.push(entry);
204
- continue;
205
- }
206
- const pkg = entry.package;
207
- const blocker = pkg.dependencies.find(d => failed.has(d));
208
- if (blocker) {
209
- failed.add(pkg.name);
210
- result.push({ ...entry, status: 'error', reason: `dependency "${blocker}" failed to publish` });
211
- continue;
212
- }
213
- const publishDir = resolvePublishDir(pkg, options.contents);
214
- const restore = preparePublishManifest(pkg, publishDir, packagesByName);
215
- try {
216
- await exec(buildPublishCommand(packageManager, options), { cwd: publishDir, stdio: 'inherit' });
217
- result.push(entry);
218
- }
219
- catch (e) {
220
- failed.add(pkg.name);
221
- result.push({ ...entry, status: 'error', reason: e.message });
222
- }
223
- finally {
224
- restore?.();
225
- }
226
- }
227
- return result;
228
- }
229
- PublishService.applyPlan = applyPlan;
230
- })(PublishService || (PublishService = {}));
231
- const execFileAsync = promisify(execFile);
232
- /** `npm view <name> version`, optionally against a custom registry/`.npmrc` - `undefined` for any
233
- * failure (never published, no network, private/restricted with no access, ...), same
234
- * catch-everything shape as `change-hash.ts`'s own `defaultNpmViewVersion` (kept separate here
235
- * since this one also needs `--registry`/`--userconfig`, which that one has no use for). */
236
- async function defaultNpmViewVersion(name, cwd, options) {
237
- const argv = ['view', name, 'version'];
238
- if (options.registry)
239
- argv.push('--registry', options.registry);
240
- if (options.userconfig)
241
- argv.push('--userconfig', options.userconfig);
242
- try {
243
- const { stdout } = await execFileAsync('npm', argv, { cwd });
244
- return stdout.trim() || undefined;
245
- }
246
- catch {
247
- return undefined;
248
- }
249
- }
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. */
254
- function resolvePublishDir(pkg, contentsOverride) {
255
- const native = pkg.json.publishConfig?.directory;
256
- const configured = pkg.config?.publish?.directory;
257
- const rel = (typeof native === 'string' && native) || configured || contentsOverride;
258
- return rel ? path.resolve(pkg.dirname, rel) : pkg.dirname;
259
- }
260
- function buildPublishCommand(packageManager, options) {
261
- const args = ['publish'];
262
- if (options.access)
263
- args.push('--access', options.access);
264
- if (options.tag)
265
- args.push('--tag', options.tag);
266
- if (options.otp)
267
- args.push('--otp', options.otp);
268
- if (options.registry)
269
- args.push('--registry', options.registry);
270
- if (options.userconfig)
271
- args.push('--userconfig', options.userconfig);
272
- return `${packageManager} ${args.join(' ')}`;
273
- }