@williamthorsen/nmr 0.38.0 → 0.40.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 +212 -4
- package/.readyup/kits/default.js +124 -104
- package/.readyup/manifest.json +12 -7
- package/CHANGELOG.md +108 -0
- package/README.md +41 -47
- package/agents/README.md +8 -8
- package/agents/guidance/rulebooks/nmr.md +8 -8
- package/bin/ensure-prepublish-hooks.js +1 -1
- package/bin/nmr-clean.js +1 -1
- package/bin/nmr-compile.js +1 -1
- package/bin/nmr-fmt.js +1 -1
- package/bin/nmr-report-catalog.js +1 -1
- package/bin/nmr-report-overrides.js +1 -1
- package/bin/nmr-taze.js +1 -1
- package/bin/nmr.js +1 -1
- package/dist/esm/UserError.d.ts +1 -2
- package/dist/esm/check-cache.d.ts +61 -63
- package/dist/esm/check-cache.js +24 -24
- package/dist/esm/cli-ensure-prepublish-hooks.js +4 -4
- package/dist/esm/commands/build-output.d.ts +37 -38
- package/dist/esm/commands/build-output.js +9 -9
- package/dist/esm/commands/build.d.ts +13 -14
- package/dist/esm/commands/build.js +67 -65
- package/dist/esm/commands/clean.d.ts +6 -6
- package/dist/esm/commands/clean.js +11 -11
- package/dist/esm/commands/ensure-prepublish-hooks.d.ts +7 -7
- package/dist/esm/commands/ensure-prepublish-hooks.js +41 -41
- package/dist/esm/commands/fmt.d.ts +8 -8
- package/dist/esm/commands/fmt.js +33 -33
- package/dist/esm/commands/report-catalog.d.ts +5 -7
- package/dist/esm/commands/report-catalog.js +10 -8
- package/dist/esm/commands/report-overrides.d.ts +0 -2
- package/dist/esm/commands/report-overrides.js +8 -8
- package/dist/esm/commands/taze.d.ts +9 -8
- package/dist/esm/config.d.ts +5 -5
- package/dist/esm/config.js +27 -27
- package/dist/esm/context.d.ts +1 -2
- package/dist/esm/context.js +8 -8
- package/dist/esm/default-scripts.d.ts +5 -6
- package/dist/esm/defineConfig.d.ts +1 -1
- package/dist/esm/git-ignored-paths.d.ts +9 -0
- package/dist/esm/git-ignored-paths.js +10 -0
- package/dist/esm/glyphs.d.ts +3 -3
- package/dist/esm/help.d.ts +4 -4
- package/dist/esm/help.js +21 -21
- package/dist/esm/helpers/clampToBytes.d.ts +4 -5
- package/dist/esm/helpers/clampToBytes.js +3 -3
- package/dist/esm/helpers/config-path.d.ts +0 -2
- package/dist/esm/helpers/createBoundedBuffer.d.ts +3 -3
- package/dist/esm/helpers/createBoundedBuffer.js +2 -2
- package/dist/esm/helpers/deriveExcerpt.d.ts +4 -5
- package/dist/esm/helpers/duration.d.ts +10 -11
- package/dist/esm/helpers/duration.js +7 -7
- package/dist/esm/helpers/filter-selection.d.ts +19 -19
- package/dist/esm/helpers/filter-selection.js +12 -12
- package/dist/esm/helpers/findClosestName.d.ts +1 -1
- package/dist/esm/helpers/findClosestName.js +15 -15
- package/dist/esm/helpers/hook-name.d.ts +2 -5
- package/dist/esm/helpers/package-json.d.ts +11 -10
- package/dist/esm/helpers/package-json.js +26 -25
- package/dist/esm/helpers/readStringValues.d.ts +2 -3
- package/dist/esm/helpers/reportClosing.d.ts +8 -9
- package/dist/esm/helpers/transcript.d.ts +15 -15
- package/dist/esm/helpers/transcript.js +14 -14
- package/dist/esm/helpers/type-guards.d.ts +1 -0
- package/dist/esm/output-style.d.ts +12 -13
- package/dist/esm/prettier.d.ts +2 -2
- package/dist/esm/prettier.js +5 -5
- package/dist/esm/recording.d.ts +25 -20
- package/dist/esm/recording.js +28 -21
- package/dist/esm/replay-assembly.d.ts +10 -10
- package/dist/esm/report-format.d.ts +15 -20
- package/dist/esm/report-format.js +7 -7
- package/dist/esm/reportCliFailure.d.ts +2 -3
- package/dist/esm/resolve-scripts.d.ts +4 -5
- package/dist/esm/resolveBinStyles.d.ts +4 -5
- package/dist/esm/resolveBinStyles.js +3 -3
- package/dist/esm/resolver.d.ts +16 -20
- package/dist/esm/resolver.js +15 -15
- package/dist/esm/runCli.d.ts +6 -6
- package/dist/esm/runCli.js +261 -195
- package/dist/esm/runner.d.ts +20 -20
- package/dist/esm/runner.js +18 -18
- package/dist/esm/steps.d.ts +51 -38
- package/dist/esm/steps.js +32 -27
- package/dist/esm/taze.d.ts +1 -1
- package/dist/esm/tests.d.ts +23 -18
- package/dist/esm/tests.js +21 -9
- package/dist/esm/tiers.d.ts +28 -24
- package/dist/esm/tiers.js +13 -9
- package/dist/esm/types.d.ts +5 -5
- package/dist/esm/verbosity.d.ts +23 -21
- package/dist/esm/verbosity.js +7 -7
- package/dist/esm/verdict.d.ts +39 -32
- package/dist/esm/verdict.js +63 -50
- package/dist/esm/vitest-source-resolution.d.ts +11 -11
- package/dist/esm/vitest-source-resolution.js +49 -47
- package/dist/esm/vitest.d.ts +27 -26
- package/dist/esm/vitest.js +90 -33
- package/dist/esm/workspace.d.ts +6 -22
- package/dist/esm/workspace.js +9 -67
- package/docs/check-cache.md +57 -57
- package/docs/prettier.md +10 -10
- package/docs/reporting.md +48 -44
- package/docs/scripts.md +29 -29
- package/docs/upgrades.md +18 -18
- package/docs/utilities.md +13 -13
- package/docs/vitest.md +68 -66
- package/docs/workspace.md +27 -4
- package/package.json +6 -6
- package/dist/esm/helpers/workspace-patterns.d.ts +0 -8
- package/dist/esm/helpers/workspace-patterns.js +0 -29
|
@@ -60,18 +60,18 @@ export function resolveTsconfigChain(packageDir, configFileName = 'tsconfig.json
|
|
|
60
60
|
const resolvedChain = [];
|
|
61
61
|
const seen = new Set();
|
|
62
62
|
function walk(configPath) {
|
|
63
|
-
const
|
|
64
|
-
if (seen.has(
|
|
63
|
+
const normalizedPath = path.resolve(configPath);
|
|
64
|
+
if (seen.has(normalizedPath)) {
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
-
seen.add(
|
|
68
|
-
resolvedChain.push(
|
|
69
|
-
const configFile = ts.readConfigFile(
|
|
67
|
+
seen.add(normalizedPath);
|
|
68
|
+
resolvedChain.push(normalizedPath);
|
|
69
|
+
const configFile = ts.readConfigFile(normalizedPath, (fileName) => ts.sys.readFile(fileName));
|
|
70
70
|
if (configFile.error) {
|
|
71
|
-
throw new Error(`nmr-compile: failed to read ${
|
|
71
|
+
throw new Error(`nmr-compile: failed to read ${normalizedPath}.\n${formatDiagnostics([configFile.error])}`);
|
|
72
72
|
}
|
|
73
73
|
for (const entry of normalizeExtendsField(configFile.config)) {
|
|
74
|
-
walk(resolveExtendsTarget(entry,
|
|
74
|
+
walk(resolveExtendsTarget(entry, normalizedPath));
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
walk(path.resolve(packageDir, configFileName));
|
|
@@ -84,26 +84,26 @@ async function emitPackage(packageDir, entryPoints, outdir) {
|
|
|
84
84
|
const emitDir = resolveEmitDir(packageDir, outdir);
|
|
85
85
|
const scratchDirs = resolveScratchDirs(emitDir);
|
|
86
86
|
await removeScratchDirs(scratchDirs);
|
|
87
|
-
const
|
|
87
|
+
const emittedFiles = new Map();
|
|
88
88
|
function collect(fileName, text, writeByteOrderMark) {
|
|
89
|
-
|
|
89
|
+
emittedFiles.set(fileName, { text, writeByteOrderMark });
|
|
90
90
|
}
|
|
91
91
|
const host = createCachingCompilerHost(compilerOptions);
|
|
92
92
|
const scriptProgram = ts.createProgram(rootNames, { ...compilerOptions, declaration: false }, host);
|
|
93
93
|
assertEmitSucceeded(scriptProgram.emit(undefined, collect));
|
|
94
94
|
const declarationProgram = ts.createProgram(rootNames, { ...compilerOptions, emitDeclarationOnly: true, removeComments: false }, host, scriptProgram);
|
|
95
95
|
assertEmitSucceeded(declarationProgram.emit(undefined, collect));
|
|
96
|
-
const
|
|
97
|
-
for (const [fileName, file] of
|
|
98
|
-
|
|
96
|
+
const stagedFiles = new Map();
|
|
97
|
+
for (const [fileName, file] of emittedFiles) {
|
|
98
|
+
stagedFiles.set(fileName, { ...file, text: rewriteSpecifiers(fileName, file.text, compilerOptions, sourceRoot) });
|
|
99
99
|
}
|
|
100
|
-
if (
|
|
100
|
+
if (stagedFiles.size === 0) {
|
|
101
101
|
await rm(emitDir, { force: true, recursive: true });
|
|
102
102
|
return 0;
|
|
103
103
|
}
|
|
104
|
-
writeStagedOutput(
|
|
104
|
+
writeStagedOutput(stagedFiles, emitDir, scratchDirs.stagingDir);
|
|
105
105
|
await swapIntoPlace(emitDir, scratchDirs);
|
|
106
|
-
return
|
|
106
|
+
return stagedFiles.size;
|
|
107
107
|
}
|
|
108
108
|
function assertEmitSucceeded(result) {
|
|
109
109
|
if (result.emitSkipped) {
|
|
@@ -112,14 +112,14 @@ function assertEmitSucceeded(result) {
|
|
|
112
112
|
}
|
|
113
113
|
function createCachingCompilerHost(compilerOptions) {
|
|
114
114
|
const host = ts.createCompilerHost(compilerOptions);
|
|
115
|
-
const
|
|
115
|
+
const parsedFiles = new Map();
|
|
116
116
|
const readSourceFile = host.getSourceFile.bind(host);
|
|
117
117
|
host.getSourceFile = (fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) => {
|
|
118
|
-
if (
|
|
119
|
-
return
|
|
118
|
+
if (parsedFiles.has(fileName)) {
|
|
119
|
+
return parsedFiles.get(fileName);
|
|
120
120
|
}
|
|
121
121
|
const sourceFile = readSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
|
|
122
|
-
|
|
122
|
+
parsedFiles.set(fileName, sourceFile);
|
|
123
123
|
return sourceFile;
|
|
124
124
|
};
|
|
125
125
|
return host;
|
|
@@ -132,29 +132,31 @@ async function discardScratchDirs(scratchDirs) {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
async function removeScratchDirs(scratchDirs) {
|
|
135
|
-
await rm(scratchDirs.
|
|
136
|
-
await rm(scratchDirs.
|
|
135
|
+
await rm(scratchDirs.previousDir, { force: true, recursive: true });
|
|
136
|
+
await rm(scratchDirs.stagingDir, { force: true, recursive: true });
|
|
137
137
|
}
|
|
138
138
|
function resolveEmitDir(packageDir, outdir) {
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
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)) {
|
|
142
144
|
throw new Error(`nmr-compile: refusing to build into '${outdir}', which does not resolve inside the package. ` +
|
|
143
|
-
'The build replaces its output directory on each emit, so the directory must
|
|
145
|
+
'The build replaces its output directory on each emit, so the directory must be below the package root.');
|
|
144
146
|
}
|
|
145
|
-
return
|
|
147
|
+
return resolvedDir;
|
|
146
148
|
}
|
|
147
149
|
async function swapIntoPlace(emitDir, scratchDirs) {
|
|
148
150
|
const hadPreviousOutput = existsSync(emitDir);
|
|
149
151
|
if (hadPreviousOutput) {
|
|
150
|
-
await rename(emitDir, scratchDirs.
|
|
152
|
+
await rename(emitDir, scratchDirs.previousDir);
|
|
151
153
|
}
|
|
152
154
|
try {
|
|
153
|
-
await rename(scratchDirs.
|
|
155
|
+
await rename(scratchDirs.stagingDir, emitDir);
|
|
154
156
|
}
|
|
155
157
|
catch (error) {
|
|
156
158
|
if (hadPreviousOutput && !existsSync(emitDir)) {
|
|
157
|
-
await rename(scratchDirs.
|
|
159
|
+
await rename(scratchDirs.previousDir, emitDir);
|
|
158
160
|
}
|
|
159
161
|
throw error;
|
|
160
162
|
}
|
|
@@ -166,13 +168,13 @@ function synthesizeCompilerOptions(packageDir, outdir) {
|
|
|
166
168
|
if (configFile.error) {
|
|
167
169
|
throw new Error(`nmr-compile: failed to read ${configPath}.\n${formatDiagnostics([configFile.error])}`);
|
|
168
170
|
}
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
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)}`);
|
|
172
174
|
}
|
|
173
175
|
const resolvedOutDir = path.resolve(packageDir, outdir);
|
|
174
176
|
return {
|
|
175
|
-
...
|
|
177
|
+
...parsedConfig.options,
|
|
176
178
|
noEmit: false,
|
|
177
179
|
emitDeclarationOnly: false,
|
|
178
180
|
declaration: true,
|
|
@@ -185,8 +187,8 @@ function synthesizeCompilerOptions(packageDir, outdir) {
|
|
|
185
187
|
noEmitOnError: false,
|
|
186
188
|
};
|
|
187
189
|
}
|
|
188
|
-
function writeStagedOutput(
|
|
189
|
-
for (const [fileName, file] of
|
|
190
|
+
function writeStagedOutput(stagedFiles, emitDir, stagingDir) {
|
|
191
|
+
for (const [fileName, file] of stagedFiles) {
|
|
190
192
|
if (!isWithin(emitDir, fileName)) {
|
|
191
193
|
throw new Error(`nmr-compile: the compiler emitted ${fileName}, which is outside the output directory ${emitDir}. ` +
|
|
192
194
|
"Verify the resolved tsconfig's 'outDir' and 'declarationDir'.");
|
|
@@ -198,57 +200,57 @@ function rewriteSpecifiers(outputFile, text, compilerOptions, sourceRoot) {
|
|
|
198
200
|
if (!isRewritableOutput(outputFile)) {
|
|
199
201
|
return text;
|
|
200
202
|
}
|
|
201
|
-
const sourceFile = ts.createSourceFile(outputFile, text, ts.ScriptTarget.Latest, false,
|
|
203
|
+
const sourceFile = ts.createSourceFile(outputFile, text, ts.ScriptTarget.Latest, false, resolveScriptKind(outputFile));
|
|
202
204
|
const aliasPrefixes = collectAliasPrefixes(compilerOptions);
|
|
203
205
|
const sourceContainingFile = mapOutputToSource(outputFile, compilerOptions, sourceRoot);
|
|
204
206
|
const edits = [];
|
|
205
|
-
|
|
207
|
+
visitModuleSpecifiers(sourceFile, (literal) => {
|
|
206
208
|
const replacement = resolveSpecifierReplacement(literal.text, sourceContainingFile, compilerOptions, sourceRoot, aliasPrefixes);
|
|
207
209
|
if (replacement === undefined) {
|
|
208
210
|
return;
|
|
209
211
|
}
|
|
210
|
-
const
|
|
211
|
-
const quote = text[
|
|
212
|
-
edits.push({
|
|
212
|
+
const startOffset = literal.getStart(sourceFile);
|
|
213
|
+
const quote = text[startOffset] ?? '"';
|
|
214
|
+
edits.push({ startOffset, endOffset: literal.getEnd(), text: `${quote}${replacement}${quote}` });
|
|
213
215
|
});
|
|
214
216
|
if (edits.length === 0) {
|
|
215
217
|
return text;
|
|
216
218
|
}
|
|
217
|
-
const orderedEdits =
|
|
219
|
+
const orderedEdits = edits.toSorted((a, b) => b.startOffset - a.startOffset);
|
|
218
220
|
let updatedText = text;
|
|
219
221
|
for (const edit of orderedEdits) {
|
|
220
|
-
updatedText = updatedText.slice(0, edit.
|
|
222
|
+
updatedText = updatedText.slice(0, edit.startOffset) + edit.text + updatedText.slice(edit.endOffset);
|
|
221
223
|
}
|
|
222
224
|
return updatedText;
|
|
223
225
|
}
|
|
224
226
|
function resolveSpecifierReplacement(specifier, sourceContainingFile, compilerOptions, sourceRoot, aliasPrefixes) {
|
|
225
227
|
if (isRelativeSpecifier(specifier)) {
|
|
226
|
-
const
|
|
227
|
-
return
|
|
228
|
+
const rewrittenSpecifier = swapTypeScriptExtension(specifier);
|
|
229
|
+
return rewrittenSpecifier === specifier ? undefined : rewrittenSpecifier;
|
|
228
230
|
}
|
|
229
231
|
if (aliasPrefixes.every((prefix) => !(specifier === prefix || specifier.startsWith(prefix)))) {
|
|
230
232
|
return undefined;
|
|
231
233
|
}
|
|
232
|
-
const
|
|
233
|
-
if (!
|
|
234
|
+
const resolvedModule = ts.resolveModuleName(specifier, sourceContainingFile, compilerOptions, ts.sys).resolvedModule;
|
|
235
|
+
if (!resolvedModule) {
|
|
234
236
|
throw new Error(`nmr-compile: could not resolve aliased import '${specifier}' from ${sourceContainingFile}. ` +
|
|
235
237
|
`Verify the tsconfig 'paths' mapping and that the target file exists.`);
|
|
236
238
|
}
|
|
237
|
-
if (!isWithin(sourceRoot,
|
|
239
|
+
if (!isWithin(sourceRoot, resolvedModule.resolvedFileName)) {
|
|
238
240
|
const { paths: _paths, baseUrl: _baseUrl, rootDirs: _rootDirs, ...nodeResolutionOptions } = compilerOptions;
|
|
239
|
-
const
|
|
240
|
-
if (
|
|
241
|
+
const bareResolvedModule = ts.resolveModuleName(specifier, sourceContainingFile, nodeResolutionOptions, ts.sys).resolvedModule;
|
|
242
|
+
if (bareResolvedModule) {
|
|
241
243
|
return undefined;
|
|
242
244
|
}
|
|
243
245
|
throw new Error(`nmr-compile: aliased import '${specifier}' from ${sourceContainingFile} resolves to ` +
|
|
244
|
-
`${
|
|
246
|
+
`${resolvedModule.resolvedFileName}, outside the package source root ${sourceRoot}, and does not resolve ` +
|
|
245
247
|
`the way Node will at runtime, which ignores tsconfig 'paths', 'baseUrl', and 'rootDirs'. The ` +
|
|
246
248
|
`emitted specifier would fail at runtime; re-anchor the alias inside the package.`);
|
|
247
249
|
}
|
|
248
|
-
const
|
|
249
|
-
return swapTypeScriptExtension(
|
|
250
|
+
const relativeSpecifier = buildRelativeSpecifier(path.dirname(sourceContainingFile), resolvedModule.resolvedFileName);
|
|
251
|
+
return swapTypeScriptExtension(relativeSpecifier);
|
|
250
252
|
}
|
|
251
|
-
function
|
|
253
|
+
function visitModuleSpecifiers(sourceFile, visit) {
|
|
252
254
|
function walk(node) {
|
|
253
255
|
const specifier = getModuleSpecifier(node);
|
|
254
256
|
if (specifier !== undefined) {
|
|
@@ -274,12 +276,12 @@ function getModuleSpecifier(node) {
|
|
|
274
276
|
}
|
|
275
277
|
return undefined;
|
|
276
278
|
}
|
|
277
|
-
async function detectBuildChanges(packageDir, files, emitConfig, toolchain, cachePath,
|
|
279
|
+
async function detectBuildChanges(packageDir, files, emitConfig, toolchain, cachePath, isOutputPresent, style) {
|
|
278
280
|
const packageName = path.basename(packageDir);
|
|
279
281
|
const previousHash = await readCacheEntry(cachePath);
|
|
280
282
|
const currentHash = await computeBuildHash(packageDir, files, emitConfig, toolchain);
|
|
281
283
|
if (previousHash === currentHash) {
|
|
282
|
-
if (
|
|
284
|
+
if (isOutputPresent) {
|
|
283
285
|
console.info(`${STATUS_GLYPHS[style].skipped.text} ${packageName}: No changes detected. Skipping build.`);
|
|
284
286
|
return { changed: false, currentHash };
|
|
285
287
|
}
|
|
@@ -307,7 +309,7 @@ function collectAliasPrefixes(compilerOptions) {
|
|
|
307
309
|
}
|
|
308
310
|
function describeEmit(fileCount, outdir) {
|
|
309
311
|
if (fileCount === 0) {
|
|
310
|
-
return '
|
|
312
|
+
return 'Did not emit any output.';
|
|
311
313
|
}
|
|
312
314
|
return `Compiled ${fileCount} files to ${outdir.replace(/\/+$/u, '')}.`;
|
|
313
315
|
}
|
|
@@ -346,12 +348,12 @@ function resolveExtendsTarget(extendsEntry, fromConfigPath) {
|
|
|
346
348
|
}
|
|
347
349
|
throw new Error(`nmr-compile: ${fromConfigPath} extends '${extendsEntry}', which does not exist.`);
|
|
348
350
|
}
|
|
349
|
-
const
|
|
351
|
+
const resolvedPath = resolvePackageSpecifier(extendsEntry, fromConfigPath) ??
|
|
350
352
|
resolvePackageSpecifier(`${extendsEntry}/tsconfig.json`, fromConfigPath);
|
|
351
|
-
if (
|
|
353
|
+
if (resolvedPath === undefined) {
|
|
352
354
|
throw new Error(`nmr-compile: ${fromConfigPath} extends '${extendsEntry}', which does not resolve to a file.`);
|
|
353
355
|
}
|
|
354
|
-
return
|
|
356
|
+
return resolvedPath;
|
|
355
357
|
}
|
|
356
358
|
function resolvePackageSpecifier(specifier, fromConfigPath) {
|
|
357
359
|
const { resolvedModule } = ts.resolveModuleName(specifier, fromConfigPath, { moduleResolution: ts.ModuleResolutionKind.NodeNext, resolveJsonModule: true }, ts.sys);
|
|
@@ -361,8 +363,8 @@ function isRelativeSpecifier(specifier) {
|
|
|
361
363
|
return specifier.startsWith('./') || specifier.startsWith('../');
|
|
362
364
|
}
|
|
363
365
|
function isWithin(parent, child) {
|
|
364
|
-
const
|
|
365
|
-
return
|
|
366
|
+
const relativePath = path.relative(parent, child);
|
|
367
|
+
return relativePath !== '' && !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
|
|
366
368
|
}
|
|
367
369
|
function mapOutputToSource(outputFile, compilerOptions, sourceRoot) {
|
|
368
370
|
const outDir = compilerOptions.outDir ?? path.dirname(outputFile);
|
|
@@ -370,11 +372,11 @@ function mapOutputToSource(outputFile, compilerOptions, sourceRoot) {
|
|
|
370
372
|
const withoutExtension = relativeFromOut.replace(/\.d\.ts$|\.js$/, '');
|
|
371
373
|
return path.join(sourceRoot, `${withoutExtension}.ts`);
|
|
372
374
|
}
|
|
373
|
-
function
|
|
374
|
-
const
|
|
375
|
-
return
|
|
375
|
+
function buildRelativeSpecifier(fromDir, targetFile) {
|
|
376
|
+
const relativePath = path.relative(fromDir, targetFile).split(path.sep).join('/');
|
|
377
|
+
return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
|
|
376
378
|
}
|
|
377
|
-
function
|
|
379
|
+
function resolveScriptKind(file) {
|
|
378
380
|
return file.endsWith('.d.ts') ? ts.ScriptKind.TS : ts.ScriptKind.JS;
|
|
379
381
|
}
|
|
380
382
|
function swapTypeScriptExtension(specifier) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type OutputStyle } from '@williamthorsen/nmr-core';
|
|
2
2
|
/**
|
|
3
|
-
* Removes a package's build output and the build-cache entry that describes it, so
|
|
4
|
-
* make the next build skip. Removal is idempotent:
|
|
3
|
+
* Removes a package's build output and the build-cache entry that describes it, so the package does not keep
|
|
4
|
+
* any state that would make the next build skip. Removal is idempotent: An already-clean package is a silent
|
|
5
|
+
* no-op.
|
|
5
6
|
*/
|
|
6
7
|
export declare function cleanPackage(packageDir: string, style: OutputStyle): Promise<void>;
|
|
7
8
|
/**
|
|
@@ -10,9 +11,8 @@ export declare function cleanPackage(packageDir: string, style: OutputStyle): Pr
|
|
|
10
11
|
* From within a package this is the built-in clean itself: `nmr` has already resolved `clean` to this bin
|
|
11
12
|
* before invoking it, so resolving again would apply the package's own override twice.
|
|
12
13
|
*
|
|
13
|
-
* This is the default `clean` script,
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* consumer's `node_modules/.bin`, nmr being a direct dependency.
|
|
14
|
+
* This is the default `clean` script, and it is published as a bin because, under pnpm's isolated layout,
|
|
15
|
+
* `pnpm exec` resolves only the bins of the consuming project's direct dependencies: pnpm links nmr's own bins
|
|
16
|
+
* into the consumer's `node_modules/.bin`, but not the bins of nmr's dependencies.
|
|
17
17
|
*/
|
|
18
18
|
export declare function runClean(cwd: string, style: OutputStyle): Promise<void>;
|
|
@@ -42,26 +42,26 @@ async function clearCheckCache(scopeDir, style) {
|
|
|
42
42
|
await removeCheckCache(scopeDir);
|
|
43
43
|
console.info(formatGlyphLine(NMR_GLYPHS, style, 'clean', 'Removed all recorded check results.'));
|
|
44
44
|
}
|
|
45
|
-
function describeSweep(
|
|
46
|
-
const packages =
|
|
47
|
-
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`;
|
|
48
48
|
return `Cleaned ${packages}${skipClause}.`;
|
|
49
49
|
}
|
|
50
50
|
async function sweepWorkspace(monorepoRoot, workspacePackageDirs, style) {
|
|
51
51
|
const config = await loadRootConfig(monorepoRoot);
|
|
52
52
|
const registry = buildWorkspaceRegistry(config);
|
|
53
|
-
let
|
|
54
|
-
let
|
|
53
|
+
let cleanedCount = 0;
|
|
54
|
+
let skippedCount = 0;
|
|
55
55
|
for (const packageDir of workspacePackageDirs) {
|
|
56
|
-
const
|
|
57
|
-
const resolvedCommand =
|
|
56
|
+
const resolvedScript = resolveScript(CLEAN_COMMAND, registry, packageDir, false);
|
|
57
|
+
const resolvedCommand = resolvedScript === undefined ? undefined : renderChain(resolvedScript.steps);
|
|
58
58
|
if (resolvedCommand === undefined || resolvedCommand === '') {
|
|
59
|
-
|
|
59
|
+
skippedCount++;
|
|
60
60
|
continue;
|
|
61
61
|
}
|
|
62
62
|
if (resolvedCommand === BUILT_IN_CLEAN) {
|
|
63
63
|
await cleanPackage(packageDir, style);
|
|
64
|
-
|
|
64
|
+
cleanedCount++;
|
|
65
65
|
continue;
|
|
66
66
|
}
|
|
67
67
|
const command = applyDevBin(resolvedCommand, config.devBin, monorepoRoot);
|
|
@@ -71,7 +71,7 @@ async function sweepWorkspace(monorepoRoot, workspacePackageDirs, style) {
|
|
|
71
71
|
if (exitCode !== 0) {
|
|
72
72
|
throw new Error(`nmr-clean: \`${command}\` failed in ${path.basename(packageDir)} with exit code ${exitCode}.`);
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
cleanedCount++;
|
|
75
75
|
}
|
|
76
|
-
reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'clean', describeSweep(
|
|
76
|
+
reportClosing(formatGlyphLine(NMR_GLYPHS, style, 'clean', describeSweep(cleanedCount, skippedCount)));
|
|
77
77
|
}
|
|
@@ -12,19 +12,19 @@ export interface EnsurePrepublishHooksResult {
|
|
|
12
12
|
}
|
|
13
13
|
export declare const DEFAULT_HOOK = "npm run build";
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Checks (and optionally fixes) whether all publishable workspace packages
|
|
16
16
|
* have a `prepublishOnly` script.
|
|
17
17
|
*/
|
|
18
18
|
export declare function ensurePrepublishHooks(monorepoRoot: string, options: {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
shouldFix: boolean;
|
|
20
|
+
isDryRun: boolean;
|
|
21
21
|
command?: string;
|
|
22
22
|
}): EnsurePrepublishHooksResult;
|
|
23
23
|
/**
|
|
24
|
-
* Reports one line per publishable package and closes with
|
|
25
|
-
* neither:
|
|
24
|
+
* Reports one line per publishable package and closes with a summary of the run. Private packages appear in
|
|
25
|
+
* neither: They publish nothing, so they do not need a `prepublishOnly`.
|
|
26
26
|
*
|
|
27
|
-
* Every line goes to stdout, a failure's included, and the exit code
|
|
28
|
-
* across two streams
|
|
27
|
+
* Every line goes to stdout, a failure's included, and the exit code signals a failure. When a report is split
|
|
28
|
+
* across two streams, a reader of either stream does not see the whole list.
|
|
29
29
|
*/
|
|
30
30
|
export declare function reportPrepublishHooks(result: EnsurePrepublishHooksResult, hookCommand: string, style: OutputStyle): void;
|
|
@@ -11,33 +11,33 @@ export function ensurePrepublishHooks(monorepoRoot, options) {
|
|
|
11
11
|
const packageDirs = getWorkspacePackageDirs(monorepoRoot);
|
|
12
12
|
const packages = [];
|
|
13
13
|
for (const packageDir of packageDirs) {
|
|
14
|
-
const
|
|
15
|
-
const packageName =
|
|
16
|
-
const isPrivate =
|
|
14
|
+
const packageJson = readPackageJson(packageDir);
|
|
15
|
+
const packageName = packageJson.name ?? path.basename(packageDir);
|
|
16
|
+
const isPrivate = packageJson.private === true;
|
|
17
17
|
if (isPrivate) {
|
|
18
18
|
packages.push({
|
|
19
19
|
packageName,
|
|
20
20
|
packageDir,
|
|
21
21
|
isPrivate: true,
|
|
22
|
-
prepublishOnly:
|
|
22
|
+
prepublishOnly: packageJson.scripts?.['prepublishOnly'],
|
|
23
23
|
action: 'ok',
|
|
24
24
|
});
|
|
25
25
|
continue;
|
|
26
26
|
}
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
27
|
+
const existingHook = packageJson.scripts?.['prepublishOnly'];
|
|
28
|
+
if (existingHook) {
|
|
29
29
|
packages.push({
|
|
30
30
|
packageName,
|
|
31
31
|
packageDir,
|
|
32
32
|
isPrivate: false,
|
|
33
|
-
prepublishOnly:
|
|
33
|
+
prepublishOnly: existingHook,
|
|
34
34
|
action: 'ok',
|
|
35
35
|
});
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
|
-
if (options.
|
|
39
|
-
const action = options.
|
|
40
|
-
if (!options.
|
|
38
|
+
if (options.shouldFix) {
|
|
39
|
+
const action = options.isDryRun ? 'would-fix' : 'fixed';
|
|
40
|
+
if (!options.isDryRun) {
|
|
41
41
|
addPrepublishOnly(packageDir, hookCommand);
|
|
42
42
|
}
|
|
43
43
|
packages.push({
|
|
@@ -62,57 +62,57 @@ export function ensurePrepublishHooks(monorepoRoot, options) {
|
|
|
62
62
|
return { packages, hasFailures };
|
|
63
63
|
}
|
|
64
64
|
export function reportPrepublishHooks(result, hookCommand, style) {
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
65
|
+
const publishablePackages = result.packages.filter((packageStatus) => !packageStatus.isPrivate);
|
|
66
|
+
if (publishablePackages.length === 0) {
|
|
67
67
|
console.info('No publishable packages found.');
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
|
-
for (const
|
|
71
|
-
console.info(renderHookStatus(
|
|
70
|
+
for (const packageStatus of publishablePackages) {
|
|
71
|
+
console.info(renderHookStatus(packageStatus, hookCommand, style));
|
|
72
72
|
}
|
|
73
|
-
reportClosing(describeHookRun(
|
|
73
|
+
reportClosing(describeHookRun(publishablePackages));
|
|
74
74
|
}
|
|
75
75
|
function addPrepublishOnly(packageDir, command) {
|
|
76
76
|
const filePath = path.join(packageDir, 'package.json');
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
if (!isObject(
|
|
77
|
+
const rawText = readFileSync(filePath, 'utf8');
|
|
78
|
+
const parsedManifest = JSON.parse(rawText);
|
|
79
|
+
if (!isObject(parsedManifest)) {
|
|
80
80
|
throw new TypeError(`Invalid package.json in ${packageDir}: expected an object`);
|
|
81
81
|
}
|
|
82
|
-
const scripts = isObject(
|
|
82
|
+
const scripts = isObject(parsedManifest['scripts']) ? parsedManifest['scripts'] : {};
|
|
83
83
|
scripts['prepublishOnly'] = command;
|
|
84
|
-
|
|
85
|
-
writeFileSync(filePath, JSON.stringify(
|
|
84
|
+
parsedManifest['scripts'] = scripts;
|
|
85
|
+
writeFileSync(filePath, JSON.stringify(parsedManifest, null, 2) + '\n', 'utf8');
|
|
86
86
|
}
|
|
87
|
-
function countAction(
|
|
88
|
-
return
|
|
87
|
+
function countAction(publishablePackages, action) {
|
|
88
|
+
return publishablePackages.filter((packageStatus) => packageStatus.action === action).length;
|
|
89
89
|
}
|
|
90
|
-
function describeHookRun(
|
|
91
|
-
const packages =
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
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.`;
|
|
95
95
|
}
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
98
|
-
return `Added prepublishOnly to ${
|
|
96
|
+
const fixedCount = countAction(publishablePackages, 'fixed');
|
|
97
|
+
if (fixedCount > 0) {
|
|
98
|
+
return `Added prepublishOnly to ${fixedCount} of ${packages}.`;
|
|
99
99
|
}
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
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}.`;
|
|
103
103
|
}
|
|
104
|
-
return `${packages} ${
|
|
104
|
+
return `${packages} ${publishablePackages.length === 1 ? 'has' : 'have'} prepublishOnly.`;
|
|
105
105
|
}
|
|
106
|
-
function renderHookStatus(
|
|
106
|
+
function renderHookStatus(packageStatus, hookCommand, style) {
|
|
107
107
|
const statuses = STATUS_GLYPHS[style];
|
|
108
|
-
switch (
|
|
108
|
+
switch (packageStatus.action) {
|
|
109
109
|
case 'ok':
|
|
110
|
-
return `${statuses.passed.text} ${
|
|
110
|
+
return `${statuses.passed.text} ${packageStatus.packageName}: prepublishOnly = "${packageStatus.prepublishOnly}"`;
|
|
111
111
|
case 'missing':
|
|
112
|
-
return `${statuses.failed.text} ${
|
|
112
|
+
return `${statuses.failed.text} ${packageStatus.packageName}: missing prepublishOnly`;
|
|
113
113
|
case 'fixed':
|
|
114
|
-
return `${statuses.passed.text} ${
|
|
114
|
+
return `${statuses.passed.text} ${packageStatus.packageName}: added prepublishOnly = "${hookCommand}"`;
|
|
115
115
|
case 'would-fix':
|
|
116
|
-
return `~ ${
|
|
116
|
+
return `~ ${packageStatus.packageName}: would add prepublishOnly = "${hookCommand}"`;
|
|
117
117
|
}
|
|
118
118
|
}
|
|
@@ -13,7 +13,7 @@ export type ResolveTargetsResult = {
|
|
|
13
13
|
error: string;
|
|
14
14
|
};
|
|
15
15
|
/**
|
|
16
|
-
* Formats or checks the files git reports for `cwd`, and returns the exit code to report.
|
|
16
|
+
* Formats or checks the files that git reports for `cwd`, and returns the exit code to report.
|
|
17
17
|
*
|
|
18
18
|
* Trailing arguments are git pathspecs, not Prettier flags; an unrecognized option is rejected rather
|
|
19
19
|
* than passed along, since git would read it as a pathspec matching nothing and the run would report
|
|
@@ -26,11 +26,11 @@ export declare function runFmt(argv: string[], cwd?: string): number;
|
|
|
26
26
|
* pattern in `packages/<pkg>/.gitignore` is invisible to a root-level run; git knows the whole hierarchy,
|
|
27
27
|
* along with `.git/info/exclude` and `core.excludesFile`, which Prettier cannot read under any flag.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
29
|
+
* Because file selection is anchored at `cwd` and ignore discovery at the repository root, scoping follows
|
|
30
|
+
* `cwd` while the ignore rules applied to a given file are the same from every directory.
|
|
31
31
|
*
|
|
32
|
-
* `pathspecs` narrow the selection and are passed to git verbatim
|
|
33
|
-
*
|
|
32
|
+
* `pathspecs` narrow the selection and are passed to git verbatim: They have git pathspec semantics rather
|
|
33
|
+
* than shell glob semantics.
|
|
34
34
|
*/
|
|
35
35
|
export declare function resolveFormatTargets(cwd: string, pathspecs?: string[]): ResolveTargetsResult;
|
|
36
36
|
/** @internal */
|
|
@@ -40,15 +40,15 @@ export interface RunPrettierOptions {
|
|
|
40
40
|
files: string[];
|
|
41
41
|
ignorePaths: string[];
|
|
42
42
|
cwd: string;
|
|
43
|
-
/** Overridable so the multi-batch path is reachable without a fixture that hits the real ceiling. */
|
|
43
|
+
/** Overridable so that the multi-batch path is reachable without a fixture that hits the real ceiling. */
|
|
44
44
|
budgetBytes?: number;
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
47
|
* Runs Prettier over the selection, returning the first non-zero exit code. Every batch runs even after
|
|
48
48
|
* one fails, so a check reports every offending file rather than only those in the first batch.
|
|
49
49
|
*
|
|
50
|
-
*
|
|
51
|
-
* the file's execute bit or on PATH
|
|
50
|
+
* Because the CLI runs under this process's own Node rather than through its shebang, it does not depend
|
|
51
|
+
* on the file's execute bit or on PATH containing a `node`.
|
|
52
52
|
*
|
|
53
53
|
* @internal
|
|
54
54
|
*/
|