rman-node 1.1.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.
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/augmentation/manifest.augmentation.d.ts +17 -0
- package/augmentation/manifest.augmentation.js +129 -0
- package/augmentation/rman.augmentation.d.ts +45 -0
- package/augmentation/rman.augmentation.js +1 -0
- package/augmentation/run.augmentation.d.ts +15 -0
- package/augmentation/run.augmentation.js +61 -0
- package/augmentation/system-info.augmentation.d.ts +26 -0
- package/augmentation/system-info.augmentation.js +79 -0
- package/augmentation/workspace.augmentation.d.ts +18 -0
- package/augmentation/workspace.augmentation.js +49 -0
- package/commands/ci.command.d.ts +9 -0
- package/commands/ci.command.js +35 -0
- package/commands/clean.command.d.ts +6 -0
- package/commands/clean.command.js +32 -0
- package/commands/publish.command.d.ts +9 -0
- package/commands/publish.command.js +228 -0
- package/index.d.ts +31 -0
- package/index.js +100 -0
- package/interfaces/rman-config.interface.d.ts +80 -0
- package/interfaces/rman-config.interface.js +7 -0
- package/package.json +54 -0
- package/services/ci.service.d.ts +38 -0
- package/services/ci.service.js +201 -0
- package/services/clean.service.d.ts +51 -0
- package/services/clean.service.js +235 -0
- package/services/publish.service.d.ts +77 -0
- package/services/publish.service.js +258 -0
- package/services/version-plan.service.d.ts +50 -0
- package/services/version-plan.service.js +67 -0
- package/utils/npm-run-path.d.ts +21 -0
- package/utils/npm-run-path.js +36 -0
- package/utils/npm-view.d.ts +15 -0
- package/utils/npm-view.js +28 -0
- package/utils/workspace-range.d.ts +26 -0
- package/utils/workspace-range.js +28 -0
|
@@ -0,0 +1,258 @@
|
|
|
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 fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { exec, filterPackages, GitHelper } from 'rman';
|
|
20
|
+
import { DEPENDENCY_KEYS } from '../augmentation/manifest.augmentation.js';
|
|
21
|
+
import { npmViewVersion } from '../utils/npm-view.js';
|
|
22
|
+
import { parseWorkspaceRange, resolveWorkspaceRange } from '../utils/workspace-range.js';
|
|
23
|
+
import { CiService } from './ci.service.js';
|
|
24
|
+
function preparePublishManifest(pkg, publishDir, packagesByName) {
|
|
25
|
+
if (path.resolve(publishDir) !== path.resolve(pkg.dirname)) {
|
|
26
|
+
const file = path.join(publishDir, 'package.json');
|
|
27
|
+
const previous = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : undefined;
|
|
28
|
+
fs.mkdirSync(publishDir, { recursive: true });
|
|
29
|
+
fs.writeFileSync(file, JSON.stringify(derivePublishManifest(pkg, packagesByName), undefined, 2) + '\n', 'utf-8');
|
|
30
|
+
return () => {
|
|
31
|
+
if (previous === undefined)
|
|
32
|
+
fs.rmSync(file, { force: true });
|
|
33
|
+
else
|
|
34
|
+
fs.writeFileSync(file, previous, 'utf-8');
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const hasWorkspaceRange = DEPENDENCY_KEYS.some(depKey => {
|
|
38
|
+
const deps = pkg.manifest.raw[depKey];
|
|
39
|
+
return deps && Object.values(deps).some(v => parseWorkspaceRange(v));
|
|
40
|
+
});
|
|
41
|
+
if (!hasWorkspaceRange)
|
|
42
|
+
return undefined;
|
|
43
|
+
/** The file's exact bytes, not a re-serialization: this is restored verbatim afterwards, so a
|
|
44
|
+
* round-trip through `JSON.stringify` would rewrite the author's formatting as a side effect of
|
|
45
|
+
* publishing. */
|
|
46
|
+
const original = fs.readFileSync(pkg.manifestFileName, 'utf-8');
|
|
47
|
+
resolveWorkspaceRanges(pkg.manifest.raw, packagesByName);
|
|
48
|
+
pkg.writeManifest();
|
|
49
|
+
return () => {
|
|
50
|
+
fs.writeFileSync(pkg.manifestFileName, original, 'utf-8');
|
|
51
|
+
pkg.reloadManifest();
|
|
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.manifest.raw);
|
|
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 viewVersion = deps.npmViewVersion ?? ((name, cwd) => npmViewVersion(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 viewVersion(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
|
+
/** The tuple is explicit: `[p.name, p]` widens to `(string | Package)[]`, and whether `new Map`
|
|
201
|
+
* still infers `Map<string, Package>` from that came down to which declaration of
|
|
202
|
+
* `getPackages()` was in scope - the source one inferred it, the built `.d.ts` did not
|
|
203
|
+
* (measured, once the tests started type-checking). */
|
|
204
|
+
const packagesByName = new Map(repository.getPackages().map(p => [p.name, p]));
|
|
205
|
+
for (const entry of plan) {
|
|
206
|
+
if (entry.status !== 'publish') {
|
|
207
|
+
result.push(entry);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const pkg = entry.package;
|
|
211
|
+
const blocker = pkg.dependencies.find(d => failed.has(d.name));
|
|
212
|
+
if (blocker) {
|
|
213
|
+
failed.add(pkg.name);
|
|
214
|
+
result.push({ ...entry, status: 'error', reason: `dependency "${blocker.name}" failed to publish` });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const publishDir = resolvePublishDir(pkg, options.contents);
|
|
218
|
+
const restore = preparePublishManifest(pkg, publishDir, packagesByName);
|
|
219
|
+
try {
|
|
220
|
+
await exec(buildPublishCommand(packageManager, options), { cwd: publishDir, stdio: 'inherit' });
|
|
221
|
+
result.push(entry);
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
failed.add(pkg.name);
|
|
225
|
+
result.push({ ...entry, status: 'error', reason: e.message });
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
restore?.();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
PublishService.applyPlan = applyPlan;
|
|
234
|
+
})(PublishService || (PublishService = {}));
|
|
235
|
+
/** Where the publishable output lives, most specific statement first: the package's own
|
|
236
|
+
* `publishConfig.directory` (npm/pnpm's native spelling, and a statement about that one package),
|
|
237
|
+
* then `.rmanrc "publish.directory"` (which a `"[*]"` block can say once for a whole repository
|
|
238
|
+
* instead of repeating in every `package.json`), then `--contents` for a single run. */
|
|
239
|
+
function resolvePublishDir(pkg, contentsOverride) {
|
|
240
|
+
const native = pkg.manifest.raw.publishConfig?.directory;
|
|
241
|
+
const configured = pkg.config?.publish?.directory;
|
|
242
|
+
const rel = (typeof native === 'string' && native) || configured || contentsOverride;
|
|
243
|
+
return rel ? path.resolve(pkg.dirname, rel) : pkg.dirname;
|
|
244
|
+
}
|
|
245
|
+
function buildPublishCommand(packageManager, options) {
|
|
246
|
+
const args = ['publish'];
|
|
247
|
+
if (options.access)
|
|
248
|
+
args.push('--access', options.access);
|
|
249
|
+
if (options.tag)
|
|
250
|
+
args.push('--tag', options.tag);
|
|
251
|
+
if (options.otp)
|
|
252
|
+
args.push('--otp', options.otp);
|
|
253
|
+
if (options.registry)
|
|
254
|
+
args.push('--registry', options.registry);
|
|
255
|
+
if (options.userconfig)
|
|
256
|
+
args.push('--userconfig', options.userconfig);
|
|
257
|
+
return `${packageManager} ${args.join(' ')}`;
|
|
258
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type GitHelper, type Package, VersionPlanService } from 'rman';
|
|
2
|
+
/**
|
|
3
|
+
* How a release is planned for an npm repository.
|
|
4
|
+
*
|
|
5
|
+
* rman's core holds the parts that are true of any repository - groups, conventional-commit
|
|
6
|
+
* severities, the cascade mechanics, the monorepo root's release identity - and leaves two decisions
|
|
7
|
+
* abstract because they are statements about an *ecosystem* rather than about releases. This is
|
|
8
|
+
* npm's pair of answers.
|
|
9
|
+
*
|
|
10
|
+
* Registered through the plugin's `versionPlanner`, so `rman version`/`rman changed` work in a
|
|
11
|
+
* repository that names `rman-node` and say what is missing in one that does not.
|
|
12
|
+
*/
|
|
13
|
+
export declare class NodeVersionPlanService extends VersionPlanService {
|
|
14
|
+
/**
|
|
15
|
+
* The shared `ChangeHashService.detect`, unmodified: this package's own latest release tag, and failing
|
|
16
|
+
* that whatever `ManifestProvider.publishedVersion` reports, mapped back onto a tag name.
|
|
17
|
+
*
|
|
18
|
+
* **Nothing npm-specific is passed in any more**, and that is the point of the seam moving: the
|
|
19
|
+
* registry lookup is `packageJsonManifest.publishedVersion`'s now, dispatched per package, so this
|
|
20
|
+
* override exists only because `detectBoundary` is abstract. If the core ever makes it a concrete
|
|
21
|
+
* default, this method can go entirely.
|
|
22
|
+
*/
|
|
23
|
+
protected detectBoundary(git: GitHelper, pkg: Package): Promise<string | undefined>;
|
|
24
|
+
/**
|
|
25
|
+
* npm's dependency ranges, read as a release policy. What decides each case is whether a
|
|
26
|
+
* dependent's **range floor** has to move for a consumer to get a correct install:
|
|
27
|
+
*
|
|
28
|
+
* - **patch** - only the changed packages. A dependent's `^1.2.0` already resolves to `1.2.1`,
|
|
29
|
+
* and a patch adds nothing for the dependent to require, so nothing downstream has to ship for
|
|
30
|
+
* consumers to receive it.
|
|
31
|
+
* - **minor** - also every transitive in-group dependent. A minor adds API; a dependent that uses
|
|
32
|
+
* it is only correct once its own published range requires the new floor, and a range lives in
|
|
33
|
+
* a manifest, which only a release puts on the registry.
|
|
34
|
+
* - **major** - the whole group, changed or not. A breaking change restates every member's
|
|
35
|
+
* compatibility, including the members that merely point at one.
|
|
36
|
+
*
|
|
37
|
+
* None of this is about versions, which is why it is not in the core: an ecosystem that pins exact
|
|
38
|
+
* versions instead has to release every dependent for a patch as well, and one that resolves
|
|
39
|
+
* dependencies from source may not need a release for any of it.
|
|
40
|
+
*/
|
|
41
|
+
protected cascade(bump: string): VersionPlanService.Cascade;
|
|
42
|
+
}
|
|
43
|
+
/** Registers the planner with rman. Called by the plugin entry point, once - and declared in the
|
|
44
|
+
* plugin object as well, since a plugin loaded through `plugins` should not need an import side
|
|
45
|
+
* effect to work. */
|
|
46
|
+
export declare function augmentVersionPlan(): void;
|
|
47
|
+
/** The instance the plugin registers. One is enough: a planner holds no per-run state, and a test
|
|
48
|
+
* wanting a different registry answer registers its own `ManifestProvider` instead - which
|
|
49
|
+
* exercises the real path rather than a bypass. */
|
|
50
|
+
export declare const nodeVersionPlanner: NodeVersionPlanService;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ChangeHashService, VersionPlanService } from 'rman';
|
|
2
|
+
/**
|
|
3
|
+
* How a release is planned for an npm repository.
|
|
4
|
+
*
|
|
5
|
+
* rman's core holds the parts that are true of any repository - groups, conventional-commit
|
|
6
|
+
* severities, the cascade mechanics, the monorepo root's release identity - and leaves two decisions
|
|
7
|
+
* abstract because they are statements about an *ecosystem* rather than about releases. This is
|
|
8
|
+
* npm's pair of answers.
|
|
9
|
+
*
|
|
10
|
+
* Registered through the plugin's `versionPlanner`, so `rman version`/`rman changed` work in a
|
|
11
|
+
* repository that names `rman-node` and say what is missing in one that does not.
|
|
12
|
+
*/
|
|
13
|
+
export class NodeVersionPlanService extends VersionPlanService {
|
|
14
|
+
/**
|
|
15
|
+
* The shared `ChangeHashService.detect`, unmodified: this package's own latest release tag, and failing
|
|
16
|
+
* that whatever `ManifestProvider.publishedVersion` reports, mapped back onto a tag name.
|
|
17
|
+
*
|
|
18
|
+
* **Nothing npm-specific is passed in any more**, and that is the point of the seam moving: the
|
|
19
|
+
* registry lookup is `packageJsonManifest.publishedVersion`'s now, dispatched per package, so this
|
|
20
|
+
* override exists only because `detectBoundary` is abstract. If the core ever makes it a concrete
|
|
21
|
+
* default, this method can go entirely.
|
|
22
|
+
*/
|
|
23
|
+
detectBoundary(git, pkg) {
|
|
24
|
+
return ChangeHashService.detect(git, pkg);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* npm's dependency ranges, read as a release policy. What decides each case is whether a
|
|
28
|
+
* dependent's **range floor** has to move for a consumer to get a correct install:
|
|
29
|
+
*
|
|
30
|
+
* - **patch** - only the changed packages. A dependent's `^1.2.0` already resolves to `1.2.1`,
|
|
31
|
+
* and a patch adds nothing for the dependent to require, so nothing downstream has to ship for
|
|
32
|
+
* consumers to receive it.
|
|
33
|
+
* - **minor** - also every transitive in-group dependent. A minor adds API; a dependent that uses
|
|
34
|
+
* it is only correct once its own published range requires the new floor, and a range lives in
|
|
35
|
+
* a manifest, which only a release puts on the registry.
|
|
36
|
+
* - **major** - the whole group, changed or not. A breaking change restates every member's
|
|
37
|
+
* compatibility, including the members that merely point at one.
|
|
38
|
+
*
|
|
39
|
+
* None of this is about versions, which is why it is not in the core: an ecosystem that pins exact
|
|
40
|
+
* versions instead has to release every dependent for a patch as well, and one that resolves
|
|
41
|
+
* dependencies from source may not need a release for any of it.
|
|
42
|
+
*/
|
|
43
|
+
cascade(bump) {
|
|
44
|
+
/** Only semver's three can arrive - `getPlan` validates against `bumpNames` and
|
|
45
|
+
* `packageJsonManifest` leaves the scheme at the semver default. The fallback is for a scheme
|
|
46
|
+
* someone swaps in underneath this planner, and it over-reaches on purpose: releasing a package
|
|
47
|
+
* that did not need it is noise, while under-reaching ships a dependent whose published range
|
|
48
|
+
* floor is wrong, which is a broken install. */
|
|
49
|
+
return CASCADE_BY_BUMP[bump] ?? 'group';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Registers the planner with rman. Called by the plugin entry point, once - and declared in the
|
|
53
|
+
* plugin object as well, since a plugin loaded through `plugins` should not need an import side
|
|
54
|
+
* effect to work. */
|
|
55
|
+
export function augmentVersionPlan() {
|
|
56
|
+
VersionPlanService.setPlanner(nodeVersionPlanner);
|
|
57
|
+
}
|
|
58
|
+
/** The instance the plugin registers. One is enough: a planner holds no per-run state, and a test
|
|
59
|
+
* wanting a different registry answer registers its own `ManifestProvider` instead - which
|
|
60
|
+
* exercises the real path rather than a bypass. */
|
|
61
|
+
export const nodeVersionPlanner = new NodeVersionPlanService();
|
|
62
|
+
/** semver's bump names against how far each has to reach - see `NodeVersionPlanService.cascade`. */
|
|
63
|
+
const CASCADE_BY_BUMP = {
|
|
64
|
+
patch: 'changed',
|
|
65
|
+
minor: 'dependents',
|
|
66
|
+
major: 'group',
|
|
67
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BinPath } from 'rman';
|
|
2
|
+
/**
|
|
3
|
+
* Where npm puts a repository's locally installed executables: `node_modules/.bin`, at **every**
|
|
4
|
+
* level from `cwd` up to the filesystem root - which is how npm itself resolves a binary, so a
|
|
5
|
+
* package's `eslint` is found whether it was installed in that package or hoisted to the workspace
|
|
6
|
+
* root.
|
|
7
|
+
*
|
|
8
|
+
* Adapted from [npm-run-path](https://github.com/sindresorhus/npm-run-path), and it used to sit in
|
|
9
|
+
* rman's core. It is npm's directory layout from end to end: a Cargo or Go repository has no
|
|
10
|
+
* `node_modules` to walk, and nothing here would ever fire for it.
|
|
11
|
+
*
|
|
12
|
+
* **The running `node`'s own directory goes last**, after the walk, and its position is
|
|
13
|
+
* load-bearing. It is there so a script calling `node` gets the interpreter rman itself runs on
|
|
14
|
+
* rather than whatever the shell would pick. It also puts rman's own bin directory ahead of the
|
|
15
|
+
* inherited PATH, which is a measured trap: a nested `rman` invocation inside a `run` script
|
|
16
|
+
* resolves to the globally installed one, not to the repository's. Shim it in
|
|
17
|
+
* `<root>/node_modules/.bin` when that has to be overridden - the walk above reaches there first.
|
|
18
|
+
*/
|
|
19
|
+
export declare const npmBinPaths: BinPath.Provider;
|
|
20
|
+
/** Registers the provider with rman. Called by the plugin entry point, once. */
|
|
21
|
+
export declare function augmentBinPath(): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { BinPath } from 'rman';
|
|
4
|
+
/**
|
|
5
|
+
* Where npm puts a repository's locally installed executables: `node_modules/.bin`, at **every**
|
|
6
|
+
* level from `cwd` up to the filesystem root - which is how npm itself resolves a binary, so a
|
|
7
|
+
* package's `eslint` is found whether it was installed in that package or hoisted to the workspace
|
|
8
|
+
* root.
|
|
9
|
+
*
|
|
10
|
+
* Adapted from [npm-run-path](https://github.com/sindresorhus/npm-run-path), and it used to sit in
|
|
11
|
+
* rman's core. It is npm's directory layout from end to end: a Cargo or Go repository has no
|
|
12
|
+
* `node_modules` to walk, and nothing here would ever fire for it.
|
|
13
|
+
*
|
|
14
|
+
* **The running `node`'s own directory goes last**, after the walk, and its position is
|
|
15
|
+
* load-bearing. It is there so a script calling `node` gets the interpreter rman itself runs on
|
|
16
|
+
* rather than whatever the shell would pick. It also puts rman's own bin directory ahead of the
|
|
17
|
+
* inherited PATH, which is a measured trap: a nested `rman` invocation inside a `run` script
|
|
18
|
+
* resolves to the globally installed one, not to the repository's. Shim it in
|
|
19
|
+
* `<root>/node_modules/.bin` when that has to be overridden - the walk above reaches there first.
|
|
20
|
+
*/
|
|
21
|
+
export const npmBinPaths = (cwd) => {
|
|
22
|
+
const result = [];
|
|
23
|
+
let previous;
|
|
24
|
+
let dir = path.resolve(cwd);
|
|
25
|
+
while (previous !== dir) {
|
|
26
|
+
result.push(path.join(dir, 'node_modules/.bin'));
|
|
27
|
+
previous = dir;
|
|
28
|
+
dir = path.resolve(dir, '..');
|
|
29
|
+
}
|
|
30
|
+
result.push(path.resolve(cwd, process.execPath, '..'));
|
|
31
|
+
return result;
|
|
32
|
+
};
|
|
33
|
+
/** Registers the provider with rman. Called by the plugin entry point, once. */
|
|
34
|
+
export function augmentBinPath() {
|
|
35
|
+
BinPath.addProvider(npmBinPaths);
|
|
36
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npm view <name> version` - `undefined` for anything that is not an answer (never published, no
|
|
3
|
+
* network, private or restricted with no access). Catch-everything on purpose: every caller treats
|
|
4
|
+
* "no answer" as a legitimate state rather than a failure.
|
|
5
|
+
*
|
|
6
|
+
* One copy, two callers with different needs: `publish` asks in order to decide whether this exact
|
|
7
|
+
* version is already out there, and can be pointed at another registry from the CLI
|
|
8
|
+
* (`--registry`/`--userconfig`); `packageJsonManifest.publishedVersion` asks so that
|
|
9
|
+
* `detectChangeHash` can guess a tag name, and passes nothing - a bare `npm view` already picks up
|
|
10
|
+
* the repository's own `.npmrc` from `cwd`, which is what that path wants.
|
|
11
|
+
*/
|
|
12
|
+
export declare function npmViewVersion(name: string, cwd: string, options?: {
|
|
13
|
+
registry?: string;
|
|
14
|
+
userconfig?: string;
|
|
15
|
+
}): Promise<string | undefined>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
/**
|
|
4
|
+
* `npm view <name> version` - `undefined` for anything that is not an answer (never published, no
|
|
5
|
+
* network, private or restricted with no access). Catch-everything on purpose: every caller treats
|
|
6
|
+
* "no answer" as a legitimate state rather than a failure.
|
|
7
|
+
*
|
|
8
|
+
* One copy, two callers with different needs: `publish` asks in order to decide whether this exact
|
|
9
|
+
* version is already out there, and can be pointed at another registry from the CLI
|
|
10
|
+
* (`--registry`/`--userconfig`); `packageJsonManifest.publishedVersion` asks so that
|
|
11
|
+
* `detectChangeHash` can guess a tag name, and passes nothing - a bare `npm view` already picks up
|
|
12
|
+
* the repository's own `.npmrc` from `cwd`, which is what that path wants.
|
|
13
|
+
*/
|
|
14
|
+
export async function npmViewVersion(name, cwd, options = {}) {
|
|
15
|
+
const argv = ['view', name, 'version'];
|
|
16
|
+
if (options.registry)
|
|
17
|
+
argv.push('--registry', options.registry);
|
|
18
|
+
if (options.userconfig)
|
|
19
|
+
argv.push('--userconfig', options.userconfig);
|
|
20
|
+
try {
|
|
21
|
+
const { stdout } = await execFileAsync('npm', argv, { cwd });
|
|
22
|
+
return stdout.trim() || undefined;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const execFileAsync = promisify(execFile);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `"workspace:"` dependency protocol - pnpm/yarn's spelling for "this dependency is a sibling in
|
|
3
|
+
* this repository", which npm's own workspaces understand too.
|
|
4
|
+
*
|
|
5
|
+
* **In `rman-node`, because it is a statement about a `package.json` dependency field.** It sat in
|
|
6
|
+
* rman's core with a comment admitting it was only there because `publish` needed it from out here -
|
|
7
|
+
* and now `publish`, the manifest provider and the dependency-range rewrite all live in this
|
|
8
|
+
* package, so nothing in the core ever looked at it.
|
|
9
|
+
*/
|
|
10
|
+
export interface ParsedWorkspaceRange {
|
|
11
|
+
/** `'*'`/`'^'`/`'~'` for the bare selector forms; `'explicit'` when the protocol is followed by
|
|
12
|
+
* a concrete semver version/range instead (e.g. `"workspace:^1.0.0"`, `"workspace:1.0.0"`). */
|
|
13
|
+
selector: '*' | '^' | '~' | 'explicit';
|
|
14
|
+
/** Only set when `selector === 'explicit'` - the literal range following `"workspace:"`. */
|
|
15
|
+
range?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Parses a dependency range value for the pnpm/yarn `"workspace:"` protocol - `undefined` when
|
|
18
|
+
* `value` isn't a workspace range at all (a plain semver range, or not a string). */
|
|
19
|
+
export declare function parseWorkspaceRange(value: unknown): ParsedWorkspaceRange | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Resolves a parsed workspace range against `version` (the dependency's actual current version)
|
|
22
|
+
* into the real range a registry consumer would need - the same substitution pnpm/yarn's own
|
|
23
|
+
* publish performs: `"*"` pins the exact version (no operator), `"^"`/`"~"` prepend themselves to
|
|
24
|
+
* it, and an explicit range is used verbatim (it was already a real range, just workspace-prefixed).
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveWorkspaceRange(parsed: ParsedWorkspaceRange, version: string): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Parses a dependency range value for the pnpm/yarn `"workspace:"` protocol - `undefined` when
|
|
2
|
+
* `value` isn't a workspace range at all (a plain semver range, or not a string). */
|
|
3
|
+
export function parseWorkspaceRange(value) {
|
|
4
|
+
if (typeof value !== 'string' || !value.startsWith('workspace:'))
|
|
5
|
+
return undefined;
|
|
6
|
+
const rest = value.slice('workspace:'.length);
|
|
7
|
+
if (rest === '*' || rest === '^' || rest === '~')
|
|
8
|
+
return { selector: rest };
|
|
9
|
+
return { selector: 'explicit', range: rest };
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolves a parsed workspace range against `version` (the dependency's actual current version)
|
|
13
|
+
* into the real range a registry consumer would need - the same substitution pnpm/yarn's own
|
|
14
|
+
* publish performs: `"*"` pins the exact version (no operator), `"^"`/`"~"` prepend themselves to
|
|
15
|
+
* it, and an explicit range is used verbatim (it was already a real range, just workspace-prefixed).
|
|
16
|
+
*/
|
|
17
|
+
export function resolveWorkspaceRange(parsed, version) {
|
|
18
|
+
switch (parsed.selector) {
|
|
19
|
+
case '*':
|
|
20
|
+
return version;
|
|
21
|
+
case '^':
|
|
22
|
+
return `^${version}`;
|
|
23
|
+
case '~':
|
|
24
|
+
return `~${version}`;
|
|
25
|
+
case 'explicit':
|
|
26
|
+
return parsed.range;
|
|
27
|
+
}
|
|
28
|
+
}
|