rman 0.38.0 → 1.0.2
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/README.md +371 -21
- package/cli.js +51 -29
- package/commands/build.command.d.ts +3 -0
- package/commands/build.command.js +14 -0
- package/commands/changed.command.d.ts +3 -0
- package/commands/changed.command.js +29 -0
- package/commands/changelog.command.d.ts +3 -0
- package/commands/changelog.command.js +63 -0
- package/commands/ci.command.d.ts +3 -0
- package/commands/ci.command.js +30 -0
- package/commands/clean.command.d.ts +3 -0
- package/commands/clean.command.js +36 -0
- package/commands/diff.command.d.ts +3 -0
- package/commands/diff.command.js +51 -0
- package/commands/exec.command.d.ts +3 -0
- package/commands/exec.command.js +83 -0
- package/commands/import.command.d.ts +3 -0
- package/commands/import.command.js +27 -0
- package/commands/info.command.d.ts +3 -0
- package/commands/info.command.js +61 -0
- package/commands/list.command.d.ts +3 -0
- package/commands/list.command.js +113 -0
- package/commands/publish.command.d.ts +3 -0
- package/commands/publish.command.js +147 -0
- package/commands/run.command.d.ts +6 -0
- package/commands/run.command.js +80 -0
- package/commands/test.command.d.ts +3 -0
- package/commands/test.command.js +14 -0
- package/commands/version.command.d.ts +3 -0
- package/commands/version.command.js +125 -0
- package/constants.js +1 -1
- package/core/config.d.ts +13 -0
- package/core/config.js +61 -0
- package/core/package.d.ts +2 -0
- package/core/package.js +2 -0
- package/core/repository.d.ts +36 -3
- package/core/repository.js +90 -45
- package/index.d.ts +17 -1
- package/index.js +15 -1
- package/package.json +8 -4
- package/rmanrc.schema.json +202 -0
- package/services/changelog.service.d.ts +99 -0
- package/services/changelog.service.js +272 -0
- package/services/ci.service.d.ts +40 -0
- package/services/ci.service.js +204 -0
- package/services/clean.service.d.ts +42 -0
- package/services/clean.service.js +226 -0
- package/services/exec.service.d.ts +35 -0
- package/services/exec.service.js +114 -0
- package/services/import.service.d.ts +30 -0
- package/services/import.service.js +84 -0
- package/services/list.service.d.ts +27 -0
- package/services/list.service.js +25 -0
- package/services/publish.service.d.ts +79 -0
- package/services/publish.service.js +199 -0
- package/services/run.service.d.ts +99 -0
- package/services/run.service.js +432 -0
- package/services/system-info.d.ts +16 -0
- package/services/system-info.js +25 -0
- package/services/version.service.d.ts +95 -0
- package/services/version.service.js +450 -0
- package/services.d.ts +10 -0
- package/services.js +10 -0
- package/utils/branch-guard.d.ts +25 -0
- package/utils/branch-guard.js +61 -0
- package/utils/change-hash.d.ts +55 -0
- package/utils/change-hash.js +80 -0
- package/utils/conventional-commits.d.ts +39 -0
- package/utils/conventional-commits.js +43 -0
- package/utils/exec.d.ts +5 -6
- package/utils/exec.js +45 -59
- package/utils/git.d.ts +86 -0
- package/utils/git.js +276 -0
- package/utils/live-region.d.ts +16 -0
- package/utils/live-region.js +59 -0
- package/utils/logger.d.ts +24 -0
- package/utils/logger.js +39 -0
- package/utils/npm-run-path.d.ts +1 -1
- package/utils/npm-run-path.js +10 -10
- package/utils/package-filter.d.ts +32 -0
- package/utils/package-filter.js +73 -0
- package/utils/progress-panel.d.ts +59 -0
- package/utils/progress-panel.js +149 -0
- package/utils/workspace-range.d.ts +17 -0
- package/utils/workspace-range.js +28 -0
- package/commands/build-command.d.ts +0 -11
- package/commands/build-command.js +0 -26
- package/commands/changed-command.d.ts +0 -16
- package/commands/changed-command.js +0 -35
- package/commands/ci-command.d.ts +0 -24
- package/commands/ci-command.js +0 -71
- package/commands/execute-command.d.ts +0 -19
- package/commands/execute-command.js +0 -75
- package/commands/info-command.d.ts +0 -10
- package/commands/info-command.js +0 -57
- package/commands/list-command.d.ts +0 -38
- package/commands/list-command.js +0 -137
- package/commands/multi-task-command.d.ts +0 -22
- package/commands/multi-task-command.js +0 -68
- package/commands/publish-command.d.ts +0 -24
- package/commands/publish-command.js +0 -135
- package/commands/run-command.d.ts +0 -28
- package/commands/run-command.js +0 -134
- package/commands/version-command.d.ts +0 -25
- package/commands/version-command.js +0 -198
- package/core/command.d.ts +0 -35
- package/core/command.js +0 -117
- package/core/constants.d.ts +0 -1
- package/core/constants.js +0 -2
- package/core/logger.d.ts +0 -12
- package/core/logger.js +0 -2
- package/utils/file-utils.d.ts +0 -4
- package/utils/file-utils.js +0 -29
- package/utils/git-utils.d.ts +0 -25
- package/utils/git-utils.js +0 -66
- package/utils/npm-utils.d.ts +0 -11
- package/utils/npm-utils.js +0 -33
- package/utils/package-not-found-error.d.ts +0 -2
- package/utils/package-not-found-error.js +0 -2
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { GitHelper } from '../utils/git.js';
|
|
5
|
+
export var ImportService;
|
|
6
|
+
(function (ImportService) {
|
|
7
|
+
/**
|
|
8
|
+
* Imports `sourcePath` (a local clone of some other git repository) as a new package under this
|
|
9
|
+
* repository, preserving its **entire commit history** - every original commit, author, date and
|
|
10
|
+
* message stays intact, just as if the package had always lived at its new path. Unlike copying
|
|
11
|
+
* the files over and making one "import" commit, `git blame`/`git log --follow` keep working on
|
|
12
|
+
* the imported files afterward.
|
|
13
|
+
*
|
|
14
|
+
* How: every commit reachable from `sourcePath`'s HEAD is turned into a patch (`git format-patch
|
|
15
|
+
* --root`, oldest first), each patch's file paths are rewritten to be prefixed with the new
|
|
16
|
+
* subdirectory, then replayed onto this repository via `git am` (preserving authorship). A
|
|
17
|
+
* source repo with merge commits or binary-file renames can still trip up a patch here or there -
|
|
18
|
+
* same caveat `lerna import` has, since both use this same replay approach rather than a real
|
|
19
|
+
* merge (which real `git subtree` would give, at the cost of far less predictable behavior
|
|
20
|
+
* across git versions - the reason this - and lerna - avoid it).
|
|
21
|
+
*/
|
|
22
|
+
async function importRepo(repository, sourcePath, options = {}) {
|
|
23
|
+
const absSource = path.resolve(sourcePath);
|
|
24
|
+
if (!fs.existsSync(path.join(absSource, '.git'))) {
|
|
25
|
+
throw new Error(`"${sourcePath}" is not a git repository (no .git found) - clone it locally first`);
|
|
26
|
+
}
|
|
27
|
+
const name = resolveSourceName(absSource);
|
|
28
|
+
const dirName = name.includes('/') ? name.slice(name.lastIndexOf('/') + 1) : name;
|
|
29
|
+
const destDir = options.dest ?? 'packages';
|
|
30
|
+
const targetDir = path.join(repository.dirname, destDir, dirName);
|
|
31
|
+
if (fs.existsSync(targetDir)) {
|
|
32
|
+
throw new Error(`"${path.relative(repository.dirname, targetDir)}" already exists`);
|
|
33
|
+
}
|
|
34
|
+
const sourceGit = new GitHelper({ cwd: absSource });
|
|
35
|
+
const patchDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rman-import-'));
|
|
36
|
+
try {
|
|
37
|
+
const patches = await sourceGit.formatPatches(patchDir);
|
|
38
|
+
if (!patches.length)
|
|
39
|
+
throw new Error(`"${sourcePath}" has no commits to import`);
|
|
40
|
+
const relTargetDir = path.relative(repository.dirname, targetDir).split(path.sep).join('/');
|
|
41
|
+
for (const patchFile of patches)
|
|
42
|
+
rewritePatchPaths(patchFile, relTargetDir);
|
|
43
|
+
const targetGit = new GitHelper({ cwd: repository.dirname });
|
|
44
|
+
await targetGit.applyPatches(patches);
|
|
45
|
+
return { name, targetDir, commitCount: patches.length };
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
fs.rmSync(patchDir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
ImportService.importRepo = importRepo;
|
|
52
|
+
})(ImportService || (ImportService = {}));
|
|
53
|
+
/** The source repo's own `package.json` name, or its directory's basename when it has none (or
|
|
54
|
+
* isn't even a package yet - a plain library repo being folded in). */
|
|
55
|
+
function resolveSourceName(absSource) {
|
|
56
|
+
const pkgJsonFile = path.join(absSource, 'package.json');
|
|
57
|
+
if (fs.existsSync(pkgJsonFile)) {
|
|
58
|
+
try {
|
|
59
|
+
const json = JSON.parse(fs.readFileSync(pkgJsonFile, 'utf-8'));
|
|
60
|
+
if (typeof json.name === 'string' && json.name)
|
|
61
|
+
return json.name;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// malformed package.json - fall through to the directory name below.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return path.basename(absSource);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Rewrites a `git format-patch`-produced file in place so every path it touches is prefixed with
|
|
71
|
+
* `prefix` (the new subdirectory, forward-slash-separated) - `diff --git`/`---`/`+++`/rename
|
|
72
|
+
* headers only; `--- /dev/null`/`+++ /dev/null` (new/deleted file markers) never match, since they
|
|
73
|
+
* have no `a/`/`b/` prefix to rewrite in the first place.
|
|
74
|
+
*/
|
|
75
|
+
function rewritePatchPaths(patchFile, prefix) {
|
|
76
|
+
const content = fs.readFileSync(patchFile, 'utf-8');
|
|
77
|
+
const rewritten = content
|
|
78
|
+
.replace(/^diff --git a\/(.+?) b\/(.+)$/gm, (_m, a, b) => `diff --git a/${prefix}/${a} b/${prefix}/${b}`)
|
|
79
|
+
.replace(/^--- a\/(.+)$/gm, (_m, p) => `--- a/${prefix}/${p}`)
|
|
80
|
+
.replace(/^\+\+\+ b\/(.+)$/gm, (_m, p) => `+++ b/${prefix}/${p}`)
|
|
81
|
+
.replace(/^rename from (.+)$/gm, (_m, p) => `rename from ${prefix}/${p}`)
|
|
82
|
+
.replace(/^rename to (.+)$/gm, (_m, p) => `rename to ${prefix}/${p}`);
|
|
83
|
+
fs.writeFileSync(patchFile, rewritten);
|
|
84
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Repository } from '../core/repository.js';
|
|
2
|
+
import { type PackageFilterOptions } from '../utils/package-filter.js';
|
|
3
|
+
export declare namespace ListService {
|
|
4
|
+
interface Options extends PackageFilterOptions {
|
|
5
|
+
/** Topological order (dependencies before dependents) instead of lexical by directory. */
|
|
6
|
+
toposort?: boolean;
|
|
7
|
+
/** Only include packages that have changed since the last publish (dirty or committed but
|
|
8
|
+
* not yet published) - or, with `changedSince`, since that specific commit/hash. */
|
|
9
|
+
changed?: boolean;
|
|
10
|
+
changedSince?: string;
|
|
11
|
+
}
|
|
12
|
+
interface Item {
|
|
13
|
+
name: string;
|
|
14
|
+
version: string;
|
|
15
|
+
location: string;
|
|
16
|
+
private: boolean;
|
|
17
|
+
status: Repository.PackageStatus;
|
|
18
|
+
/** In-repo package names this one depends on - enough to build a dependency graph without a
|
|
19
|
+
* second call, e.g. `Object.fromEntries(items.map(i => [i.name, i.dependencies]))`. */
|
|
20
|
+
dependencies: string[];
|
|
21
|
+
}
|
|
22
|
+
/** `list`: every package in the repository (or, with `changed`/`changedSince`, only the ones
|
|
23
|
+
* that have actually changed), each with its version, location, private flag, and change
|
|
24
|
+
* status relative to upstream. Pure data - no console output; `rman list`'s own command
|
|
25
|
+
* decides how to present it (table, JSON, parseable, names only, or a dependency graph). */
|
|
26
|
+
function getPackages(repository: Repository, options?: Options): Promise<Item[]>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { filterPackages } from '../utils/package-filter.js';
|
|
3
|
+
export var ListService;
|
|
4
|
+
(function (ListService) {
|
|
5
|
+
/** `list`: every package in the repository (or, with `changed`/`changedSince`, only the ones
|
|
6
|
+
* that have actually changed), each with its version, location, private flag, and change
|
|
7
|
+
* status relative to upstream. Pure data - no console output; `rman list`'s own command
|
|
8
|
+
* decides how to present it (table, JSON, parseable, names only, or a dependency graph). */
|
|
9
|
+
async function getPackages(repository, options = {}) {
|
|
10
|
+
const packages = filterPackages(repository.getPackages({ toposort: options.toposort }), options);
|
|
11
|
+
const status = await repository.listStatus({ hash: options.changedSince });
|
|
12
|
+
let items = packages.map(p => ({
|
|
13
|
+
name: p.name,
|
|
14
|
+
version: p.version,
|
|
15
|
+
location: path.relative(repository.dirname, p.dirname) || '.',
|
|
16
|
+
private: p.isPrivate,
|
|
17
|
+
status: status[p.name],
|
|
18
|
+
dependencies: [...p.dependencies],
|
|
19
|
+
}));
|
|
20
|
+
if (options.changed || options.changedSince)
|
|
21
|
+
items = items.filter(it => it.status !== 'clean');
|
|
22
|
+
return items;
|
|
23
|
+
}
|
|
24
|
+
ListService.getPackages = getPackages;
|
|
25
|
+
})(ListService || (ListService = {}));
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { exec } from '../utils/exec.js';
|
|
6
|
+
import { GitHelper } from '../utils/git.js';
|
|
7
|
+
import { filterPackages } from '../utils/package-filter.js';
|
|
8
|
+
import { parseWorkspaceRange, resolveWorkspaceRange } from '../utils/workspace-range.js';
|
|
9
|
+
import { CiService } from './ci.service.js';
|
|
10
|
+
import { DEPENDENCY_KEYS } from './version.service.js';
|
|
11
|
+
export var PublishService;
|
|
12
|
+
(function (PublishService) {
|
|
13
|
+
/**
|
|
14
|
+
* Computes what `publish` *would* do, across every non-private package (topological order,
|
|
15
|
+
* dependencies before dependents) - never touches the registry to publish anything, just
|
|
16
|
+
* queries it to decide, so it's safe to call any time, including as the plan a bare `rman
|
|
17
|
+
* publish` shows before asking for confirmation.
|
|
18
|
+
*
|
|
19
|
+
* A `private: true` package is always `'skip'`ped outright. A package with uncommitted local
|
|
20
|
+
* changes is `'error'` (aborts the whole plan) unless `options.ignoreDirty` downgrades it to
|
|
21
|
+
* `'skip'` instead - same rule `version` uses, since publishing untracked local edits is worse
|
|
22
|
+
* than a bad commit. Otherwise, its currently-published registry version (via `npm view`,
|
|
23
|
+
* queried concurrently across every remaining package) decides the rest: identical to the local
|
|
24
|
+
* `package.json` version is `'up-to-date'`; anything else (including never having been
|
|
25
|
+
* published at all) is `'publish'`.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately decoupled from `version`: this only ever looks at what's *currently* on disk and
|
|
28
|
+
* on the registry, never at whether `version` was just run - so it works equally well right
|
|
29
|
+
* after a version bump, or standing alone in a release pipeline that bumped days earlier.
|
|
30
|
+
*
|
|
31
|
+
* A monorepo's root package is never a candidate at all - `repository.getPackages()` already
|
|
32
|
+
* excludes it for a real monorepo (it only doubles as "the" package in a single-package repo,
|
|
33
|
+
* where it's a normal candidate like any other).
|
|
34
|
+
*/
|
|
35
|
+
async function getPlan(repository, options = {}, deps = {}) {
|
|
36
|
+
const git = new GitHelper({ cwd: repository.dirname });
|
|
37
|
+
const packages = filterPackages(repository.getPackages({ toposort: true }), options);
|
|
38
|
+
const dirtyFiles = await git.listDirtyFiles({ absolute: true });
|
|
39
|
+
const isDirty = (pkg) => dirtyFiles.some(f => !path.relative(pkg.dirname, f).startsWith('..'));
|
|
40
|
+
const npmViewVersion = deps.npmViewVersion ?? ((name, cwd) => defaultNpmViewVersion(name, cwd, options));
|
|
41
|
+
const entries = new Map();
|
|
42
|
+
const toCheck = [];
|
|
43
|
+
for (const pkg of packages) {
|
|
44
|
+
if (pkg.isPrivate) {
|
|
45
|
+
entries.set(pkg.name, { package: pkg, version: pkg.version, status: 'skip', reason: 'private package' });
|
|
46
|
+
}
|
|
47
|
+
else if (isDirty(pkg)) {
|
|
48
|
+
entries.set(pkg.name, {
|
|
49
|
+
package: pkg,
|
|
50
|
+
version: pkg.version,
|
|
51
|
+
status: options.ignoreDirty ? 'skip' : 'error',
|
|
52
|
+
reason: 'uncommitted local changes',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
toCheck.push(pkg);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
await Promise.all(toCheck.map(async (pkg) => {
|
|
60
|
+
const registryVersion = await npmViewVersion(pkg.name, pkg.dirname);
|
|
61
|
+
entries.set(pkg.name, {
|
|
62
|
+
package: pkg,
|
|
63
|
+
version: pkg.version,
|
|
64
|
+
registryVersion,
|
|
65
|
+
status: registryVersion === pkg.version ? 'up-to-date' : 'publish',
|
|
66
|
+
reason: registryVersion ? `registry has ${registryVersion}` : 'never published',
|
|
67
|
+
});
|
|
68
|
+
}));
|
|
69
|
+
return packages.map(pkg => entries.get(pkg.name));
|
|
70
|
+
}
|
|
71
|
+
PublishService.getPlan = getPlan;
|
|
72
|
+
/**
|
|
73
|
+
* Publishes every `'publish'` entry in `plan`, topological order (already `plan`'s own order -
|
|
74
|
+
* see `getPlan`), via the configured `packageManager`'s own `publish` command. Sequential, not
|
|
75
|
+
* concurrent: unlike `run`/`build`, a package genuinely needs its own dependencies to have
|
|
76
|
+
* landed on the registry first, and publishing is rare enough (once per release) that the
|
|
77
|
+
* simplicity is worth more than the parallelism `run` gets from `power-tasks`.
|
|
78
|
+
*
|
|
79
|
+
* If a package fails, every other still-pending entry depending on it (transitively) is marked
|
|
80
|
+
* `'error'` too and never attempted - publishing a package whose own new dependency range points
|
|
81
|
+
* at a version that never actually reached the registry would hand consumers a broken install.
|
|
82
|
+
* A package's own failure doesn't stop unrelated packages elsewhere in the plan, though.
|
|
83
|
+
*/
|
|
84
|
+
async function applyPlan(repository, plan, options = {}) {
|
|
85
|
+
const packageManager = CiService.resolvePackageManager(repository, options.packageManager);
|
|
86
|
+
const failed = new Set();
|
|
87
|
+
const result = [];
|
|
88
|
+
const packagesByName = new Map(repository.getPackages().map(p => [p.name, p]));
|
|
89
|
+
for (const entry of plan) {
|
|
90
|
+
if (entry.status !== 'publish') {
|
|
91
|
+
result.push(entry);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const pkg = entry.package;
|
|
95
|
+
const blocker = pkg.dependencies.find(d => failed.has(d));
|
|
96
|
+
if (blocker) {
|
|
97
|
+
failed.add(pkg.name);
|
|
98
|
+
result.push({ ...entry, status: 'error', reason: `dependency "${blocker}" failed to publish` });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const restore = rewriteWorkspaceRangesForPublish(pkg, packagesByName);
|
|
102
|
+
try {
|
|
103
|
+
await exec(buildPublishCommand(packageManager, options), {
|
|
104
|
+
cwd: resolvePublishDir(pkg, options.contents),
|
|
105
|
+
stdio: 'inherit',
|
|
106
|
+
});
|
|
107
|
+
result.push(entry);
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
failed.add(pkg.name);
|
|
111
|
+
result.push({ ...entry, status: 'error', reason: e.message });
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
restore?.();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
PublishService.applyPlan = applyPlan;
|
|
120
|
+
})(PublishService || (PublishService = {}));
|
|
121
|
+
const execFileAsync = promisify(execFile);
|
|
122
|
+
/** `npm view <name> version`, optionally against a custom registry/`.npmrc` - `undefined` for any
|
|
123
|
+
* failure (never published, no network, private/restricted with no access, ...), same
|
|
124
|
+
* catch-everything shape as `change-hash.ts`'s own `defaultNpmViewVersion` (kept separate here
|
|
125
|
+
* since this one also needs `--registry`/`--userconfig`, which that one has no use for). */
|
|
126
|
+
async function defaultNpmViewVersion(name, cwd, options) {
|
|
127
|
+
const argv = ['view', name, 'version'];
|
|
128
|
+
if (options.registry)
|
|
129
|
+
argv.push('--registry', options.registry);
|
|
130
|
+
if (options.userconfig)
|
|
131
|
+
argv.push('--userconfig', options.userconfig);
|
|
132
|
+
try {
|
|
133
|
+
const { stdout } = await execFileAsync('npm', argv, { cwd });
|
|
134
|
+
return stdout.trim() || undefined;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** npm's own native `publishConfig.directory` (if the package declares one) always wins over
|
|
141
|
+
* `options.contents` - the package's own package.json is the more authoritative, persistent
|
|
142
|
+
* statement of "this is where the publishable output lives", `--contents` is just a fallback for
|
|
143
|
+
* when it doesn't declare one at all. */
|
|
144
|
+
function resolvePublishDir(pkg, contentsOverride) {
|
|
145
|
+
const native = pkg.json.publishConfig?.directory;
|
|
146
|
+
const rel = (typeof native === 'string' && native) || contentsOverride;
|
|
147
|
+
return rel ? path.resolve(pkg.dirname, rel) : pkg.dirname;
|
|
148
|
+
}
|
|
149
|
+
function buildPublishCommand(packageManager, options) {
|
|
150
|
+
const args = ['publish'];
|
|
151
|
+
if (options.access)
|
|
152
|
+
args.push('--access', options.access);
|
|
153
|
+
if (options.tag)
|
|
154
|
+
args.push('--tag', options.tag);
|
|
155
|
+
if (options.otp)
|
|
156
|
+
args.push('--otp', options.otp);
|
|
157
|
+
if (options.registry)
|
|
158
|
+
args.push('--registry', options.registry);
|
|
159
|
+
if (options.userconfig)
|
|
160
|
+
args.push('--userconfig', options.userconfig);
|
|
161
|
+
return `${packageManager} ${args.join(' ')}`;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Rewrites every `"workspace:"` dependency range in `pkg`'s `package.json` to a real,
|
|
165
|
+
* registry-publishable range (the same substitution pnpm/yarn's own publish performs - see
|
|
166
|
+
* `resolveWorkspaceRange`), just before shelling out to `npm publish` - which reads whatever is
|
|
167
|
+
* actually on disk, unlike pnpm/yarn's own publish this never packs into a staging tarball first.
|
|
168
|
+
* Returns a function that restores the file's original bytes (and `pkg`'s in-memory state)
|
|
169
|
+
* verbatim; always invoke it from a `finally`, so a failed publish never leaves a rewritten
|
|
170
|
+
* `package.json` behind. Returns `undefined` (nothing to restore, no disk write at all) when `pkg`
|
|
171
|
+
* has no `"workspace:"` ranges to begin with.
|
|
172
|
+
*/
|
|
173
|
+
function rewriteWorkspaceRangesForPublish(pkg, packagesByName) {
|
|
174
|
+
const hasWorkspaceRange = DEPENDENCY_KEYS.some(depKey => {
|
|
175
|
+
const deps = pkg.json[depKey];
|
|
176
|
+
return deps && Object.values(deps).some(v => parseWorkspaceRange(v));
|
|
177
|
+
});
|
|
178
|
+
if (!hasWorkspaceRange)
|
|
179
|
+
return undefined;
|
|
180
|
+
const original = fs.readFileSync(pkg.jsonFileName, 'utf-8');
|
|
181
|
+
for (const depKey of DEPENDENCY_KEYS) {
|
|
182
|
+
const deps = pkg.json[depKey];
|
|
183
|
+
if (!deps)
|
|
184
|
+
continue;
|
|
185
|
+
for (const depName of Object.keys(deps)) {
|
|
186
|
+
const parsed = parseWorkspaceRange(deps[depName]);
|
|
187
|
+
if (!parsed)
|
|
188
|
+
continue;
|
|
189
|
+
const depPkg = packagesByName.get(depName);
|
|
190
|
+
if (depPkg)
|
|
191
|
+
deps[depName] = resolveWorkspaceRange(parsed, depPkg.version);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
pkg.writeJson();
|
|
195
|
+
return () => {
|
|
196
|
+
fs.writeFileSync(pkg.jsonFileName, original, 'utf-8');
|
|
197
|
+
pkg.reloadJson();
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Package } from '../core/package.js';
|
|
2
|
+
import type { Repository } from '../core/repository.js';
|
|
3
|
+
import { type LogLevel } from '../utils/logger.js';
|
|
4
|
+
import { type PackageFilterOptions } from '../utils/package-filter.js';
|
|
5
|
+
export declare namespace RunService {
|
|
6
|
+
interface Options extends PackageFilterOptions {
|
|
7
|
+
/** Max packages built at once: `true`/omitted = CPU count, a number = that many, `false` = serial (1). */
|
|
8
|
+
parallel?: boolean | number;
|
|
9
|
+
/** Respect the package dependency graph: a package waits for its dependencies and is skipped
|
|
10
|
+
* if one fails. Default true (right for `build`). Set false for scripts like `lint`/`test`
|
|
11
|
+
* where packages are independent - order becomes alphabetical and one package's failure
|
|
12
|
+
* (or its dependency's) never skips another. */
|
|
13
|
+
topo?: boolean;
|
|
14
|
+
bail?: boolean;
|
|
15
|
+
changed?: boolean;
|
|
16
|
+
changedSince?: string;
|
|
17
|
+
/** Show the live progress panel. Default true; auto-disabled when stdout isn't a TTY. */
|
|
18
|
+
progress?: boolean;
|
|
19
|
+
/** Verbosity of the classic per-step log (only applies when the live panel is off). Falls back to
|
|
20
|
+
* the root's `.rmanrc logLevel`, then 'info' - see `resolveRootLogLevel`. */
|
|
21
|
+
logLevel?: LogLevel;
|
|
22
|
+
/** Run across the whole repository even when the current directory is inside a single package
|
|
23
|
+
* (which otherwise scopes the run to just that package, and skips the root's own pre/post
|
|
24
|
+
* bookend - see `Repository.currentPackage`). Has no effect when already at the repository
|
|
25
|
+
* root, or outside any known package. */
|
|
26
|
+
root?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A package's `.rmanrc` (cascaded) can configure `run.<script>.*` - e.g.
|
|
30
|
+
* run:
|
|
31
|
+
* build:
|
|
32
|
+
* concurrency: 2
|
|
33
|
+
* lint:
|
|
34
|
+
* topo: false
|
|
35
|
+
* bail: false
|
|
36
|
+
* and a package can opt itself out of a script entirely:
|
|
37
|
+
* run:
|
|
38
|
+
* build:
|
|
39
|
+
* skip: true
|
|
40
|
+
* or supply the command(s) to run when its own package.json doesn't define this script (or its
|
|
41
|
+
* pre/post hooks) at all - a single string, or an array to run several in sequence - see
|
|
42
|
+
* `getScriptSteps`:
|
|
43
|
+
* run:
|
|
44
|
+
* build:
|
|
45
|
+
* script: tsc -b
|
|
46
|
+
* preScript: [node ./generate.js, node ./validate.js]
|
|
47
|
+
* postScript: node ./copy-assets.js
|
|
48
|
+
* override: true # use these even if the package *does* define its own
|
|
49
|
+
*/
|
|
50
|
+
function getConfig(pkg: Package, script: string): Record<string, unknown>;
|
|
51
|
+
/**
|
|
52
|
+
* `.rmanrc` conditional execution, GitHub Actions-`if`-flavored but a small closed grammar
|
|
53
|
+
* instead of a full expression language (less to get wrong, still covers what's asked for) -
|
|
54
|
+
* atoms combined with `and`/`or` (`and` binds tighter, same as most languages) and `(...)`:
|
|
55
|
+
*
|
|
56
|
+
* run:
|
|
57
|
+
* build:
|
|
58
|
+
* if: changed # changed since the last publish
|
|
59
|
+
* test:
|
|
60
|
+
* if: changed = a1b2c3d # changed since a specific commit
|
|
61
|
+
* if: changed = {CHANGE_HASH} # {NAME} -> process.env.NAME first
|
|
62
|
+
* if: (changed or dirty) and not committed
|
|
63
|
+
*/
|
|
64
|
+
type IfNode = {
|
|
65
|
+
kind: 'atom';
|
|
66
|
+
name: string;
|
|
67
|
+
value?: string;
|
|
68
|
+
} | {
|
|
69
|
+
kind: 'not';
|
|
70
|
+
node: IfNode;
|
|
71
|
+
} | {
|
|
72
|
+
kind: 'and';
|
|
73
|
+
left: IfNode;
|
|
74
|
+
right: IfNode;
|
|
75
|
+
} | {
|
|
76
|
+
kind: 'or';
|
|
77
|
+
left: IfNode;
|
|
78
|
+
right: IfNode;
|
|
79
|
+
};
|
|
80
|
+
/** Recursive-descent parser over `tokenizeIf`'s output: expr := or ; or := and ('or' and)* ;
|
|
81
|
+
* and := unary ('and' unary)* ; unary := 'not' unary | GROUP | NAME ['=' VALUE] */
|
|
82
|
+
function parseIfExpr(raw: unknown): IfNode | undefined;
|
|
83
|
+
/** Evaluates a parsed `if` expression for one package. `statusCache` avoids repeat `git` calls
|
|
84
|
+
* for the same reference hash across packages/scripts in a single run. */
|
|
85
|
+
function evaluateIf(repository: Repository, pkg: Package, node: IfNode, statusCache: Map<string, Record<string, Repository.PackageStatus>>): Promise<boolean>;
|
|
86
|
+
function runScript(repository: Repository, script: string, options?: Options & {
|
|
87
|
+
commandName?: string;
|
|
88
|
+
}): Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
/** Resolution order: explicit CLI flag > the package's resolved `.rmanrc` > `fallback`. */
|
|
91
|
+
export declare function resolveBool(cliValue: boolean | undefined, pkg: Package, script: string, key: string, fallback: boolean): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Bail is the one setting where a package's own `.rmanrc` outranks even an explicit CLI flag:
|
|
94
|
+
* "this package's failure must always stop the batch" is a more specific, intentional statement
|
|
95
|
+
* than a broad `--no-bail` meant for the run as a whole, and shouldn't be silently overridable by it.
|
|
96
|
+
*/
|
|
97
|
+
export declare function resolveBail(cliValue: boolean | undefined, pkg: Package, script: string, fallback: boolean): boolean;
|
|
98
|
+
export declare function resolveNumber(cliValue: number | undefined, pkg: Package, script: string, key: string, fallback: number): number;
|
|
99
|
+
export declare function resolveLogLevel(cliValue: LogLevel | undefined, pkg: Package, script: string, fallback: LogLevel): LogLevel;
|