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,204 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import colors from 'ansi-colors';
|
|
4
|
+
import { exec } from '../utils/exec.js';
|
|
5
|
+
import { Logger, resolveRootLogLevel } from '../utils/logger.js';
|
|
6
|
+
import { filterPackages } from '../utils/package-filter.js';
|
|
7
|
+
import { formatDuration, ProgressPanel } from '../utils/progress-panel.js';
|
|
8
|
+
export var CiService;
|
|
9
|
+
(function (CiService) {
|
|
10
|
+
/** `.rmanrc packageManager` (root only) picks the package manager used for the final install;
|
|
11
|
+
* explicit CLI value wins over it. Defaults to 'npm'. */
|
|
12
|
+
function resolvePackageManager(repository, cliValue) {
|
|
13
|
+
if (cliValue)
|
|
14
|
+
return cliValue;
|
|
15
|
+
const configured = repository.config?.packageManager;
|
|
16
|
+
if (configured === undefined)
|
|
17
|
+
return 'npm';
|
|
18
|
+
if (PACKAGE_MANAGERS.includes(configured))
|
|
19
|
+
return configured;
|
|
20
|
+
throw new Error(`Invalid "packageManager" in .rmanrc: "${configured}" (expected one of: ${PACKAGE_MANAGERS.join(', ')})`);
|
|
21
|
+
}
|
|
22
|
+
CiService.resolvePackageManager = resolvePackageManager;
|
|
23
|
+
/** Deletes `node_modules` and any known lockfile directly under `dirname`. Returns the names
|
|
24
|
+
* that actually existed (and were removed), so the caller can log only those. */
|
|
25
|
+
async function wipe(dirname) {
|
|
26
|
+
const removed = [];
|
|
27
|
+
for (const name of ['node_modules', ...LOCK_FILES]) {
|
|
28
|
+
const target = path.join(dirname, name);
|
|
29
|
+
const existed = await fs
|
|
30
|
+
.access(target)
|
|
31
|
+
.then(() => true)
|
|
32
|
+
.catch(() => false);
|
|
33
|
+
if (!existed)
|
|
34
|
+
continue;
|
|
35
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
36
|
+
removed.push(name);
|
|
37
|
+
}
|
|
38
|
+
return removed;
|
|
39
|
+
}
|
|
40
|
+
CiService.wipe = wipe;
|
|
41
|
+
/**
|
|
42
|
+
* `ci`: a from-scratch, reproducible install for CI pipelines. For every package (root
|
|
43
|
+
* included), deletes `node_modules` and any lockfile - or, if the package defines its own
|
|
44
|
+
* `"ci"` script, runs that instead. Once every package is clean, installs once at the root
|
|
45
|
+
* with the configured package manager (`npm`/`yarn`/`pnpm`/`bun`).
|
|
46
|
+
*
|
|
47
|
+
* Uses the same live progress panel as `run`/`build` (see `../utils/progress-panel.ts`) while it
|
|
48
|
+
* runs, falling back to a plain rmdir/clean/run/install log line per step when the panel is off.
|
|
49
|
+
*
|
|
50
|
+
* Unlike `run`/`build`, it does *not* end with a per-package success tally: `ci`'s packages don't
|
|
51
|
+
* have independently meaningful outcomes the way a build or test run does - wiping a package is
|
|
52
|
+
* trivial and the one step that can genuinely fail, the install, is a single operation for the
|
|
53
|
+
* whole repository. Counting "N succeeded" across packages would just be noise, so only actual
|
|
54
|
+
* failures get called out (by name, with whatever output they produced), followed by one plain
|
|
55
|
+
* completed/failed line.
|
|
56
|
+
*/
|
|
57
|
+
async function reinstall(repository, options = {}) {
|
|
58
|
+
const packageManager = resolvePackageManager(repository, options.packageManager);
|
|
59
|
+
const logger = new Logger(options.logLevel ?? resolveRootLogLevel(repository));
|
|
60
|
+
// root is handled separately below - for a non-monorepo, getPackages() would otherwise
|
|
61
|
+
// include it a second time (it doubles as "the" package).
|
|
62
|
+
const packages = filterPackages(repository.getPackages().filter(p => p !== repository.rootPackage), options);
|
|
63
|
+
const progress = options.progress ?? true;
|
|
64
|
+
const panel = new ProgressPanel('CI', !!process.stdout.isTTY && progress);
|
|
65
|
+
const runStartedAt = Date.now();
|
|
66
|
+
panel.start();
|
|
67
|
+
const items = [];
|
|
68
|
+
let failed = false;
|
|
69
|
+
try {
|
|
70
|
+
const results = await Promise.allSettled(packages.map(pkg => {
|
|
71
|
+
const item = panel.addItem(pkg.name);
|
|
72
|
+
items.push(item);
|
|
73
|
+
return ciForPackage(pkg, item, panel.enabled, logger);
|
|
74
|
+
}));
|
|
75
|
+
if (results.some(r => r.status === 'rejected'))
|
|
76
|
+
failed = true;
|
|
77
|
+
const rootItem = panel.addItem('root');
|
|
78
|
+
items.push(rootItem);
|
|
79
|
+
try {
|
|
80
|
+
await ciForRoot(repository, rootItem, panel.enabled, packageManager, logger);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
failed = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
panel.stop();
|
|
88
|
+
}
|
|
89
|
+
for (const item of items) {
|
|
90
|
+
if (item.status !== 'failed')
|
|
91
|
+
continue;
|
|
92
|
+
logger.error(colors.red.bold('X'), item.name);
|
|
93
|
+
if (item.log.length)
|
|
94
|
+
logger.error(colors.red(item.log.join('\n')));
|
|
95
|
+
}
|
|
96
|
+
const totalElapsed = formatDuration(Date.now() - runStartedAt);
|
|
97
|
+
const summary = [failed ? colors.red('ci failed') : colors.green('ci completed'), colors.gray(`(${totalElapsed})`)];
|
|
98
|
+
if (failed)
|
|
99
|
+
logger.error(...summary);
|
|
100
|
+
else
|
|
101
|
+
logger.info(...summary);
|
|
102
|
+
if (failed) {
|
|
103
|
+
const err = new Error('"ci" failed');
|
|
104
|
+
err.logged = true;
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
CiService.reinstall = reinstall;
|
|
109
|
+
})(CiService || (CiService = {}));
|
|
110
|
+
const PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm', 'bun'];
|
|
111
|
+
const LOCK_FILES = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'bun.lock', 'bun.lockb'];
|
|
112
|
+
/** Runs a shell `command` in `cwd`, driving `item` for the live panel when it's on, or falling
|
|
113
|
+
* back to the plain "classic" log line `logLine` prints first - the same split `run`'s classic
|
|
114
|
+
* log uses. `logLine` is expected to go through a `Logger` (so it respects `--log-level`), not a
|
|
115
|
+
* raw `console.log`. */
|
|
116
|
+
async function runStep(item, panelEnabled, cwd, command, logLine) {
|
|
117
|
+
if (panelEnabled) {
|
|
118
|
+
await exec(command, {
|
|
119
|
+
cwd,
|
|
120
|
+
stdio: 'pipe',
|
|
121
|
+
onLine: line => {
|
|
122
|
+
item.log.push(line);
|
|
123
|
+
item.lastLine = line;
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
logLine();
|
|
129
|
+
await exec(command, { cwd, stdio: 'inherit' });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Logs (or reflects onto the live panel) the outcome of a `wipe()` call - including the "nothing
|
|
133
|
+
* to remove" case, which otherwise prints nothing at all in the classic log and can look like the
|
|
134
|
+
* package was never processed (most workspace layouts hoist deps to the root's `node_modules`,
|
|
135
|
+
* so an individual package legitimately has nothing of its own to wipe most of the time). */
|
|
136
|
+
function logWipeResult(item, panelEnabled, logger, label, removed) {
|
|
137
|
+
if (panelEnabled) {
|
|
138
|
+
item.lastLine = removed.length ? `removed ${removed.join(', ')}` : 'already clean';
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (!removed.length) {
|
|
142
|
+
logger.info(colors.gray('clean'), colors.cyan(label));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
for (const name of removed)
|
|
146
|
+
logger.info(colors.yellow('rmdir'), colors.cyan(label), name);
|
|
147
|
+
}
|
|
148
|
+
/** A package can opt out of the default wipe by defining its own `"ci"` npm script - if present,
|
|
149
|
+
* that runs instead of the wipe, same as any other rman script. */
|
|
150
|
+
async function ciForPackage(pkg, item, panelEnabled, logger) {
|
|
151
|
+
item.status = 'running';
|
|
152
|
+
item.startedAt = Date.now();
|
|
153
|
+
try {
|
|
154
|
+
const script = pkg.json.scripts?.ci;
|
|
155
|
+
if (typeof script === 'string' && script) {
|
|
156
|
+
item.currentStep = 'ci';
|
|
157
|
+
await runStep(item, panelEnabled, pkg.dirname, script, () => logger.info(colors.cyan('run'), colors.cyan(item.name), script));
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
item.currentStep = 'wipe';
|
|
161
|
+
const removed = await CiService.wipe(pkg.dirname);
|
|
162
|
+
logWipeResult(item, panelEnabled, logger, item.name, removed);
|
|
163
|
+
}
|
|
164
|
+
item.status = 'success';
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
item.status = 'failed';
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
item.finishedAt = Date.now();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** The root gets one extra step over a regular package once it isn't opting out with its own
|
|
175
|
+
* `"ci"` script: after its own wipe, it installs once for the whole repository. */
|
|
176
|
+
async function ciForRoot(repository, item, panelEnabled, packageManager, logger) {
|
|
177
|
+
item.status = 'running';
|
|
178
|
+
item.startedAt = Date.now();
|
|
179
|
+
try {
|
|
180
|
+
const script = repository.rootPackage.json.scripts?.ci;
|
|
181
|
+
if (typeof script === 'string' && script) {
|
|
182
|
+
item.currentStep = 'ci';
|
|
183
|
+
await runStep(item, panelEnabled, repository.dirname, script, () => logger.info(colors.cyan('run'), colors.cyan('root'), script));
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
item.stepsTotal = 2;
|
|
187
|
+
item.stepIndex = 0;
|
|
188
|
+
item.currentStep = 'wipe';
|
|
189
|
+
const removed = await CiService.wipe(repository.dirname);
|
|
190
|
+
logWipeResult(item, panelEnabled, logger, 'root', removed);
|
|
191
|
+
item.stepIndex = 1;
|
|
192
|
+
item.currentStep = 'install';
|
|
193
|
+
await runStep(item, panelEnabled, repository.dirname, `${packageManager} install`, () => logger.info(colors.cyan('install'), `Running "${packageManager} install"`));
|
|
194
|
+
}
|
|
195
|
+
item.status = 'success';
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
item.status = 'failed';
|
|
199
|
+
throw e;
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
item.finishedAt = Date.now();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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 ExecService {
|
|
5
|
+
interface Options extends PackageFilterOptions {
|
|
6
|
+
/** Max packages at once: `true`/omitted = CPU count, a number = that many, `false` = serial (1). */
|
|
7
|
+
parallel?: boolean | number;
|
|
8
|
+
/** Respect the package dependency graph: a package waits for its dependencies and is skipped
|
|
9
|
+
* if one fails. Default true. Set false to run in every matching package independently,
|
|
10
|
+
* alphabetically, regardless of the dependency graph. */
|
|
11
|
+
topo?: boolean;
|
|
12
|
+
bail?: boolean;
|
|
13
|
+
changed?: boolean;
|
|
14
|
+
changedSince?: string;
|
|
15
|
+
/** Show the live progress panel. Default true; auto-disabled when stdout isn't a TTY. */
|
|
16
|
+
progress?: boolean;
|
|
17
|
+
/** Verbosity of the classic per-package log (only applies when the live panel is off). Falls
|
|
18
|
+
* back to the root's `.rmanrc logLevel`, then 'info' - see `resolveRootLogLevel`. */
|
|
19
|
+
logLevel?: LogLevel;
|
|
20
|
+
/** Run across the whole repository even when the current directory is inside a single package
|
|
21
|
+
* (which otherwise scopes the run to just that package). Has no effect when already at the
|
|
22
|
+
* repository root, or outside any known package. */
|
|
23
|
+
root?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* `exec`: runs `command` (an arbitrary shell command, not an npm script) directly in every
|
|
27
|
+
* matching package's own directory - unlike `run`, there's no `package.json` script to resolve
|
|
28
|
+
* and no `pre<script>`/`post<script>` npm lifecycle convention to honor, since the command isn't
|
|
29
|
+
* tied to any script name at all. Everything else about how packages are selected and scheduled
|
|
30
|
+
* matches `run`: topological order and per-package bail by default, the same live progress panel
|
|
31
|
+
* (falling back to a classic one-line-per-package log when it's off), and the same
|
|
32
|
+
* `--scope`/`--ignore`/`--deps`/`--dependents`/`--changed` package filtering.
|
|
33
|
+
*/
|
|
34
|
+
function exec(repository: Repository, command: string, options?: Options): Promise<void>;
|
|
35
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import colors from 'ansi-colors';
|
|
3
|
+
import { Task } from 'power-tasks';
|
|
4
|
+
import { exec as execCommand } from '../utils/exec.js';
|
|
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 ExecService;
|
|
9
|
+
(function (ExecService) {
|
|
10
|
+
/**
|
|
11
|
+
* `exec`: runs `command` (an arbitrary shell command, not an npm script) directly in every
|
|
12
|
+
* matching package's own directory - unlike `run`, there's no `package.json` script to resolve
|
|
13
|
+
* and no `pre<script>`/`post<script>` npm lifecycle convention to honor, since the command isn't
|
|
14
|
+
* tied to any script name at all. Everything else about how packages are selected and scheduled
|
|
15
|
+
* matches `run`: topological order and per-package bail by default, the same live progress panel
|
|
16
|
+
* (falling back to a classic one-line-per-package log when it's off), and the same
|
|
17
|
+
* `--scope`/`--ignore`/`--deps`/`--dependents`/`--changed` package filtering.
|
|
18
|
+
*/
|
|
19
|
+
async function exec(repository, command, options = {}) {
|
|
20
|
+
const logLevelDefault = resolveRootLogLevel(repository);
|
|
21
|
+
const logger = new Logger(options.logLevel ?? logLevelDefault);
|
|
22
|
+
const cwdScope = options.root ? undefined : repository.currentPackage;
|
|
23
|
+
const topo = options.topo ?? true;
|
|
24
|
+
let packages = repository.getPackages({ toposort: topo, scope: cwdScope?.name });
|
|
25
|
+
if (!topo)
|
|
26
|
+
packages = [...packages].sort((a, b) => a.name.localeCompare(b.name));
|
|
27
|
+
packages = filterPackages(packages, options);
|
|
28
|
+
if (options.changed || options.changedSince) {
|
|
29
|
+
const status = await repository.listStatus({ hash: options.changedSince });
|
|
30
|
+
packages = packages.filter(p => status[p.name] !== 'clean');
|
|
31
|
+
}
|
|
32
|
+
if (!packages.length) {
|
|
33
|
+
logger.info(colors.gray('No package matched.'));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const concurrency = options.parallel === false
|
|
37
|
+
? 1
|
|
38
|
+
: typeof options.parallel === 'number'
|
|
39
|
+
? options.parallel
|
|
40
|
+
: options.parallel === true
|
|
41
|
+
? os.cpus().length
|
|
42
|
+
: os.cpus().length;
|
|
43
|
+
const progress = options.progress ?? true;
|
|
44
|
+
const panel = new ProgressPanel('EXEC', !!process.stdout.isTTY && progress);
|
|
45
|
+
const bail = options.bail ?? true;
|
|
46
|
+
let rootTask;
|
|
47
|
+
const names = new Set(packages.map(p => p.name));
|
|
48
|
+
const children = packages.map(pkg => {
|
|
49
|
+
const ctx = panel.addItem(pkg.name);
|
|
50
|
+
const dependencies = topo ? pkg.dependencies.filter(d => names.has(d)) : [];
|
|
51
|
+
return new Task(() => execForPackage(ctx, panel.enabled, pkg.dirname, command, logger, bail ? () => rootTask?.abort() : () => { }), { name: pkg.name, dependencies });
|
|
52
|
+
});
|
|
53
|
+
panel.start();
|
|
54
|
+
let failed = false;
|
|
55
|
+
try {
|
|
56
|
+
/** bail:false here - each item's own task decides whether to call `rootTask.abort()`
|
|
57
|
+
* itself (see `execForPackage`), mirroring `run`'s own reasoning: power-tasks' own `bail`
|
|
58
|
+
* is a single blanket policy, it can't be conditional on `options.bail`. `toPromise()`
|
|
59
|
+
* still rejects on any child's failure regardless, whether or not it triggered an abort. */
|
|
60
|
+
rootTask = new Task(children, { concurrency, bail: false });
|
|
61
|
+
await rootTask.toPromise();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
failed = true;
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
panel.stop();
|
|
68
|
+
}
|
|
69
|
+
panel.printSummary();
|
|
70
|
+
if (failed) {
|
|
71
|
+
const err = new Error('"exec" failed');
|
|
72
|
+
err.logged = true;
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
ExecService.exec = exec;
|
|
77
|
+
})(ExecService || (ExecService = {}));
|
|
78
|
+
async function execForPackage(ctx, panelEnabled, cwd, command, logger, abort) {
|
|
79
|
+
ctx.status = 'running';
|
|
80
|
+
ctx.startedAt = Date.now();
|
|
81
|
+
try {
|
|
82
|
+
if (panelEnabled) {
|
|
83
|
+
await execCommand(command, {
|
|
84
|
+
cwd,
|
|
85
|
+
stdio: 'pipe',
|
|
86
|
+
onLine: line => {
|
|
87
|
+
ctx.log.push(line);
|
|
88
|
+
ctx.lastLine = line;
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
logger.info(colors.cyan('exec'), colors.cyan(ctx.name), command);
|
|
94
|
+
const t = Date.now();
|
|
95
|
+
try {
|
|
96
|
+
await execCommand(command, { cwd, stdio: 'inherit' });
|
|
97
|
+
logger.info(colors.green('success'), colors.cyan(ctx.name), colors.yellow(`(${Date.now() - t} ms)`));
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
logger.error(colors.red('failed'), colors.cyan(ctx.name), colors.yellow(`(${Date.now() - t} ms)`));
|
|
101
|
+
throw e;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
ctx.status = 'success';
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
ctx.status = 'failed';
|
|
108
|
+
abort();
|
|
109
|
+
throw e;
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
ctx.finishedAt = Date.now();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Repository } from '../core/repository.js';
|
|
2
|
+
export declare namespace ImportService {
|
|
3
|
+
interface Options {
|
|
4
|
+
/** Subdirectory (relative to the repository root) the new package is placed under - default `'packages'`. */
|
|
5
|
+
dest?: string;
|
|
6
|
+
}
|
|
7
|
+
interface Result {
|
|
8
|
+
/** The imported package's name (from its own `package.json`, or its directory basename). */
|
|
9
|
+
name: string;
|
|
10
|
+
/** Where it was placed, absolute. */
|
|
11
|
+
targetDir: string;
|
|
12
|
+
commitCount: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Imports `sourcePath` (a local clone of some other git repository) as a new package under this
|
|
16
|
+
* repository, preserving its **entire commit history** - every original commit, author, date and
|
|
17
|
+
* message stays intact, just as if the package had always lived at its new path. Unlike copying
|
|
18
|
+
* the files over and making one "import" commit, `git blame`/`git log --follow` keep working on
|
|
19
|
+
* the imported files afterward.
|
|
20
|
+
*
|
|
21
|
+
* How: every commit reachable from `sourcePath`'s HEAD is turned into a patch (`git format-patch
|
|
22
|
+
* --root`, oldest first), each patch's file paths are rewritten to be prefixed with the new
|
|
23
|
+
* subdirectory, then replayed onto this repository via `git am` (preserving authorship). A
|
|
24
|
+
* source repo with merge commits or binary-file renames can still trip up a patch here or there -
|
|
25
|
+
* same caveat `lerna import` has, since both use this same replay approach rather than a real
|
|
26
|
+
* merge (which real `git subtree` would give, at the cost of far less predictable behavior
|
|
27
|
+
* across git versions - the reason this - and lerna - avoid it).
|
|
28
|
+
*/
|
|
29
|
+
function importRepo(repository: Repository, sourcePath: string, options?: Options): Promise<Result>;
|
|
30
|
+
}
|