@williamthorsen/nmr 0.37.0 → 0.39.0
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 +114 -0
- package/.readyup/kits/default.js +92 -87
- package/.readyup/manifest.json +8 -8
- package/CHANGELOG.md +89 -0
- package/README.md +37 -28
- package/agents/guidance/rulebooks/nmr.md +2 -2
- package/dist/esm/check-cache.d.ts +4 -3
- package/dist/esm/check-cache.js +28 -28
- package/dist/esm/cli-build.js +3 -0
- package/dist/esm/cli-clean.js +3 -1
- package/dist/esm/cli-ensure-prepublish-hooks.js +7 -5
- package/dist/esm/cli-report-catalog.js +2 -1
- package/dist/esm/cli-report-overrides.js +2 -1
- package/dist/esm/commands/build-output.d.ts +2 -2
- package/dist/esm/commands/build-output.js +9 -9
- package/dist/esm/commands/build.d.ts +6 -1
- package/dist/esm/commands/build.js +73 -72
- package/dist/esm/commands/clean.d.ts +3 -2
- package/dist/esm/commands/clean.js +25 -24
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +5 -4
- package/dist/esm/commands/ensure-prepublish-hooks.js +44 -42
- package/dist/esm/commands/fmt.js +33 -33
- package/dist/esm/commands/report-catalog.d.ts +2 -1
- package/dist/esm/commands/report-catalog.js +14 -11
- package/dist/esm/commands/report-overrides.d.ts +2 -1
- package/dist/esm/commands/report-overrides.js +11 -10
- package/dist/esm/config.js +39 -29
- package/dist/esm/context.js +8 -8
- package/dist/esm/default-scripts.d.ts +2 -2
- package/dist/esm/default-scripts.js +5 -5
- package/dist/esm/glyphs.d.ts +9 -0
- package/dist/esm/glyphs.js +9 -0
- package/dist/esm/help.d.ts +2 -2
- package/dist/esm/help.js +36 -30
- package/dist/esm/helpers/clampToBytes.js +3 -3
- package/dist/esm/helpers/createBoundedBuffer.d.ts +1 -1
- package/dist/esm/helpers/createBoundedBuffer.js +2 -2
- package/dist/esm/helpers/duration.d.ts +2 -2
- package/dist/esm/helpers/duration.js +7 -7
- package/dist/esm/helpers/filter-selection.js +12 -12
- package/dist/esm/helpers/findClosestName.js +15 -15
- package/dist/esm/helpers/package-json.d.ts +2 -2
- package/dist/esm/helpers/package-json.js +26 -25
- package/dist/esm/helpers/transcript.d.ts +2 -2
- package/dist/esm/helpers/transcript.js +14 -14
- package/dist/esm/output-style.d.ts +41 -0
- package/dist/esm/output-style.js +17 -0
- package/dist/esm/prettier.js +5 -5
- package/dist/esm/recording.d.ts +8 -5
- package/dist/esm/recording.js +27 -17
- package/dist/esm/report-format.d.ts +1 -1
- package/dist/esm/report-format.js +7 -7
- package/dist/esm/resolveBinStyles.d.ts +12 -0
- package/dist/esm/resolveBinStyles.js +16 -0
- package/dist/esm/resolver.d.ts +2 -2
- package/dist/esm/resolver.js +16 -16
- package/dist/esm/runCli.js +309 -221
- package/dist/esm/runner.d.ts +1 -1
- package/dist/esm/runner.js +18 -18
- package/dist/esm/steps.d.ts +17 -5
- package/dist/esm/steps.js +37 -28
- package/dist/esm/tests.d.ts +7 -7
- package/dist/esm/tests.js +20 -8
- package/dist/esm/tiers.d.ts +1 -1
- package/dist/esm/tiers.js +8 -8
- package/dist/esm/verbosity.d.ts +1 -1
- package/dist/esm/verbosity.js +7 -7
- package/dist/esm/verdict.d.ts +15 -4
- package/dist/esm/verdict.js +78 -59
- package/dist/esm/vitest-source-resolution.js +48 -46
- package/dist/esm/vitest.d.ts +2 -2
- package/dist/esm/vitest.js +74 -25
- package/dist/esm/workspace.d.ts +2 -18
- package/dist/esm/workspace.js +9 -67
- package/docs/check-cache.md +5 -5
- package/docs/reporting.md +20 -2
- package/docs/scripts.md +2 -2
- package/docs/vitest.md +6 -6
- package/docs/workspace.md +24 -1
- package/package.json +9 -10
- package/dist/esm/helpers/workspace-patterns.d.ts +0 -8
- package/dist/esm/helpers/workspace-patterns.js +0 -29
|
@@ -2,19 +2,18 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { readFile, rename, rm } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { readCacheEntry, writeCacheEntry } from '@williamthorsen/nmr-core';
|
|
5
|
+
import { formatGlyphLine, readCacheEntry, STATUS_GLYPHS, writeCacheEntry, } from '@williamthorsen/nmr-core';
|
|
6
6
|
import { glob } from 'glob';
|
|
7
7
|
import * as ts from 'typescript';
|
|
8
|
+
import { NMR_GLYPHS } from "../glyphs.js";
|
|
8
9
|
import { resolveConfigPath } from "../helpers/config-path.js";
|
|
9
10
|
import { DEFAULT_ENTRY_GLOBS, DEFAULT_IGNORE_PATTERNS, DEFAULT_OUTDIR, hasExpectedBuildOutput, resolveBuildCachePath, resolveScratchDirs, resolveToolchainFingerprint, } from "./build-output.js";
|
|
10
|
-
const PACKAGE_ICON = '📦';
|
|
11
|
-
const SKIPPED_ICON = '⏭️';
|
|
12
11
|
const SOURCE_ROOT = 'src';
|
|
13
12
|
const MINIMUM_TYPESCRIPT_MAJOR = 5;
|
|
14
13
|
const MINIMUM_TYPESCRIPT_MINOR = 7;
|
|
15
14
|
const TS_EXTENSION = '.ts';
|
|
16
15
|
const JS_EXTENSION = '.js';
|
|
17
|
-
export async function buildPackage(packageDir, options
|
|
16
|
+
export async function buildPackage(packageDir, options) {
|
|
18
17
|
assertSupportedTypeScript();
|
|
19
18
|
const cachePath = resolveBuildCachePath(packageDir);
|
|
20
19
|
const outdir = options.outdir ?? DEFAULT_OUTDIR;
|
|
@@ -34,14 +33,14 @@ export async function buildPackage(packageDir, options = {}) {
|
|
|
34
33
|
compilerVersion: ts.version,
|
|
35
34
|
fingerprint: await resolveToolchainFingerprint(packageDir),
|
|
36
35
|
};
|
|
37
|
-
const { changed, currentHash } = await detectBuildChanges(packageDir, [...entryPoints, ...dependencies], emitConfig, toolchain, cachePath, hasExpectedBuildOutput(packageDir, outdir, entryPoints));
|
|
36
|
+
const { changed, currentHash } = await detectBuildChanges(packageDir, [...entryPoints, ...dependencies], emitConfig, toolchain, cachePath, hasExpectedBuildOutput(packageDir, outdir, entryPoints), options.style);
|
|
38
37
|
if (!changed) {
|
|
39
38
|
await discardScratchDirs(resolveScratchDirs(emitDir));
|
|
40
39
|
return;
|
|
41
40
|
}
|
|
42
41
|
const emittedFileCount = await emitPackage(packageDir, entryPoints, outdir);
|
|
43
42
|
await writeCacheEntry(cachePath, currentHash);
|
|
44
|
-
console.info(`${
|
|
43
|
+
console.info(formatGlyphLine(NMR_GLYPHS, options.style, 'package', `${path.basename(packageDir)}: ${describeEmit(emittedFileCount, outdir)}`));
|
|
45
44
|
}
|
|
46
45
|
export async function computeBuildHash(packageDir, files, emitConfig, toolchain) {
|
|
47
46
|
const hash = createHash('sha256');
|
|
@@ -61,18 +60,18 @@ export function resolveTsconfigChain(packageDir, configFileName = 'tsconfig.json
|
|
|
61
60
|
const resolvedChain = [];
|
|
62
61
|
const seen = new Set();
|
|
63
62
|
function walk(configPath) {
|
|
64
|
-
const
|
|
65
|
-
if (seen.has(
|
|
63
|
+
const normalizedPath = path.resolve(configPath);
|
|
64
|
+
if (seen.has(normalizedPath)) {
|
|
66
65
|
return;
|
|
67
66
|
}
|
|
68
|
-
seen.add(
|
|
69
|
-
resolvedChain.push(
|
|
70
|
-
const configFile = ts.readConfigFile(
|
|
67
|
+
seen.add(normalizedPath);
|
|
68
|
+
resolvedChain.push(normalizedPath);
|
|
69
|
+
const configFile = ts.readConfigFile(normalizedPath, (fileName) => ts.sys.readFile(fileName));
|
|
71
70
|
if (configFile.error) {
|
|
72
|
-
throw new Error(`nmr-compile: failed to read ${
|
|
71
|
+
throw new Error(`nmr-compile: failed to read ${normalizedPath}.\n${formatDiagnostics([configFile.error])}`);
|
|
73
72
|
}
|
|
74
73
|
for (const entry of normalizeExtendsField(configFile.config)) {
|
|
75
|
-
walk(resolveExtendsTarget(entry,
|
|
74
|
+
walk(resolveExtendsTarget(entry, normalizedPath));
|
|
76
75
|
}
|
|
77
76
|
}
|
|
78
77
|
walk(path.resolve(packageDir, configFileName));
|
|
@@ -85,26 +84,26 @@ async function emitPackage(packageDir, entryPoints, outdir) {
|
|
|
85
84
|
const emitDir = resolveEmitDir(packageDir, outdir);
|
|
86
85
|
const scratchDirs = resolveScratchDirs(emitDir);
|
|
87
86
|
await removeScratchDirs(scratchDirs);
|
|
88
|
-
const
|
|
87
|
+
const emittedFiles = new Map();
|
|
89
88
|
function collect(fileName, text, writeByteOrderMark) {
|
|
90
|
-
|
|
89
|
+
emittedFiles.set(fileName, { text, writeByteOrderMark });
|
|
91
90
|
}
|
|
92
91
|
const host = createCachingCompilerHost(compilerOptions);
|
|
93
92
|
const scriptProgram = ts.createProgram(rootNames, { ...compilerOptions, declaration: false }, host);
|
|
94
93
|
assertEmitSucceeded(scriptProgram.emit(undefined, collect));
|
|
95
94
|
const declarationProgram = ts.createProgram(rootNames, { ...compilerOptions, emitDeclarationOnly: true, removeComments: false }, host, scriptProgram);
|
|
96
95
|
assertEmitSucceeded(declarationProgram.emit(undefined, collect));
|
|
97
|
-
const
|
|
98
|
-
for (const [fileName, file] of
|
|
99
|
-
|
|
96
|
+
const stagedFiles = new Map();
|
|
97
|
+
for (const [fileName, file] of emittedFiles) {
|
|
98
|
+
stagedFiles.set(fileName, { ...file, text: rewriteSpecifiers(fileName, file.text, compilerOptions, sourceRoot) });
|
|
100
99
|
}
|
|
101
|
-
if (
|
|
100
|
+
if (stagedFiles.size === 0) {
|
|
102
101
|
await rm(emitDir, { force: true, recursive: true });
|
|
103
102
|
return 0;
|
|
104
103
|
}
|
|
105
|
-
writeStagedOutput(
|
|
104
|
+
writeStagedOutput(stagedFiles, emitDir, scratchDirs.stagingDir);
|
|
106
105
|
await swapIntoPlace(emitDir, scratchDirs);
|
|
107
|
-
return
|
|
106
|
+
return stagedFiles.size;
|
|
108
107
|
}
|
|
109
108
|
function assertEmitSucceeded(result) {
|
|
110
109
|
if (result.emitSkipped) {
|
|
@@ -113,14 +112,14 @@ function assertEmitSucceeded(result) {
|
|
|
113
112
|
}
|
|
114
113
|
function createCachingCompilerHost(compilerOptions) {
|
|
115
114
|
const host = ts.createCompilerHost(compilerOptions);
|
|
116
|
-
const
|
|
115
|
+
const parsedFiles = new Map();
|
|
117
116
|
const readSourceFile = host.getSourceFile.bind(host);
|
|
118
117
|
host.getSourceFile = (fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) => {
|
|
119
|
-
if (
|
|
120
|
-
return
|
|
118
|
+
if (parsedFiles.has(fileName)) {
|
|
119
|
+
return parsedFiles.get(fileName);
|
|
121
120
|
}
|
|
122
121
|
const sourceFile = readSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
|
|
123
|
-
|
|
122
|
+
parsedFiles.set(fileName, sourceFile);
|
|
124
123
|
return sourceFile;
|
|
125
124
|
};
|
|
126
125
|
return host;
|
|
@@ -133,29 +132,31 @@ async function discardScratchDirs(scratchDirs) {
|
|
|
133
132
|
}
|
|
134
133
|
}
|
|
135
134
|
async function removeScratchDirs(scratchDirs) {
|
|
136
|
-
await rm(scratchDirs.
|
|
137
|
-
await rm(scratchDirs.
|
|
135
|
+
await rm(scratchDirs.previousDir, { force: true, recursive: true });
|
|
136
|
+
await rm(scratchDirs.stagingDir, { force: true, recursive: true });
|
|
138
137
|
}
|
|
139
138
|
function resolveEmitDir(packageDir, outdir) {
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
139
|
+
const resolvedDir = path.resolve(packageDir, outdir);
|
|
140
|
+
const relativePathFromPackage = path.relative(packageDir, resolvedDir);
|
|
141
|
+
if (relativePathFromPackage === '' ||
|
|
142
|
+
relativePathFromPackage.startsWith('..') ||
|
|
143
|
+
path.isAbsolute(relativePathFromPackage)) {
|
|
143
144
|
throw new Error(`nmr-compile: refusing to build into '${outdir}', which does not resolve inside the package. ` +
|
|
144
145
|
'The build replaces its output directory on each emit, so the directory must sit below the package root.');
|
|
145
146
|
}
|
|
146
|
-
return
|
|
147
|
+
return resolvedDir;
|
|
147
148
|
}
|
|
148
149
|
async function swapIntoPlace(emitDir, scratchDirs) {
|
|
149
150
|
const hadPreviousOutput = existsSync(emitDir);
|
|
150
151
|
if (hadPreviousOutput) {
|
|
151
|
-
await rename(emitDir, scratchDirs.
|
|
152
|
+
await rename(emitDir, scratchDirs.previousDir);
|
|
152
153
|
}
|
|
153
154
|
try {
|
|
154
|
-
await rename(scratchDirs.
|
|
155
|
+
await rename(scratchDirs.stagingDir, emitDir);
|
|
155
156
|
}
|
|
156
157
|
catch (error) {
|
|
157
158
|
if (hadPreviousOutput && !existsSync(emitDir)) {
|
|
158
|
-
await rename(scratchDirs.
|
|
159
|
+
await rename(scratchDirs.previousDir, emitDir);
|
|
159
160
|
}
|
|
160
161
|
throw error;
|
|
161
162
|
}
|
|
@@ -167,13 +168,13 @@ function synthesizeCompilerOptions(packageDir, outdir) {
|
|
|
167
168
|
if (configFile.error) {
|
|
168
169
|
throw new Error(`nmr-compile: failed to read ${configPath}.\n${formatDiagnostics([configFile.error])}`);
|
|
169
170
|
}
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
throw new Error(`nmr-compile: failed to parse ${configPath}.\n${formatDiagnostics(
|
|
171
|
+
const parsedConfig = ts.parseJsonConfigFileContent(configFile.config, ts.sys, packageDir);
|
|
172
|
+
if (parsedConfig.errors.length > 0) {
|
|
173
|
+
throw new Error(`nmr-compile: failed to parse ${configPath}.\n${formatDiagnostics(parsedConfig.errors)}`);
|
|
173
174
|
}
|
|
174
175
|
const resolvedOutDir = path.resolve(packageDir, outdir);
|
|
175
176
|
return {
|
|
176
|
-
...
|
|
177
|
+
...parsedConfig.options,
|
|
177
178
|
noEmit: false,
|
|
178
179
|
emitDeclarationOnly: false,
|
|
179
180
|
declaration: true,
|
|
@@ -186,8 +187,8 @@ function synthesizeCompilerOptions(packageDir, outdir) {
|
|
|
186
187
|
noEmitOnError: false,
|
|
187
188
|
};
|
|
188
189
|
}
|
|
189
|
-
function writeStagedOutput(
|
|
190
|
-
for (const [fileName, file] of
|
|
190
|
+
function writeStagedOutput(stagedFiles, emitDir, stagingDir) {
|
|
191
|
+
for (const [fileName, file] of stagedFiles) {
|
|
191
192
|
if (!isWithin(emitDir, fileName)) {
|
|
192
193
|
throw new Error(`nmr-compile: the compiler emitted ${fileName}, which is outside the output directory ${emitDir}. ` +
|
|
193
194
|
"Verify the resolved tsconfig's 'outDir' and 'declarationDir'.");
|
|
@@ -199,57 +200,57 @@ function rewriteSpecifiers(outputFile, text, compilerOptions, sourceRoot) {
|
|
|
199
200
|
if (!isRewritableOutput(outputFile)) {
|
|
200
201
|
return text;
|
|
201
202
|
}
|
|
202
|
-
const sourceFile = ts.createSourceFile(outputFile, text, ts.ScriptTarget.Latest, false,
|
|
203
|
+
const sourceFile = ts.createSourceFile(outputFile, text, ts.ScriptTarget.Latest, false, resolveScriptKind(outputFile));
|
|
203
204
|
const aliasPrefixes = collectAliasPrefixes(compilerOptions);
|
|
204
205
|
const sourceContainingFile = mapOutputToSource(outputFile, compilerOptions, sourceRoot);
|
|
205
206
|
const edits = [];
|
|
206
|
-
|
|
207
|
+
visitModuleSpecifiers(sourceFile, (literal) => {
|
|
207
208
|
const replacement = resolveSpecifierReplacement(literal.text, sourceContainingFile, compilerOptions, sourceRoot, aliasPrefixes);
|
|
208
209
|
if (replacement === undefined) {
|
|
209
210
|
return;
|
|
210
211
|
}
|
|
211
|
-
const
|
|
212
|
-
const quote = text[
|
|
213
|
-
edits.push({
|
|
212
|
+
const startOffset = literal.getStart(sourceFile);
|
|
213
|
+
const quote = text[startOffset] ?? '"';
|
|
214
|
+
edits.push({ startOffset, endOffset: literal.getEnd(), text: `${quote}${replacement}${quote}` });
|
|
214
215
|
});
|
|
215
216
|
if (edits.length === 0) {
|
|
216
217
|
return text;
|
|
217
218
|
}
|
|
218
|
-
const orderedEdits = [...edits].sort((a, b) => b.
|
|
219
|
+
const orderedEdits = [...edits].sort((a, b) => b.startOffset - a.startOffset);
|
|
219
220
|
let updatedText = text;
|
|
220
221
|
for (const edit of orderedEdits) {
|
|
221
|
-
updatedText = updatedText.slice(0, edit.
|
|
222
|
+
updatedText = updatedText.slice(0, edit.startOffset) + edit.text + updatedText.slice(edit.endOffset);
|
|
222
223
|
}
|
|
223
224
|
return updatedText;
|
|
224
225
|
}
|
|
225
226
|
function resolveSpecifierReplacement(specifier, sourceContainingFile, compilerOptions, sourceRoot, aliasPrefixes) {
|
|
226
227
|
if (isRelativeSpecifier(specifier)) {
|
|
227
|
-
const
|
|
228
|
-
return
|
|
228
|
+
const rewrittenSpecifier = swapTypeScriptExtension(specifier);
|
|
229
|
+
return rewrittenSpecifier === specifier ? undefined : rewrittenSpecifier;
|
|
229
230
|
}
|
|
230
231
|
if (aliasPrefixes.every((prefix) => !(specifier === prefix || specifier.startsWith(prefix)))) {
|
|
231
232
|
return undefined;
|
|
232
233
|
}
|
|
233
|
-
const
|
|
234
|
-
if (!
|
|
234
|
+
const resolvedModule = ts.resolveModuleName(specifier, sourceContainingFile, compilerOptions, ts.sys).resolvedModule;
|
|
235
|
+
if (!resolvedModule) {
|
|
235
236
|
throw new Error(`nmr-compile: could not resolve aliased import '${specifier}' from ${sourceContainingFile}. ` +
|
|
236
237
|
`Verify the tsconfig 'paths' mapping and that the target file exists.`);
|
|
237
238
|
}
|
|
238
|
-
if (!isWithin(sourceRoot,
|
|
239
|
+
if (!isWithin(sourceRoot, resolvedModule.resolvedFileName)) {
|
|
239
240
|
const { paths: _paths, baseUrl: _baseUrl, rootDirs: _rootDirs, ...nodeResolutionOptions } = compilerOptions;
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
241
|
+
const bareResolvedModule = ts.resolveModuleName(specifier, sourceContainingFile, nodeResolutionOptions, ts.sys).resolvedModule;
|
|
242
|
+
if (bareResolvedModule) {
|
|
242
243
|
return undefined;
|
|
243
244
|
}
|
|
244
245
|
throw new Error(`nmr-compile: aliased import '${specifier}' from ${sourceContainingFile} resolves to ` +
|
|
245
|
-
`${
|
|
246
|
+
`${resolvedModule.resolvedFileName}, outside the package source root ${sourceRoot}, and does not resolve ` +
|
|
246
247
|
`the way Node will at runtime, which ignores tsconfig 'paths', 'baseUrl', and 'rootDirs'. The ` +
|
|
247
248
|
`emitted specifier would fail at runtime; re-anchor the alias inside the package.`);
|
|
248
249
|
}
|
|
249
|
-
const
|
|
250
|
-
return swapTypeScriptExtension(
|
|
250
|
+
const relativeSpecifier = buildRelativeSpecifier(path.dirname(sourceContainingFile), resolvedModule.resolvedFileName);
|
|
251
|
+
return swapTypeScriptExtension(relativeSpecifier);
|
|
251
252
|
}
|
|
252
|
-
function
|
|
253
|
+
function visitModuleSpecifiers(sourceFile, visit) {
|
|
253
254
|
function walk(node) {
|
|
254
255
|
const specifier = getModuleSpecifier(node);
|
|
255
256
|
if (specifier !== undefined) {
|
|
@@ -275,19 +276,19 @@ function getModuleSpecifier(node) {
|
|
|
275
276
|
}
|
|
276
277
|
return undefined;
|
|
277
278
|
}
|
|
278
|
-
async function detectBuildChanges(packageDir, files, emitConfig, toolchain, cachePath,
|
|
279
|
+
async function detectBuildChanges(packageDir, files, emitConfig, toolchain, cachePath, isOutputPresent, style) {
|
|
279
280
|
const packageName = path.basename(packageDir);
|
|
280
281
|
const previousHash = await readCacheEntry(cachePath);
|
|
281
282
|
const currentHash = await computeBuildHash(packageDir, files, emitConfig, toolchain);
|
|
282
283
|
if (previousHash === currentHash) {
|
|
283
|
-
if (
|
|
284
|
-
console.info(`${
|
|
284
|
+
if (isOutputPresent) {
|
|
285
|
+
console.info(`${STATUS_GLYPHS[style].skipped.text} ${packageName}: No changes detected. Skipping build.`);
|
|
285
286
|
return { changed: false, currentHash };
|
|
286
287
|
}
|
|
287
|
-
console.info(`${
|
|
288
|
+
console.info(formatGlyphLine(NMR_GLYPHS, style, 'package', `${packageName}: Build output is missing. Rebuilding.`));
|
|
288
289
|
return { changed: true, currentHash };
|
|
289
290
|
}
|
|
290
|
-
console.info(`${
|
|
291
|
+
console.info(formatGlyphLine(NMR_GLYPHS, style, 'package', `${packageName}: Changes detected.`));
|
|
291
292
|
return { changed: true, currentHash };
|
|
292
293
|
}
|
|
293
294
|
function assertSupportedTypeScript() {
|
|
@@ -347,12 +348,12 @@ function resolveExtendsTarget(extendsEntry, fromConfigPath) {
|
|
|
347
348
|
}
|
|
348
349
|
throw new Error(`nmr-compile: ${fromConfigPath} extends '${extendsEntry}', which does not exist.`);
|
|
349
350
|
}
|
|
350
|
-
const
|
|
351
|
+
const resolvedPath = resolvePackageSpecifier(extendsEntry, fromConfigPath) ??
|
|
351
352
|
resolvePackageSpecifier(`${extendsEntry}/tsconfig.json`, fromConfigPath);
|
|
352
|
-
if (
|
|
353
|
+
if (resolvedPath === undefined) {
|
|
353
354
|
throw new Error(`nmr-compile: ${fromConfigPath} extends '${extendsEntry}', which does not resolve to a file.`);
|
|
354
355
|
}
|
|
355
|
-
return
|
|
356
|
+
return resolvedPath;
|
|
356
357
|
}
|
|
357
358
|
function resolvePackageSpecifier(specifier, fromConfigPath) {
|
|
358
359
|
const { resolvedModule } = ts.resolveModuleName(specifier, fromConfigPath, { moduleResolution: ts.ModuleResolutionKind.NodeNext, resolveJsonModule: true }, ts.sys);
|
|
@@ -362,8 +363,8 @@ function isRelativeSpecifier(specifier) {
|
|
|
362
363
|
return specifier.startsWith('./') || specifier.startsWith('../');
|
|
363
364
|
}
|
|
364
365
|
function isWithin(parent, child) {
|
|
365
|
-
const
|
|
366
|
-
return
|
|
366
|
+
const relativePath = path.relative(parent, child);
|
|
367
|
+
return relativePath !== '' && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
|
|
367
368
|
}
|
|
368
369
|
function mapOutputToSource(outputFile, compilerOptions, sourceRoot) {
|
|
369
370
|
const outDir = compilerOptions.outDir ?? path.dirname(outputFile);
|
|
@@ -371,11 +372,11 @@ function mapOutputToSource(outputFile, compilerOptions, sourceRoot) {
|
|
|
371
372
|
const withoutExtension = relativeFromOut.replace(/\.d\.ts$|\.js$/, '');
|
|
372
373
|
return path.join(sourceRoot, `${withoutExtension}.ts`);
|
|
373
374
|
}
|
|
374
|
-
function
|
|
375
|
-
const
|
|
376
|
-
return
|
|
375
|
+
function buildRelativeSpecifier(fromDir, targetFile) {
|
|
376
|
+
const relativePath = path.relative(fromDir, targetFile).split(path.sep).join('/');
|
|
377
|
+
return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
|
|
377
378
|
}
|
|
378
|
-
function
|
|
379
|
+
function resolveScriptKind(file) {
|
|
379
380
|
return file.endsWith('.d.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS;
|
|
380
381
|
}
|
|
381
382
|
function swapTypeScriptExtension(specifier) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
1
2
|
/**
|
|
2
3
|
* Removes a package's build output and the build-cache entry that describes it, so no state survives to
|
|
3
4
|
* make the next build skip. Removal is idempotent: an already-clean package is a silent no-op.
|
|
4
5
|
*/
|
|
5
|
-
export declare function cleanPackage(packageDir: string): Promise<void>;
|
|
6
|
+
export declare function cleanPackage(packageDir: string, style: OutputStyle): Promise<void>;
|
|
6
7
|
/**
|
|
7
8
|
* Cleans the package containing `cwd`, or every workspace package when run from the monorepo root.
|
|
8
9
|
*
|
|
@@ -14,4 +15,4 @@ export declare function cleanPackage(packageDir: string): Promise<void>;
|
|
|
14
15
|
* a `rimraf` in nmr's dependency tree would be unreachable — while nmr's own bins are linked into the
|
|
15
16
|
* consumer's `node_modules/.bin`, nmr being a direct dependency.
|
|
16
17
|
*/
|
|
17
|
-
export declare function runClean(cwd
|
|
18
|
+
export declare function runClean(cwd: string, style: OutputStyle): Promise<void>;
|
|
@@ -1,66 +1,67 @@
|
|
|
1
1
|
import { rm } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { formatGlyphLine } from '@williamthorsen/nmr-core';
|
|
3
4
|
import { removeCheckCache } from "../check-cache.js";
|
|
4
5
|
import { loadRootConfig } from "../config.js";
|
|
5
6
|
import { findContainingPackageDir } from "../context.js";
|
|
7
|
+
import { NMR_GLYPHS } from "../glyphs.js";
|
|
6
8
|
import { reportClosing } from "../helpers/reportClosing.js";
|
|
7
9
|
import { applyDevBin, buildWorkspaceRegistry, resolveScript } from "../resolver.js";
|
|
8
10
|
import { resolveChannel, runCommand } from "../runner.js";
|
|
9
11
|
import { renderChain } from "../steps.js";
|
|
10
12
|
import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
|
|
11
13
|
import { resolveBuildCachePath } from "./build-output.js";
|
|
12
|
-
const CLEAN_ICON = '🧹';
|
|
13
14
|
const CLEAN_COMMAND = 'clean';
|
|
14
15
|
const BUILT_IN_CLEAN = 'nmr-clean';
|
|
15
16
|
const OUTPUT_ROOT = 'dist';
|
|
16
|
-
export async function cleanPackage(packageDir) {
|
|
17
|
+
export async function cleanPackage(packageDir, style) {
|
|
17
18
|
await rm(path.resolve(packageDir, OUTPUT_ROOT), { recursive: true, force: true });
|
|
18
19
|
await rm(resolveBuildCachePath(packageDir), { force: true });
|
|
19
|
-
console.info(`${
|
|
20
|
+
console.info(formatGlyphLine(NMR_GLYPHS, style, 'clean', `${path.basename(packageDir)}: Removed build output and cache.`));
|
|
20
21
|
}
|
|
21
|
-
export async function runClean(cwd
|
|
22
|
+
export async function runClean(cwd, style) {
|
|
22
23
|
let monorepoRoot;
|
|
23
24
|
try {
|
|
24
25
|
monorepoRoot = findMonorepoRoot(cwd);
|
|
25
26
|
}
|
|
26
27
|
catch {
|
|
27
|
-
await clearCheckCache(cwd);
|
|
28
|
-
await cleanPackage(cwd);
|
|
28
|
+
await clearCheckCache(cwd, style);
|
|
29
|
+
await cleanPackage(cwd, style);
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
|
-
await clearCheckCache(monorepoRoot);
|
|
32
|
+
await clearCheckCache(monorepoRoot, style);
|
|
32
33
|
const workspacePackageDirs = getWorkspacePackageDirs(monorepoRoot);
|
|
33
34
|
const packageDir = findContainingPackageDir(cwd, workspacePackageDirs);
|
|
34
35
|
if (packageDir !== undefined) {
|
|
35
|
-
await cleanPackage(packageDir);
|
|
36
|
+
await cleanPackage(packageDir, style);
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
38
|
-
await sweepWorkspace(monorepoRoot, workspacePackageDirs);
|
|
39
|
+
await sweepWorkspace(monorepoRoot, workspacePackageDirs, style);
|
|
39
40
|
}
|
|
40
|
-
async function clearCheckCache(scopeDir) {
|
|
41
|
+
async function clearCheckCache(scopeDir, style) {
|
|
41
42
|
await removeCheckCache(scopeDir);
|
|
42
|
-
console.info(
|
|
43
|
+
console.info(formatGlyphLine(NMR_GLYPHS, style, 'clean', 'Removed all recorded check results.'));
|
|
43
44
|
}
|
|
44
|
-
function describeSweep(
|
|
45
|
-
const packages =
|
|
46
|
-
const skipClause =
|
|
45
|
+
function describeSweep(cleanedCount, skippedCount) {
|
|
46
|
+
const packages = cleanedCount === 1 ? '1 package' : `${cleanedCount} packages`;
|
|
47
|
+
const skipClause = skippedCount === 0 ? '' : `, skipping ${skippedCount} with an empty clean override`;
|
|
47
48
|
return `Cleaned ${packages}${skipClause}.`;
|
|
48
49
|
}
|
|
49
|
-
async function sweepWorkspace(monorepoRoot, workspacePackageDirs) {
|
|
50
|
+
async function sweepWorkspace(monorepoRoot, workspacePackageDirs, style) {
|
|
50
51
|
const config = await loadRootConfig(monorepoRoot);
|
|
51
52
|
const registry = buildWorkspaceRegistry(config);
|
|
52
|
-
let
|
|
53
|
-
let
|
|
53
|
+
let cleanedCount = 0;
|
|
54
|
+
let skippedCount = 0;
|
|
54
55
|
for (const packageDir of workspacePackageDirs) {
|
|
55
|
-
const
|
|
56
|
-
const resolvedCommand =
|
|
56
|
+
const resolvedScript = resolveScript(CLEAN_COMMAND, registry, packageDir, false);
|
|
57
|
+
const resolvedCommand = resolvedScript === undefined ? undefined : renderChain(resolvedScript.steps);
|
|
57
58
|
if (resolvedCommand === undefined || resolvedCommand === '') {
|
|
58
|
-
|
|
59
|
+
skippedCount++;
|
|
59
60
|
continue;
|
|
60
61
|
}
|
|
61
62
|
if (resolvedCommand === BUILT_IN_CLEAN) {
|
|
62
|
-
await cleanPackage(packageDir);
|
|
63
|
-
|
|
63
|
+
await cleanPackage(packageDir, style);
|
|
64
|
+
cleanedCount++;
|
|
64
65
|
continue;
|
|
65
66
|
}
|
|
66
67
|
const command = applyDevBin(resolvedCommand, config.devBin, monorepoRoot);
|
|
@@ -70,7 +71,7 @@ async function sweepWorkspace(monorepoRoot, workspacePackageDirs) {
|
|
|
70
71
|
if (exitCode !== 0) {
|
|
71
72
|
throw new Error(`nmr-clean: \`${command}\` failed in ${path.basename(packageDir)} with exit code ${exitCode}.`);
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
+
cleanedCount++;
|
|
74
75
|
}
|
|
75
|
-
reportClosing(
|
|
76
|
+
reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'clean', describeSweep(cleanedCount, skippedCount)));
|
|
76
77
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
1
2
|
export interface PackageHookStatus {
|
|
2
3
|
packageName: string;
|
|
3
4
|
packageDir: string;
|
|
@@ -15,15 +16,15 @@ export declare const DEFAULT_HOOK = "npm run build";
|
|
|
15
16
|
* have a `prepublishOnly` script.
|
|
16
17
|
*/
|
|
17
18
|
export declare function ensurePrepublishHooks(monorepoRoot: string, options: {
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
shouldFix: boolean;
|
|
20
|
+
isDryRun: boolean;
|
|
20
21
|
command?: string;
|
|
21
22
|
}): EnsurePrepublishHooksResult;
|
|
22
23
|
/**
|
|
23
24
|
* Reports one line per publishable package and closes with what the run came to. Private packages appear in
|
|
24
25
|
* neither: they publish nothing, so a `prepublishOnly` is not theirs to carry.
|
|
25
26
|
*
|
|
26
|
-
* Every line goes to stdout,
|
|
27
|
+
* Every line goes to stdout, a failure's included, and the exit code is what carries a failure. A report split
|
|
27
28
|
* across two streams leaves no reader holding the whole list.
|
|
28
29
|
*/
|
|
29
|
-
export declare function reportPrepublishHooks(result: EnsurePrepublishHooksResult, hookCommand: string): void;
|
|
30
|
+
export declare function reportPrepublishHooks(result: EnsurePrepublishHooksResult, hookCommand: string, style: OutputStyle): void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { STATUS_GLYPHS } from '@williamthorsen/nmr-core';
|
|
3
4
|
import { readPackageJson } from "../helpers/package-json.js";
|
|
4
5
|
import { reportClosing } from "../helpers/reportClosing.js";
|
|
5
6
|
import { isObject } from "../helpers/type-guards.js";
|
|
@@ -10,33 +11,33 @@ export function ensurePrepublishHooks(monorepoRoot, options) {
|
|
|
10
11
|
const packageDirs = getWorkspacePackageDirs(monorepoRoot);
|
|
11
12
|
const packages = [];
|
|
12
13
|
for (const packageDir of packageDirs) {
|
|
13
|
-
const
|
|
14
|
-
const packageName =
|
|
15
|
-
const isPrivate =
|
|
14
|
+
const packageJson = readPackageJson(packageDir);
|
|
15
|
+
const packageName = packageJson.name ?? path.basename(packageDir);
|
|
16
|
+
const isPrivate = packageJson.private === true;
|
|
16
17
|
if (isPrivate) {
|
|
17
18
|
packages.push({
|
|
18
19
|
packageName,
|
|
19
20
|
packageDir,
|
|
20
21
|
isPrivate: true,
|
|
21
|
-
prepublishOnly:
|
|
22
|
+
prepublishOnly: packageJson.scripts?.['prepublishOnly'],
|
|
22
23
|
action: 'ok',
|
|
23
24
|
});
|
|
24
25
|
continue;
|
|
25
26
|
}
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
27
|
+
const existingHook = packageJson.scripts?.['prepublishOnly'];
|
|
28
|
+
if (existingHook) {
|
|
28
29
|
packages.push({
|
|
29
30
|
packageName,
|
|
30
31
|
packageDir,
|
|
31
32
|
isPrivate: false,
|
|
32
|
-
prepublishOnly:
|
|
33
|
+
prepublishOnly: existingHook,
|
|
33
34
|
action: 'ok',
|
|
34
35
|
});
|
|
35
36
|
continue;
|
|
36
37
|
}
|
|
37
|
-
if (options.
|
|
38
|
-
const action = options.
|
|
39
|
-
if (!options.
|
|
38
|
+
if (options.shouldFix) {
|
|
39
|
+
const action = options.isDryRun ? 'would-fix' : 'fixed';
|
|
40
|
+
if (!options.isDryRun) {
|
|
40
41
|
addPrepublishOnly(packageDir, hookCommand);
|
|
41
42
|
}
|
|
42
43
|
packages.push({
|
|
@@ -60,57 +61,58 @@ export function ensurePrepublishHooks(monorepoRoot, options) {
|
|
|
60
61
|
const hasFailures = packages.some((p) => p.action === 'missing');
|
|
61
62
|
return { packages, hasFailures };
|
|
62
63
|
}
|
|
63
|
-
export function reportPrepublishHooks(result, hookCommand) {
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
64
|
+
export function reportPrepublishHooks(result, hookCommand, style) {
|
|
65
|
+
const publishablePackages = result.packages.filter((packageStatus) => !packageStatus.isPrivate);
|
|
66
|
+
if (publishablePackages.length === 0) {
|
|
66
67
|
console.info('No publishable packages found.');
|
|
67
68
|
return;
|
|
68
69
|
}
|
|
69
|
-
for (const
|
|
70
|
-
console.info(renderHookStatus(
|
|
70
|
+
for (const packageStatus of publishablePackages) {
|
|
71
|
+
console.info(renderHookStatus(packageStatus, hookCommand, style));
|
|
71
72
|
}
|
|
72
|
-
reportClosing(describeHookRun(
|
|
73
|
+
reportClosing(describeHookRun(publishablePackages));
|
|
73
74
|
}
|
|
74
75
|
function addPrepublishOnly(packageDir, command) {
|
|
75
76
|
const filePath = path.join(packageDir, 'package.json');
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
if (!isObject(
|
|
77
|
+
const rawText = readFileSync(filePath, 'utf8');
|
|
78
|
+
const parsedManifest = JSON.parse(rawText);
|
|
79
|
+
if (!isObject(parsedManifest)) {
|
|
79
80
|
throw new TypeError(`Invalid package.json in ${packageDir}: expected an object`);
|
|
80
81
|
}
|
|
81
|
-
const scripts = isObject(
|
|
82
|
+
const scripts = isObject(parsedManifest['scripts']) ? parsedManifest['scripts'] : {};
|
|
82
83
|
scripts['prepublishOnly'] = command;
|
|
83
|
-
|
|
84
|
-
writeFileSync(filePath, JSON.stringify(
|
|
84
|
+
parsedManifest['scripts'] = scripts;
|
|
85
|
+
writeFileSync(filePath, JSON.stringify(parsedManifest, null, 2) + '\n', 'utf8');
|
|
85
86
|
}
|
|
86
|
-
function countAction(
|
|
87
|
-
return
|
|
87
|
+
function countAction(publishablePackages, action) {
|
|
88
|
+
return publishablePackages.filter((packageStatus) => packageStatus.action === action).length;
|
|
88
89
|
}
|
|
89
|
-
function describeHookRun(
|
|
90
|
-
const packages =
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
93
|
-
return `${
|
|
90
|
+
function describeHookRun(publishablePackages) {
|
|
91
|
+
const packages = publishablePackages.length === 1 ? '1 publishable package' : `${publishablePackages.length} publishable packages`;
|
|
92
|
+
const missingCount = countAction(publishablePackages, 'missing');
|
|
93
|
+
if (missingCount > 0) {
|
|
94
|
+
return `${missingCount} of ${packages} ${missingCount === 1 ? 'is' : 'are'} missing prepublishOnly. Run with --fix to add it.`;
|
|
94
95
|
}
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
return `Added prepublishOnly to ${
|
|
96
|
+
const fixedCount = countAction(publishablePackages, 'fixed');
|
|
97
|
+
if (fixedCount > 0) {
|
|
98
|
+
return `Added prepublishOnly to ${fixedCount} of ${packages}.`;
|
|
98
99
|
}
|
|
99
|
-
const
|
|
100
|
-
if (
|
|
101
|
-
return `Would add prepublishOnly to ${
|
|
100
|
+
const wouldFixCount = countAction(publishablePackages, 'would-fix');
|
|
101
|
+
if (wouldFixCount > 0) {
|
|
102
|
+
return `Would add prepublishOnly to ${wouldFixCount} of ${packages}.`;
|
|
102
103
|
}
|
|
103
|
-
return `${packages} ${
|
|
104
|
+
return `${packages} ${publishablePackages.length === 1 ? 'has' : 'have'} prepublishOnly.`;
|
|
104
105
|
}
|
|
105
|
-
function renderHookStatus(
|
|
106
|
-
|
|
106
|
+
function renderHookStatus(packageStatus, hookCommand, style) {
|
|
107
|
+
const statuses = STATUS_GLYPHS[style];
|
|
108
|
+
switch (packageStatus.action) {
|
|
107
109
|
case 'ok':
|
|
108
|
-
return
|
|
110
|
+
return `${statuses.passed.text} ${packageStatus.packageName}: prepublishOnly = "${packageStatus.prepublishOnly}"`;
|
|
109
111
|
case 'missing':
|
|
110
|
-
return
|
|
112
|
+
return `${statuses.failed.text} ${packageStatus.packageName}: missing prepublishOnly`;
|
|
111
113
|
case 'fixed':
|
|
112
|
-
return
|
|
114
|
+
return `${statuses.passed.text} ${packageStatus.packageName}: added prepublishOnly = "${hookCommand}"`;
|
|
113
115
|
case 'would-fix':
|
|
114
|
-
return `~ ${
|
|
116
|
+
return `~ ${packageStatus.packageName}: would add prepublishOnly = "${hookCommand}"`;
|
|
115
117
|
}
|
|
116
118
|
}
|