@williamthorsen/nmr 1.0.0 → 2.0.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/.meta/changelog.json +104 -0
- package/.readyup/kits/default.js +12 -28
- package/.readyup/manifest.json +19 -24
- package/CHANGELOG.md +33 -0
- package/README.md +9 -3
- package/dist/esm/check-cache-gate.d.ts +0 -3
- package/dist/esm/check-cache-gate.js +1 -5
- package/dist/esm/cli-build.js +3 -10
- package/dist/esm/cli-clean.js +1 -2
- package/dist/esm/cli-report-catalog.js +2 -3
- package/dist/esm/cli-report-overrides.js +3 -5
- package/dist/esm/commands/build-output.d.ts +7 -32
- package/dist/esm/commands/build-output.js +8 -13
- package/dist/esm/commands/build-report.d.ts +6 -0
- package/dist/esm/commands/build-report.js +21 -0
- package/dist/esm/commands/build.d.ts +27 -4
- package/dist/esm/commands/build.js +24 -86
- package/dist/esm/commands/clean-report.d.ts +6 -0
- package/dist/esm/commands/clean-report.js +20 -0
- package/dist/esm/commands/clean.d.ts +39 -7
- package/dist/esm/commands/clean.js +57 -34
- package/dist/esm/commands/compile.d.ts +5 -0
- package/dist/esm/commands/compile.js +32 -0
- package/dist/esm/commands/fmt.d.ts +11 -0
- package/dist/esm/commands/fmt.js +6 -2
- package/dist/esm/commands/publish-output.d.ts +40 -0
- package/dist/esm/commands/publish-output.js +66 -0
- package/dist/esm/commands/report-catalog.d.ts +5 -0
- package/dist/esm/commands/report-catalog.js +19 -0
- package/dist/esm/commands/report-overrides.d.ts +5 -0
- package/dist/esm/commands/report-overrides.js +20 -1
- package/dist/esm/config-validation.js +1 -14
- package/dist/esm/config.d.ts +1 -1
- package/dist/esm/crossing-diagnostics.d.ts +1 -1
- package/dist/esm/resolver.d.ts +0 -12
- package/dist/esm/resolver.js +0 -35
- package/dist/esm/run-chain.d.ts +1 -1
- package/dist/esm/run-chain.js +1 -5
- package/dist/esm/types.d.ts +0 -1
- package/docs/check-cache.md +0 -1
- package/docs/scripts.md +17 -12
- package/docs/upgrades.md +1 -1
- package/docs/utilities.md +2 -2
- package/package.json +8 -8
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
2
1
|
import type * as TypeScript from 'typescript';
|
|
3
2
|
import { type BuildOptions } from './build-output.js';
|
|
3
|
+
/**
|
|
4
|
+
* How a build ended: skipped because its inputs and output were unchanged, with the emit time recorded for those
|
|
5
|
+
* inputs when one was; or emitted, with the count of files that it published to `outdir`.
|
|
6
|
+
*/
|
|
7
|
+
export type BuildOutcome = {
|
|
8
|
+
kind: 'unchanged';
|
|
9
|
+
savedMs?: number;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'emitted';
|
|
12
|
+
fileCount: number;
|
|
13
|
+
outdir: string;
|
|
14
|
+
};
|
|
15
|
+
/** Why a build is about to emit: its inputs changed, or its inputs match the cache and its output is missing. */
|
|
16
|
+
export type BuildProgress = {
|
|
17
|
+
kind: 'inputs-changed';
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'output-missing';
|
|
20
|
+
};
|
|
4
21
|
/**
|
|
5
22
|
* The toolchain from which a build's emit comes: the resolved compiler and the nmr running it. `computeBuildHash`
|
|
6
23
|
* folds both into the build hash, because the same sources can compile to different output under a different
|
|
@@ -10,9 +27,10 @@ export interface BuildToolchain {
|
|
|
10
27
|
compilerVersion: string;
|
|
11
28
|
fingerprint: string;
|
|
12
29
|
}
|
|
13
|
-
/** What a build needs beyond its sources: the options shaping the emit, and
|
|
30
|
+
/** What a build needs beyond its sources: the options shaping the emit, and a listener for its progress. */
|
|
14
31
|
export interface BuildPackageOptions extends BuildOptions {
|
|
15
|
-
|
|
32
|
+
/** Called before the emit begins, so that a caller can report the build ahead of a slow compile. */
|
|
33
|
+
onProgress?: (progress: BuildProgress) => void;
|
|
16
34
|
/**
|
|
17
35
|
* Overridable so that a missing `typescript` peer is reachable without uninstalling it.
|
|
18
36
|
*
|
|
@@ -30,10 +48,15 @@ export interface BuildPackageOptions extends BuildOptions {
|
|
|
30
48
|
* current inputs rather than an accumulation of every build that ever ran. Assets belong outside it, or in a
|
|
31
49
|
* `build:post` hook, which runs after the output is published.
|
|
32
50
|
*/
|
|
33
|
-
export declare function buildPackage(packageDir: string, options
|
|
51
|
+
export declare function buildPackage(packageDir: string, options?: BuildPackageOptions): Promise<BuildOutcome>;
|
|
34
52
|
/**
|
|
35
53
|
* Produces a digest of the given files (paths and contents), the emit config, and the toolchain.
|
|
36
54
|
* The file list is sorted so that the digest is order-invariant, and each path is folded in so that renames are
|
|
37
55
|
* detected.
|
|
38
56
|
*/
|
|
39
57
|
export declare function computeBuildHash(packageDir: string, files: string[], emitConfig: object, toolchain: BuildToolchain): Promise<string>;
|
|
58
|
+
/**
|
|
59
|
+
* Records a build skipped on unchanged inputs: states the `build-unchanged` outcome with the emit time recorded under
|
|
60
|
+
* `digest`, and returns that time, which is absent when none was recorded.
|
|
61
|
+
*/
|
|
62
|
+
export declare function recordUnchangedBuild(packageDir: string, digest: string, env: NodeJS.ProcessEnv): Promise<number | undefined>;
|
|
@@ -1,30 +1,24 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import { readFile
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
7
|
-
import { glob } from 'glob';
|
|
8
|
-
import { NMR_GLYPHS } from "../glyphs.js";
|
|
5
|
+
import { readCacheEntry, startTimer, UserError, writeCacheEntry } from '@williamthorsen/nmr-core';
|
|
9
6
|
import { resolveConfigPath } from "../helpers/config-path.js";
|
|
10
|
-
import { formatSaving } from "../helpers/duration.js";
|
|
11
7
|
import { writeStatedOutcome } from "../stated-outcome.js";
|
|
12
|
-
import {
|
|
8
|
+
import { DEFAULT_OUTDIR, hasExpectedBuildOutput, readEmitDuration, resolveBuildCachePath, resolveEntryPoints, resolveToolchainFingerprint, writeEmitDuration, } from "./build-output.js";
|
|
13
9
|
import { assertSupportedTypeScript, formatDiagnostics, getCompiler, loadCompiler } from "./compiler.js";
|
|
14
|
-
import {
|
|
10
|
+
import { discardScratchDirs, publishOutput, removeScratchDirs } from "./publish-output.js";
|
|
11
|
+
import { rewriteSpecifiers } from "./rewrite-specifiers.js";
|
|
15
12
|
import { resolveTsconfigChain } from "./tsconfig-chain.js";
|
|
16
13
|
const SOURCE_ROOT = 'src';
|
|
17
|
-
export async function buildPackage(packageDir, options) {
|
|
14
|
+
export async function buildPackage(packageDir, options = {}) {
|
|
18
15
|
await loadCompiler(options.loadTypeScript);
|
|
19
16
|
assertSupportedTypeScript();
|
|
20
17
|
const cachePath = resolveBuildCachePath(packageDir);
|
|
21
18
|
const outdir = options.outdir ?? DEFAULT_OUTDIR;
|
|
22
19
|
const emitDir = resolveEmitDir(packageDir, outdir);
|
|
23
20
|
const emitConfig = { outdir, declaration: true, rewriteRelativeImportExtensions: true };
|
|
24
|
-
const entryPoints = await
|
|
25
|
-
cwd: packageDir,
|
|
26
|
-
ignore: [...(options.ignorePatterns ?? DEFAULT_IGNORE_PATTERNS), ...(options.extraIgnorePatterns ?? [])],
|
|
27
|
-
});
|
|
21
|
+
const entryPoints = await resolveEntryPoints(packageDir, options);
|
|
28
22
|
const configPath = resolveConfigPath(packageDir);
|
|
29
23
|
const dependencies = [
|
|
30
24
|
'package.json',
|
|
@@ -35,17 +29,19 @@ export async function buildPackage(packageDir, options) {
|
|
|
35
29
|
compilerVersion: getCompiler().version,
|
|
36
30
|
fingerprint: await resolveToolchainFingerprint(packageDir),
|
|
37
31
|
};
|
|
38
|
-
const {
|
|
39
|
-
if (
|
|
40
|
-
await discardScratchDirs(
|
|
41
|
-
|
|
32
|
+
const { reason, currentHash } = await detectBuildChanges(packageDir, [...entryPoints, ...dependencies], emitConfig, toolchain, cachePath, hasExpectedBuildOutput(packageDir, outdir, entryPoints));
|
|
33
|
+
if (reason === 'unchanged') {
|
|
34
|
+
await discardScratchDirs(emitDir);
|
|
35
|
+
const savedMs = await recordUnchangedBuild(packageDir, currentHash, process.env);
|
|
36
|
+
return { kind: 'unchanged', ...(savedMs !== undefined && { savedMs }) };
|
|
42
37
|
}
|
|
38
|
+
options.onProgress?.({ kind: reason });
|
|
43
39
|
const readEmitMs = startTimer();
|
|
44
40
|
const emittedFileCount = await emitPackage(packageDir, entryPoints, outdir);
|
|
45
41
|
const emitMs = readEmitMs();
|
|
46
42
|
await writeCacheEntry(cachePath, currentHash);
|
|
47
43
|
await writeEmitDuration(packageDir, currentHash, emitMs);
|
|
48
|
-
|
|
44
|
+
return { kind: 'emitted', fileCount: emittedFileCount, outdir };
|
|
49
45
|
}
|
|
50
46
|
export async function computeBuildHash(packageDir, files, emitConfig, toolchain) {
|
|
51
47
|
const hash = createHash('sha256');
|
|
@@ -67,8 +63,7 @@ async function emitPackage(packageDir, entryPoints, outdir) {
|
|
|
67
63
|
const rootNames = entryPoints.map((entry) => path.resolve(packageDir, entry));
|
|
68
64
|
const sourceRoot = path.resolve(packageDir, SOURCE_ROOT);
|
|
69
65
|
const emitDir = resolveEmitDir(packageDir, outdir);
|
|
70
|
-
|
|
71
|
-
await removeScratchDirs(scratchDirs);
|
|
66
|
+
await removeScratchDirs(emitDir);
|
|
72
67
|
const emittedFiles = new Map();
|
|
73
68
|
function collect(fileName, text, writeByteOrderMark) {
|
|
74
69
|
emittedFiles.set(fileName, { text, writeByteOrderMark });
|
|
@@ -82,13 +77,7 @@ async function emitPackage(packageDir, entryPoints, outdir) {
|
|
|
82
77
|
for (const [fileName, file] of emittedFiles) {
|
|
83
78
|
stagedFiles.set(fileName, { ...file, text: rewriteSpecifiers(fileName, file.text, compilerOptions, sourceRoot) });
|
|
84
79
|
}
|
|
85
|
-
|
|
86
|
-
await rm(emitDir, { force: true, recursive: true });
|
|
87
|
-
return 0;
|
|
88
|
-
}
|
|
89
|
-
writeStagedOutput(stagedFiles, emitDir, scratchDirs.stagingDir);
|
|
90
|
-
await swapIntoPlace(emitDir, scratchDirs);
|
|
91
|
-
return stagedFiles.size;
|
|
80
|
+
return publishOutput(emitDir, stagedFiles);
|
|
92
81
|
}
|
|
93
82
|
function assertEmitSucceeded(result) {
|
|
94
83
|
if (result.emitSkipped) {
|
|
@@ -110,17 +99,6 @@ function createCachingCompilerHost(compilerOptions) {
|
|
|
110
99
|
};
|
|
111
100
|
return host;
|
|
112
101
|
}
|
|
113
|
-
async function discardScratchDirs(scratchDirs) {
|
|
114
|
-
try {
|
|
115
|
-
await removeScratchDirs(scratchDirs);
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
async function removeScratchDirs(scratchDirs) {
|
|
121
|
-
await rm(scratchDirs.previousDir, { force: true, recursive: true });
|
|
122
|
-
await rm(scratchDirs.stagingDir, { force: true, recursive: true });
|
|
123
|
-
}
|
|
124
102
|
function resolveEmitDir(packageDir, outdir) {
|
|
125
103
|
const resolvedDir = path.resolve(packageDir, outdir);
|
|
126
104
|
const relativePathFromPackage = path.relative(packageDir, resolvedDir);
|
|
@@ -132,24 +110,6 @@ function resolveEmitDir(packageDir, outdir) {
|
|
|
132
110
|
}
|
|
133
111
|
return resolvedDir;
|
|
134
112
|
}
|
|
135
|
-
async function swapIntoPlace(emitDir, scratchDirs) {
|
|
136
|
-
const hadPreviousOutput = existsSync(emitDir);
|
|
137
|
-
if (hadPreviousOutput) {
|
|
138
|
-
await rename(emitDir, scratchDirs.previousDir);
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
await rename(scratchDirs.stagingDir, emitDir);
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
if (hadPreviousOutput && !existsSync(emitDir)) {
|
|
145
|
-
await rename(scratchDirs.previousDir, emitDir);
|
|
146
|
-
}
|
|
147
|
-
throw new UserError(`failed to move the built output into ${emitDir}: ${describeError(error)}`, {
|
|
148
|
-
cause: error,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
await discardScratchDirs(scratchDirs);
|
|
152
|
-
}
|
|
153
113
|
function synthesizeCompilerOptions(packageDir, outdir) {
|
|
154
114
|
const ts = getCompiler();
|
|
155
115
|
const configPath = path.join(packageDir, 'tsconfig.json');
|
|
@@ -176,38 +136,16 @@ function synthesizeCompilerOptions(packageDir, outdir) {
|
|
|
176
136
|
noEmitOnError: false,
|
|
177
137
|
};
|
|
178
138
|
}
|
|
179
|
-
function
|
|
180
|
-
const ts = getCompiler();
|
|
181
|
-
for (const [fileName, file] of stagedFiles) {
|
|
182
|
-
if (!isWithin(emitDir, fileName)) {
|
|
183
|
-
throw new Error(`the compiler emitted ${fileName}, which is outside the output directory ${emitDir}. ` +
|
|
184
|
-
"Verify the resolved tsconfig's 'outDir' and 'declarationDir'.");
|
|
185
|
-
}
|
|
186
|
-
ts.sys.writeFile(path.join(stagingDir, path.relative(emitDir, fileName)), file.text, file.writeByteOrderMark);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
async function detectBuildChanges(packageDir, files, emitConfig, toolchain, cachePath, isOutputPresent, style) {
|
|
190
|
-
const packageName = path.basename(packageDir);
|
|
139
|
+
async function detectBuildChanges(packageDir, files, emitConfig, toolchain, cachePath, isOutputPresent) {
|
|
191
140
|
const previousHash = await readCacheEntry(cachePath);
|
|
192
141
|
const currentHash = await computeBuildHash(packageDir, files, emitConfig, toolchain);
|
|
193
|
-
if (previousHash
|
|
194
|
-
|
|
195
|
-
const savedMs = await readEmitDuration(packageDir, currentHash);
|
|
196
|
-
await writeStatedOutcome(process.env, { kind: 'build-unchanged', ...(savedMs !== undefined && { savedMs }) });
|
|
197
|
-
const saving = formatSaving(savedMs ?? 0);
|
|
198
|
-
const savingClause = saving === undefined ? '' : `, ${saving}`;
|
|
199
|
-
console.info(`${STATUS_GLYPHS[style].skipped.text} ${packageName}: No changes detected. Skipping build${savingClause}.`);
|
|
200
|
-
return { changed: false, currentHash };
|
|
201
|
-
}
|
|
202
|
-
console.info(formatGlyphLine(NMR_GLYPHS, style, 'package', `${packageName}: Build output is missing. Rebuilding.`));
|
|
203
|
-
return { changed: true, currentHash };
|
|
142
|
+
if (previousHash !== currentHash) {
|
|
143
|
+
return { reason: 'inputs-changed', currentHash };
|
|
204
144
|
}
|
|
205
|
-
|
|
206
|
-
return { changed: true, currentHash };
|
|
145
|
+
return { reason: isOutputPresent ? 'unchanged' : 'output-missing', currentHash };
|
|
207
146
|
}
|
|
208
|
-
function
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return `Compiled ${fileCount} files to ${outdir.replace(/\/+$/u, '')}.`;
|
|
147
|
+
export async function recordUnchangedBuild(packageDir, digest, env) {
|
|
148
|
+
const savedMs = await readEmitDuration(packageDir, digest);
|
|
149
|
+
await writeStatedOutcome(env, { kind: 'build-unchanged', ...(savedMs !== undefined && { savedMs }) });
|
|
150
|
+
return savedMs;
|
|
213
151
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
2
|
+
import type { CleanOutcome, CleanProgress } from './clean.js';
|
|
3
|
+
/** Renders the line that closes a sweep, or `undefined` for a one-package clean, whose own line concludes it. */
|
|
4
|
+
export declare function renderCleanOutcome(outcome: CleanOutcome, style: OutputStyle): string | undefined;
|
|
5
|
+
/** Renders the line that reports a completed step of a clean. */
|
|
6
|
+
export declare function renderCleanProgress(progress: CleanProgress, style: OutputStyle): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { formatGlyphLine } from '@williamthorsen/nmr-core';
|
|
3
|
+
import { NMR_GLYPHS } from "../glyphs.js";
|
|
4
|
+
export function renderCleanOutcome(outcome, style) {
|
|
5
|
+
if (outcome.kind === 'package') {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
return formatGlyphLine(NMR_GLYPHS, style, 'clean', describeSweep(outcome.cleanedDirs.length, outcome.skippedCount));
|
|
9
|
+
}
|
|
10
|
+
export function renderCleanProgress(progress, style) {
|
|
11
|
+
const message = progress.kind === 'check-results-removed'
|
|
12
|
+
? 'Removed all recorded check results.'
|
|
13
|
+
: `${path.basename(progress.packageDir)}: Removed build output and cache.`;
|
|
14
|
+
return formatGlyphLine(NMR_GLYPHS, style, 'clean', message);
|
|
15
|
+
}
|
|
16
|
+
function describeSweep(cleanedCount, skippedCount) {
|
|
17
|
+
const packages = cleanedCount === 1 ? '1 package' : `${cleanedCount} packages`;
|
|
18
|
+
const skipClause = skippedCount === 0 ? '' : `, skipping ${skippedCount} with an empty clean override`;
|
|
19
|
+
return `Cleaned ${packages}${skipClause}.`;
|
|
20
|
+
}
|
|
@@ -1,18 +1,50 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Writable } from 'node:stream';
|
|
2
|
+
/** What a clean did: one package's clean, or a sweep of every workspace package. */
|
|
3
|
+
export type CleanOutcome = {
|
|
4
|
+
kind: 'package';
|
|
5
|
+
checkResultsDir: string;
|
|
6
|
+
packageDir: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'sweep';
|
|
9
|
+
checkResultsDir: string;
|
|
10
|
+
cleanedDirs: string[];
|
|
11
|
+
skippedCount: number;
|
|
12
|
+
};
|
|
13
|
+
/** A step of a clean, reported as soon as it completes. */
|
|
14
|
+
export type CleanProgress = {
|
|
15
|
+
kind: 'check-results-removed';
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'package-cleaned';
|
|
18
|
+
packageDir: string;
|
|
19
|
+
};
|
|
20
|
+
/** What a clean needs from its caller: a listener for its progress, and the streams for an override's output. */
|
|
21
|
+
export interface CleanOptions {
|
|
22
|
+
/** Called after each step completes, so that a caller can report it before a later override runs or fails. */
|
|
23
|
+
onProgress?: (progress: CleanProgress) => void;
|
|
24
|
+
/** The stream to which a package's own `clean` override writes its errors. */
|
|
25
|
+
stderr: Writable;
|
|
26
|
+
/** The stream to which a package's own `clean` override writes its output. */
|
|
27
|
+
stdout: Writable;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Cleans the package containing `cwd`, or every workspace package when run from the monorepo root.
|
|
31
|
+
*
|
|
32
|
+
* From within a package this is the built-in clean itself: `nmr` has already resolved `clean` to `nmr-clean`
|
|
33
|
+
* before invoking it, so resolving again would apply the package's own override twice.
|
|
34
|
+
*/
|
|
35
|
+
export declare function cleanFrom(cwd: string, options: CleanOptions): Promise<CleanOutcome>;
|
|
2
36
|
/**
|
|
3
37
|
* Removes a package's build output and the build-cache entries that describe it, so the package does not keep
|
|
4
38
|
* any state that would make the next build skip. Removal is idempotent: An already-clean package is a silent
|
|
5
39
|
* no-op.
|
|
6
40
|
*/
|
|
7
|
-
export declare function cleanPackage(packageDir: string
|
|
41
|
+
export declare function cleanPackage(packageDir: string): Promise<void>;
|
|
8
42
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* From within a package this is the built-in clean itself: `nmr` has already resolved `clean` to this bin
|
|
12
|
-
* before invoking it, so resolving again would apply the package's own override twice.
|
|
43
|
+
* Runs `nmr-clean` against `argv` from `cwd` and returns the exit code to report. The command takes neither an
|
|
44
|
+
* option nor an operand, so anything but `--help` is a usage error and removes nothing.
|
|
13
45
|
*
|
|
14
46
|
* This is the default `clean` script, and it is published as a bin because, under pnpm's isolated layout,
|
|
15
47
|
* `pnpm exec` resolves only the bins of the consuming project's direct dependencies: pnpm links nmr's own bins
|
|
16
48
|
* into the consumer's `node_modules/.bin`, but not the bins of nmr's dependencies.
|
|
17
49
|
*/
|
|
18
|
-
export declare function runClean(
|
|
50
|
+
export declare function runClean(argv: string[], cwd: string): Promise<number>;
|
|
@@ -1,59 +1,81 @@
|
|
|
1
1
|
import { rm } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { UserError } from '@williamthorsen/nmr-core';
|
|
5
|
+
import { defineCommand, runCli } from '@williamthorsen/nmr-core/cli';
|
|
4
6
|
import { NMR_CLEAN_BIN } from "../bins.js";
|
|
5
7
|
import { removeCheckCache } from "../check-cache-entries.js";
|
|
6
8
|
import { loadRootConfig } from "../config.js";
|
|
7
9
|
import { findContainingPackageDir } from "../context.js";
|
|
8
|
-
import { NMR_GLYPHS } from "../glyphs.js";
|
|
9
10
|
import { reportClosing } from "../helpers/reportClosing.js";
|
|
10
11
|
import { resolveChannel } from "../output-channel.js";
|
|
11
|
-
import {
|
|
12
|
+
import { resolveBinStyles } from "../resolveBinStyles.js";
|
|
13
|
+
import { buildWorkspaceRegistry, resolveScript } from "../resolver.js";
|
|
12
14
|
import { runCommand } from "../runner.js";
|
|
13
15
|
import { renderChain } from "../steps.js";
|
|
14
16
|
import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
|
|
15
17
|
import { resolveBuildCachePath, resolveEmitDurationPath } from "./build-output.js";
|
|
18
|
+
import { renderCleanOutcome, renderCleanProgress } from "./clean-report.js";
|
|
16
19
|
const CLEAN_COMMAND = 'clean';
|
|
17
20
|
const BUILT_IN_CLEAN = 'nmr-clean';
|
|
18
21
|
const OUTPUT_ROOT = 'dist';
|
|
19
|
-
export async function
|
|
20
|
-
await rm(path.resolve(packageDir, OUTPUT_ROOT), { recursive: true, force: true });
|
|
21
|
-
await rm(resolveBuildCachePath(packageDir), { force: true });
|
|
22
|
-
await rm(resolveEmitDurationPath(packageDir), { force: true });
|
|
23
|
-
console.info(formatGlyphLine(NMR_GLYPHS, style, 'clean', `${path.basename(packageDir)}: Removed build output and cache.`));
|
|
24
|
-
}
|
|
25
|
-
export async function runClean(cwd, style) {
|
|
22
|
+
export async function cleanFrom(cwd, options) {
|
|
26
23
|
let monorepoRoot;
|
|
27
24
|
try {
|
|
28
25
|
monorepoRoot = findMonorepoRoot(cwd);
|
|
29
26
|
}
|
|
30
27
|
catch {
|
|
31
|
-
await clearCheckCache(cwd,
|
|
32
|
-
await
|
|
33
|
-
return;
|
|
28
|
+
await clearCheckCache(cwd, options);
|
|
29
|
+
await cleanReportedPackage(cwd, options);
|
|
30
|
+
return { kind: 'package', checkResultsDir: cwd, packageDir: cwd };
|
|
34
31
|
}
|
|
35
|
-
await clearCheckCache(monorepoRoot,
|
|
32
|
+
await clearCheckCache(monorepoRoot, options);
|
|
36
33
|
const workspacePackageDirs = getWorkspacePackageDirs(monorepoRoot);
|
|
37
34
|
const packageDir = findContainingPackageDir(cwd, workspacePackageDirs);
|
|
38
35
|
if (packageDir !== undefined) {
|
|
39
|
-
await
|
|
40
|
-
return;
|
|
36
|
+
await cleanReportedPackage(packageDir, options);
|
|
37
|
+
return { kind: 'package', checkResultsDir: monorepoRoot, packageDir };
|
|
41
38
|
}
|
|
42
|
-
|
|
39
|
+
return sweepWorkspace(monorepoRoot, workspacePackageDirs, options);
|
|
43
40
|
}
|
|
44
|
-
async function
|
|
45
|
-
await
|
|
46
|
-
|
|
41
|
+
export async function cleanPackage(packageDir) {
|
|
42
|
+
await rm(path.resolve(packageDir, OUTPUT_ROOT), { recursive: true, force: true });
|
|
43
|
+
await rm(resolveBuildCachePath(packageDir), { force: true });
|
|
44
|
+
await rm(resolveEmitDurationPath(packageDir), { force: true });
|
|
45
|
+
}
|
|
46
|
+
export async function runClean(argv, cwd) {
|
|
47
|
+
const command = defineCommand({
|
|
48
|
+
summary: 'Remove the build output and build cache of this package, or of every package from the monorepo root',
|
|
49
|
+
run: async () => {
|
|
50
|
+
const style = resolveBinStyles().stdout;
|
|
51
|
+
const outcome = await cleanFrom(cwd, {
|
|
52
|
+
onProgress: (progress) => {
|
|
53
|
+
console.info(renderCleanProgress(progress, style));
|
|
54
|
+
},
|
|
55
|
+
stderr: process.stderr,
|
|
56
|
+
stdout: process.stdout,
|
|
57
|
+
});
|
|
58
|
+
const closing = renderCleanOutcome(outcome, style);
|
|
59
|
+
if (closing !== undefined) {
|
|
60
|
+
reportClosing(closing);
|
|
61
|
+
}
|
|
62
|
+
return 0;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
return runCli(argv, command, { name: NMR_CLEAN_BIN, stdout: process.stdout, stderr: process.stderr, baseDir: cwd });
|
|
47
66
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
67
|
+
async function cleanReportedPackage(packageDir, options) {
|
|
68
|
+
await cleanPackage(packageDir);
|
|
69
|
+
options.onProgress?.({ kind: 'package-cleaned', packageDir });
|
|
70
|
+
}
|
|
71
|
+
async function clearCheckCache(scopeDir, options) {
|
|
72
|
+
await removeCheckCache(scopeDir);
|
|
73
|
+
options.onProgress?.({ kind: 'check-results-removed' });
|
|
52
74
|
}
|
|
53
|
-
async function sweepWorkspace(monorepoRoot, workspacePackageDirs,
|
|
75
|
+
async function sweepWorkspace(monorepoRoot, workspacePackageDirs, options) {
|
|
54
76
|
const config = await loadRootConfig(monorepoRoot);
|
|
55
77
|
const registry = buildWorkspaceRegistry(config);
|
|
56
|
-
|
|
78
|
+
const cleanedDirs = [];
|
|
57
79
|
let skippedCount = 0;
|
|
58
80
|
for (const packageDir of workspacePackageDirs) {
|
|
59
81
|
const resolvedScript = resolveScript(CLEAN_COMMAND, registry, packageDir, false);
|
|
@@ -63,19 +85,20 @@ async function sweepWorkspace(monorepoRoot, workspacePackageDirs, style) {
|
|
|
63
85
|
continue;
|
|
64
86
|
}
|
|
65
87
|
if (resolvedCommand === BUILT_IN_CLEAN) {
|
|
66
|
-
await
|
|
67
|
-
|
|
88
|
+
await cleanReportedPackage(packageDir, options);
|
|
89
|
+
cleanedDirs.push(packageDir);
|
|
68
90
|
continue;
|
|
69
91
|
}
|
|
70
|
-
const
|
|
71
|
-
const { exitCode } = await runCommand(command, packageDir, {
|
|
92
|
+
const { exitCode } = await runCommand(resolvedCommand, packageDir, {
|
|
72
93
|
bin: NMR_CLEAN_BIN,
|
|
73
|
-
channels: { stderr: resolveChannel(
|
|
94
|
+
channels: { stderr: resolveChannel(options.stderr, false), stdout: resolveChannel(options.stdout, false) },
|
|
95
|
+
stderr: options.stderr,
|
|
96
|
+
stdout: options.stdout,
|
|
74
97
|
});
|
|
75
98
|
if (exitCode !== 0) {
|
|
76
|
-
throw new UserError(`\`${
|
|
99
|
+
throw new UserError(`\`${resolvedCommand}\` failed in ${path.basename(packageDir)} with exit code ${exitCode}.`);
|
|
77
100
|
}
|
|
78
|
-
|
|
101
|
+
cleanedDirs.push(packageDir);
|
|
79
102
|
}
|
|
80
|
-
|
|
103
|
+
return { kind: 'sweep', checkResultsDir: monorepoRoot, cleanedDirs, skippedCount };
|
|
81
104
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs `nmr-compile` against `argv` for the package at `packageDir` and returns the exit code to report. The command
|
|
3
|
+
* takes neither an option nor an operand, so anything but `--help` is a usage error and compiles nothing.
|
|
4
|
+
*/
|
|
5
|
+
export declare function runCompile(argv: string[], packageDir: string): Promise<number>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { defineCommand, runCli } from '@williamthorsen/nmr-core/cli';
|
|
4
|
+
import { NMR_COMPILE_BIN } from "../bins.js";
|
|
5
|
+
import { loadWorkspaceConfig } from "../config.js";
|
|
6
|
+
import { resolveBinStyles } from "../resolveBinStyles.js";
|
|
7
|
+
import { buildPackage } from "./build.js";
|
|
8
|
+
import { renderBuildOutcome, renderBuildProgress } from "./build-report.js";
|
|
9
|
+
export async function runCompile(argv, packageDir) {
|
|
10
|
+
const command = defineCommand({
|
|
11
|
+
summary: "Compile the package's src tree to dist/esm",
|
|
12
|
+
run: async () => {
|
|
13
|
+
const { stdout } = resolveBinStyles();
|
|
14
|
+
const { build } = await loadWorkspaceConfig(packageDir);
|
|
15
|
+
const packageName = path.basename(packageDir);
|
|
16
|
+
const outcome = await buildPackage(packageDir, {
|
|
17
|
+
onProgress: (progress) => {
|
|
18
|
+
console.info(renderBuildProgress(progress, packageName, stdout));
|
|
19
|
+
},
|
|
20
|
+
...(build?.extraIgnorePatterns !== undefined && { extraIgnorePatterns: build.extraIgnorePatterns }),
|
|
21
|
+
});
|
|
22
|
+
console.info(renderBuildOutcome(outcome, packageName, stdout));
|
|
23
|
+
return 0;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
return runCli(argv, command, {
|
|
27
|
+
name: NMR_COMPILE_BIN,
|
|
28
|
+
stdout: process.stdout,
|
|
29
|
+
stderr: process.stderr,
|
|
30
|
+
baseDir: packageDir,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -10,6 +10,15 @@ export type FormatMode = 'check' | 'write';
|
|
|
10
10
|
* with `-`.
|
|
11
11
|
*/
|
|
12
12
|
export declare function runFmt(argv: string[], cwd?: string): Promise<number>;
|
|
13
|
+
/**
|
|
14
|
+
* How a spawned Prettier process ended.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export interface PrettierOutcome {
|
|
19
|
+
error?: Error | undefined;
|
|
20
|
+
status: number | null;
|
|
21
|
+
}
|
|
13
22
|
/** @internal */
|
|
14
23
|
export interface RunPrettierOptions {
|
|
15
24
|
cliPath: string;
|
|
@@ -23,6 +32,8 @@ export interface RunPrettierOptions {
|
|
|
23
32
|
cwd: string;
|
|
24
33
|
/** Overridable so that the multi-batch path is reachable without a fixture that hits the real ceiling. */
|
|
25
34
|
budgetBytes?: number;
|
|
35
|
+
/** Runs the Prettier CLI as `file` with `args` in `cwd`. */
|
|
36
|
+
spawn?: (file: string, args: string[], cwd: string) => PrettierOutcome;
|
|
26
37
|
}
|
|
27
38
|
/**
|
|
28
39
|
* Runs Prettier over the selection, returning the first non-zero exit code. Every batch runs even after
|
package/dist/esm/commands/fmt.js
CHANGED
|
@@ -122,7 +122,7 @@ async function findRepositoryConfig(manifestPath, cwd) {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
export function runPrettier(options) {
|
|
125
|
-
const { cacheLocation, cliPath, configPath, mode, files, ignorePaths, cwd, budgetBytes = ARGUMENT_BUDGET_BYTES, } = options;
|
|
125
|
+
const { cacheLocation, cliPath, configPath, mode, files, ignorePaths, cwd, budgetBytes = ARGUMENT_BUDGET_BYTES, spawn = spawnInherited, } = options;
|
|
126
126
|
const prettierArgs = [
|
|
127
127
|
cliPath,
|
|
128
128
|
...(configPath === undefined ? [] : ['--config', configPath]),
|
|
@@ -136,7 +136,7 @@ export function runPrettier(options) {
|
|
|
136
136
|
];
|
|
137
137
|
let firstFailureStatus = 0;
|
|
138
138
|
for (const batch of batchWithinBudget(files, budgetBytes)) {
|
|
139
|
-
const result =
|
|
139
|
+
const result = spawn(process.execPath, [...prettierArgs, ...batch], cwd);
|
|
140
140
|
if (result.error) {
|
|
141
141
|
reportError(NMR_FMT_BIN, `could not run \`${PRETTIER_PACKAGE}\` (${result.error.message}).`);
|
|
142
142
|
return 1;
|
|
@@ -174,3 +174,7 @@ function resolveFrom(parent, specifier) {
|
|
|
174
174
|
return undefined;
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
|
+
function spawnInherited(file, args, cwd) {
|
|
178
|
+
const { error, status } = spawnSync(file, args, { cwd, stdio: 'inherit' });
|
|
179
|
+
return { error, status };
|
|
180
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** The pair of directories through which a build publishes, both siblings of the emit directory. */
|
|
2
|
+
export interface ScratchDirs {
|
|
3
|
+
previousDir: string;
|
|
4
|
+
stagingDir: string;
|
|
5
|
+
}
|
|
6
|
+
/** One file emitted by the compiler, kept in memory until the whole emit is ready to publish. */
|
|
7
|
+
export interface StagedFile {
|
|
8
|
+
text: string;
|
|
9
|
+
writeByteOrderMark: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Removes both scratch directories of `emitDir` without failing the caller. `force` suppresses only a missing
|
|
13
|
+
* top-level path and `rm` does not retry, so a removal racing another build's leaves `ENOTEMPTY`, `EBUSY`, and
|
|
14
|
+
* `EPERM` live.
|
|
15
|
+
*/
|
|
16
|
+
export declare function discardScratchDirs(emitDir: string): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Publishes the staged emit to `emitDir` atomically: It writes the files to a staging directory and swaps that
|
|
19
|
+
* into place by rename. Returns how many files it published.
|
|
20
|
+
*
|
|
21
|
+
* An emit that produces nothing removes `emitDir` instead, because swapping an empty directory into place would
|
|
22
|
+
* leave a `dist` behind for a package whose entry points do not emit any output.
|
|
23
|
+
*
|
|
24
|
+
* A failure while staging precedes the first rename, so it leaves the previous output exactly as it was.
|
|
25
|
+
*/
|
|
26
|
+
export declare function publishOutput(emitDir: string, stagedFiles: Map<string, StagedFile>): Promise<number>;
|
|
27
|
+
/** Removes both scratch directories of `emitDir`, tolerating their absence. */
|
|
28
|
+
export declare function removeScratchDirs(emitDir: string): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Resolves the two scratch directories through which a build publishes: `staging`, which the emit is written to,
|
|
31
|
+
* and `previous`, which the outgoing output is renamed aside to. Both are siblings of the emit directory, so a
|
|
32
|
+
* rename between them never crosses a filesystem, and both are dot-prefixed so that a leftover stays out of the
|
|
33
|
+
* globs that select sources.
|
|
34
|
+
*
|
|
35
|
+
* The names are fixed rather than unique because the build removes both before use. A unique name would need a
|
|
36
|
+
* sweep to accomplish that removal, and a sweep cannot tell a directory orphaned by a killed run from one that a
|
|
37
|
+
* concurrent build is still writing. Because a directory left behind by a failed build is cleared the same way,
|
|
38
|
+
* the failure path does not need any cleanup of its own.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveScratchDirs(emitDir: string): ScratchDirs;
|