@williamthorsen/nmr 0.22.0 → 0.24.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.
@@ -0,0 +1,76 @@
1
+ import type { Writable } from 'node:stream';
2
+ import type { CheckCacheConfig, NmrConfig } from './types.js';
3
+ export interface CheckCacheEntry {
4
+ key: string;
5
+ treeHash: string;
6
+ headSha: string;
7
+ commandString: string;
8
+ nmrVersion: string;
9
+ nodeVersion: string;
10
+ durationMs: number;
11
+ recordedAt: string;
12
+ buildDigests: Record<string, string>;
13
+ }
14
+ export interface BuildOutputState {
15
+ missing: string[];
16
+ digests: Record<string, string>;
17
+ }
18
+ export interface RuntimeIdentity {
19
+ arch: string;
20
+ nodeVersion: string;
21
+ platform: string;
22
+ }
23
+ export interface TreeSnapshot {
24
+ hash: string;
25
+ headSha: string;
26
+ }
27
+ export declare const CURRENT_RUNTIME: RuntimeIdentity;
28
+ export declare const DEBUG_ENV_VAR = "NMR_DEBUG";
29
+ export declare const DEFAULT_CACHEABLE_COMMANDS: string[];
30
+ export declare const NO_CACHE_ENV_VAR = "NMR_NO_CACHE";
31
+ export declare const TREE_SNAPSHOT_ENV_VAR = "NMR_TREE_SNAPSHOT";
32
+ export declare function computeCacheKey(options: {
33
+ anchorDir: string;
34
+ command: string;
35
+ commandString: string;
36
+ env: NodeJS.ProcessEnv;
37
+ monorepoRoot: string;
38
+ nmrVersion: string;
39
+ runtime?: RuntimeIdentity;
40
+ snapshot: TreeSnapshot;
41
+ }): {
42
+ ok: true;
43
+ key: string;
44
+ } | {
45
+ ok: false;
46
+ reason: string;
47
+ };
48
+ export declare function encodeTreeSnapshot(snapshot: TreeSnapshot): string;
49
+ export declare function findStaleBuildOutput(recorded: Record<string, string>, current: Record<string, string>): string | undefined;
50
+ export declare function formatMisplacedNoCacheWarning(command: string): string;
51
+ export declare function formatSkipLine(command: string, entry: CheckCacheEntry, now: number): string;
52
+ export declare function readBuildOutputState(monorepoRoot: string, config: NmrConfig): Promise<BuildOutputState>;
53
+ export declare function readCheckCacheEntry(options: {
54
+ anchorDir: string;
55
+ command: string;
56
+ monorepoRoot: string;
57
+ }): Promise<CheckCacheEntry | undefined>;
58
+ export declare function removeCheckCache(scopeDir: string): Promise<void>;
59
+ export declare function resolveCacheableCommands(checkCache: CheckCacheConfig | undefined): Set<string>;
60
+ export declare function resolveTreeSnapshot(options: {
61
+ monorepoRoot: string;
62
+ env: NodeJS.ProcessEnv;
63
+ }): {
64
+ ok: true;
65
+ snapshot: TreeSnapshot;
66
+ } | {
67
+ ok: false;
68
+ reason: string;
69
+ };
70
+ export declare function writeCheckCacheEntry(options: {
71
+ anchorDir: string;
72
+ command: string;
73
+ entry: CheckCacheEntry;
74
+ monorepoRoot: string;
75
+ }): Promise<void>;
76
+ export declare function writeDebugNote(message: string, env: NodeJS.ProcessEnv, stderr: Writable): void;
@@ -0,0 +1,236 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync, realpathSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import process from 'node:process';
5
+ import { hashWorkingTree, readHeadSha, readJsonCacheEntry, removeCacheDir, resolveCacheEntryPath, writeCacheEntry, } from '@williamthorsen/nmr-core';
6
+ import { hasBuildOutput, readBuildDigest } from "./commands/build-output.js";
7
+ import { loadWorkspaceConfig } from "./config.js";
8
+ import { isObject, isStringRecord } from "./helpers/type-guards.js";
9
+ import { getDefaultWorkspaceScripts } from "./resolve-scripts.js";
10
+ import { buildWorkspaceRegistry, resolveScript } from "./resolver.js";
11
+ import { getWorkspacePackageDirs } from "./workspace.js";
12
+ export const CURRENT_RUNTIME = {
13
+ arch: process.arch,
14
+ nodeVersion: process.version,
15
+ platform: process.platform,
16
+ };
17
+ export const DEBUG_ENV_VAR = 'NMR_DEBUG';
18
+ export const DEFAULT_CACHEABLE_COMMANDS = [
19
+ 'check',
20
+ 'check:agent-files',
21
+ 'check:strict',
22
+ 'ci',
23
+ 'fix:check',
24
+ 'fmt:check',
25
+ 'lint:check',
26
+ 'lint:strict',
27
+ 'root:check',
28
+ 'root:lint:check',
29
+ 'root:lint:strict',
30
+ 'root:test',
31
+ 'root:test:tool',
32
+ 'root:test:unit',
33
+ 'root:typecheck',
34
+ 'test',
35
+ 'test:coverage',
36
+ 'test:tool',
37
+ 'test:unit',
38
+ 'typecheck',
39
+ ];
40
+ export const NO_CACHE_ENV_VAR = 'NMR_NO_CACHE';
41
+ export const TREE_SNAPSHOT_ENV_VAR = 'NMR_TREE_SNAPSHOT';
42
+ const BUILT_IN_COMPILE = 'nmr-compile';
43
+ const CACHE_TOOL = 'nmr-check';
44
+ const INSTALL_FINGERPRINT_FILES = [
45
+ path.join('node_modules', '.modules.yaml'),
46
+ path.join('node_modules', '.pnpm', 'lock.yaml'),
47
+ ];
48
+ const KEY_FORMAT = 'nmr-check-cache-v1';
49
+ const KEYED_ENV_VARS = ['LANG', 'LC_ALL', 'NODE_OPTIONS', 'TZ'];
50
+ const UNSAFE_SLUG_CHARACTERS = /[^\w.-]+/g;
51
+ export function computeCacheKey(options) {
52
+ const fingerprint = resolveInstallFingerprint(options.monorepoRoot);
53
+ if (!fingerprint.ok) {
54
+ return fingerprint;
55
+ }
56
+ const runtime = options.runtime ?? CURRENT_RUNTIME;
57
+ const scope = path.relative(options.monorepoRoot, options.anchorDir);
58
+ const parts = [
59
+ KEY_FORMAT,
60
+ options.snapshot.hash,
61
+ scope === '' ? '.' : scope,
62
+ options.command,
63
+ options.commandString,
64
+ options.nmrVersion,
65
+ runtime.nodeVersion,
66
+ runtime.platform,
67
+ runtime.arch,
68
+ fingerprint.fingerprint,
69
+ ];
70
+ for (const name of KEYED_ENV_VARS) {
71
+ const value = options.env[name];
72
+ parts.push(name, value === undefined ? 'unset' : 'set', value ?? '');
73
+ }
74
+ const hash = createHash('sha256');
75
+ for (const part of parts) {
76
+ hash.update(part);
77
+ hash.update('\0');
78
+ }
79
+ return { ok: true, key: hash.digest('hex') };
80
+ }
81
+ export function encodeTreeSnapshot(snapshot) {
82
+ return `${snapshot.hash} ${snapshot.headSha}`;
83
+ }
84
+ export function findStaleBuildOutput(recorded, current) {
85
+ const names = [...new Set([...Object.keys(recorded), ...Object.keys(current)])].toSorted();
86
+ return names.find((name) => recorded[name] !== current[name]);
87
+ }
88
+ export function formatMisplacedNoCacheWarning(command) {
89
+ return (`⚠️ --no-cache after the command name is passed to \`${command}\`, not read by nmr. ` +
90
+ `Did you mean \`nmr --no-cache ${command}\`?`);
91
+ }
92
+ export function formatSkipLine(command, entry, now) {
93
+ const age = formatDuration(Math.max(0, now - Date.parse(entry.recordedAt)));
94
+ const saved = formatDuration(entry.durationMs);
95
+ return `⏭️ ${command}: passed ${age} ago on this tree (saved ~${saved}). Re-run with --no-cache.`;
96
+ }
97
+ export async function readBuildOutputState(monorepoRoot, config) {
98
+ const state = { missing: [], digests: {} };
99
+ let packageDirs;
100
+ try {
101
+ packageDirs = getWorkspacePackageDirs(monorepoRoot);
102
+ }
103
+ catch {
104
+ return state;
105
+ }
106
+ const registry = buildWorkspaceRegistry(config);
107
+ if (JSON.stringify(registry.build) !== JSON.stringify(getDefaultWorkspaceScripts().build)) {
108
+ return state;
109
+ }
110
+ for (const packageDir of packageDirs) {
111
+ if (!isProbeSubject(packageDir, registry)) {
112
+ continue;
113
+ }
114
+ const { build } = await loadWorkspaceConfig(packageDir);
115
+ const options = build?.extraIgnorePatterns === undefined ? {} : { extraIgnorePatterns: build.extraIgnorePatterns };
116
+ const name = path.relative(monorepoRoot, packageDir);
117
+ if (await hasBuildOutput(packageDir, options)) {
118
+ state.digests[name] = (await readBuildDigest(packageDir)) ?? '';
119
+ }
120
+ else {
121
+ state.missing.push(name);
122
+ }
123
+ }
124
+ return state;
125
+ }
126
+ export async function readCheckCacheEntry(options) {
127
+ return readJsonCacheEntry(resolveEntryPath(options), isCheckCacheEntry);
128
+ }
129
+ export async function removeCheckCache(scopeDir) {
130
+ await removeCacheDir({ tool: CACHE_TOOL, scopeDir });
131
+ }
132
+ export function resolveCacheableCommands(checkCache) {
133
+ const commands = new Set([...DEFAULT_CACHEABLE_COMMANDS, ...(checkCache?.extraCommands ?? [])]);
134
+ for (const excluded of checkCache?.excludeCommands ?? []) {
135
+ commands.delete(excluded);
136
+ }
137
+ return commands;
138
+ }
139
+ export function resolveTreeSnapshot(options) {
140
+ const inherited = decodeTreeSnapshot(options.env[TREE_SNAPSHOT_ENV_VAR]);
141
+ if (inherited !== undefined && readHeadSha(options.monorepoRoot) === inherited.headSha) {
142
+ return { ok: true, snapshot: inherited };
143
+ }
144
+ const hashed = hashWorkingTree(options.monorepoRoot);
145
+ if (!hashed.ok) {
146
+ return hashed;
147
+ }
148
+ let physicalRoot;
149
+ try {
150
+ physicalRoot = realpathSync(options.monorepoRoot);
151
+ }
152
+ catch {
153
+ return { ok: false, reason: `could not resolve ${options.monorepoRoot}` };
154
+ }
155
+ if (hashed.toplevel !== physicalRoot) {
156
+ return { ok: false, reason: `the monorepo root is not the git toplevel (${hashed.toplevel})` };
157
+ }
158
+ return { ok: true, snapshot: { hash: hashed.hash, headSha: hashed.headSha } };
159
+ }
160
+ export async function writeCheckCacheEntry(options) {
161
+ await writeCacheEntry(resolveEntryPath(options), JSON.stringify(options.entry, undefined, 2));
162
+ }
163
+ export function writeDebugNote(message, env, stderr) {
164
+ if (env[DEBUG_ENV_VAR] === '1') {
165
+ stderr.write(`nmr check-cache: ${message}\n`);
166
+ }
167
+ }
168
+ const MILLISECONDS_PER_SECOND = 1000;
169
+ const SECONDS_PER_MINUTE = 60;
170
+ const MINUTES_PER_HOUR = 60;
171
+ function decodeTreeSnapshot(encoded) {
172
+ if (encoded === undefined) {
173
+ return undefined;
174
+ }
175
+ const [hash, headSha] = encoded.split(' ', 2);
176
+ if (hash === undefined || headSha === undefined || hash === '' || headSha === '') {
177
+ return undefined;
178
+ }
179
+ return { hash, headSha };
180
+ }
181
+ function formatDuration(milliseconds) {
182
+ const seconds = Math.round(milliseconds / MILLISECONDS_PER_SECOND);
183
+ if (seconds < SECONDS_PER_MINUTE) {
184
+ return `${seconds}s`;
185
+ }
186
+ const minutes = Math.round(seconds / SECONDS_PER_MINUTE);
187
+ if (minutes < MINUTES_PER_HOUR) {
188
+ return `${minutes}m`;
189
+ }
190
+ return `${Math.round(minutes / MINUTES_PER_HOUR)}h`;
191
+ }
192
+ function isCheckCacheEntry(value) {
193
+ if (!isObject(value)) {
194
+ return false;
195
+ }
196
+ const stringFields = ['key', 'treeHash', 'headSha', 'commandString', 'nmrVersion', 'nodeVersion', 'recordedAt'];
197
+ if (stringFields.some((field) => typeof value[field] !== 'string')) {
198
+ return false;
199
+ }
200
+ return (typeof value.durationMs === 'number' &&
201
+ Number.isFinite(value.durationMs) &&
202
+ !Number.isNaN(Date.parse(String(value.recordedAt))) &&
203
+ isStringRecord(value.buildDigests));
204
+ }
205
+ function isProbeSubject(packageDir, registry) {
206
+ const build = resolveScript('build', registry, packageDir, false);
207
+ if (build === undefined || build.source === 'package') {
208
+ return false;
209
+ }
210
+ const compile = resolveScript('compile', registry, packageDir, false);
211
+ return compile !== undefined && compile.command === BUILT_IN_COMPILE;
212
+ }
213
+ function resolveEntryPath(options) {
214
+ const anchorDir = path.resolve(options.anchorDir);
215
+ const ref = {
216
+ tool: CACHE_TOOL,
217
+ scopeDir: options.monorepoRoot,
218
+ slug: `${path.basename(anchorDir)}-${options.command.replaceAll(UNSAFE_SLUG_CHARACTERS, '-')}`,
219
+ extension: '.json',
220
+ discriminators: [anchorDir, options.command],
221
+ };
222
+ return resolveCacheEntryPath(ref);
223
+ }
224
+ function resolveInstallFingerprint(monorepoRoot) {
225
+ const hash = createHash('sha256');
226
+ for (const relativePath of INSTALL_FINGERPRINT_FILES) {
227
+ try {
228
+ hash.update(readFileSync(path.join(monorepoRoot, relativePath)));
229
+ }
230
+ catch {
231
+ return { ok: false, reason: `no install fingerprint: ${relativePath} is unreadable` };
232
+ }
233
+ hash.update('\0');
234
+ }
235
+ return { ok: true, fingerprint: hash.digest('hex') };
236
+ }
@@ -1,8 +1,14 @@
1
+ import { reportError } from '@williamthorsen/nmr-core';
1
2
  import { buildPackage } from "./commands/build.js";
3
+ import { loadWorkspaceConfig } from "./config.js";
2
4
  try {
3
- await buildPackage(process.cwd());
5
+ const packageDir = process.cwd();
6
+ const { build } = await loadWorkspaceConfig(packageDir);
7
+ await buildPackage(packageDir, {
8
+ ...(build?.extraIgnorePatterns !== undefined && { extraIgnorePatterns: build.extraIgnorePatterns }),
9
+ });
4
10
  }
5
11
  catch (error) {
6
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
12
+ reportError(error instanceof Error ? error.message : String(error));
7
13
  process.exitCode = 1;
8
14
  }
@@ -0,0 +1,13 @@
1
+ export interface BuildOptions {
2
+ entryGlobs?: string[];
3
+ extraIgnorePatterns?: string[];
4
+ ignorePatterns?: string[];
5
+ outdir?: string;
6
+ }
7
+ export declare const DEFAULT_ENTRY_GLOBS: string[];
8
+ export declare const DEFAULT_IGNORE_PATTERNS: string[];
9
+ export declare const DEFAULT_OUTDIR = "dist/esm/";
10
+ export declare function hasBuildOutput(packageDir: string, options?: BuildOptions): Promise<boolean>;
11
+ export declare function hasExpectedBuildOutput(packageDir: string, outdir: string, entryPoints: string[]): boolean;
12
+ export declare function readBuildDigest(packageDir: string): Promise<string | undefined>;
13
+ export declare function resolveBuildCachePath(packageDir: string): string;
@@ -0,0 +1,36 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readCacheEntry, resolveCacheEntryPath } from '@williamthorsen/nmr-core';
4
+ import { glob } from 'glob';
5
+ export const DEFAULT_ENTRY_GLOBS = ['src/**/*.ts'];
6
+ export const DEFAULT_IGNORE_PATTERNS = ['**/__fixtures__/**', '**/__mocks__/**', '**/__tests__/**', '**/test-utils/**'];
7
+ export const DEFAULT_OUTDIR = 'dist/esm/';
8
+ const BUILD_CACHE_TOOL = 'nmr-compile';
9
+ export async function hasBuildOutput(packageDir, options = {}) {
10
+ const outdir = options.outdir ?? DEFAULT_OUTDIR;
11
+ const entryPoints = await glob(options.entryGlobs ?? DEFAULT_ENTRY_GLOBS, {
12
+ cwd: packageDir,
13
+ ignore: [...(options.ignorePatterns ?? DEFAULT_IGNORE_PATTERNS), ...(options.extraIgnorePatterns ?? [])],
14
+ });
15
+ return hasExpectedBuildOutput(packageDir, outdir, entryPoints);
16
+ }
17
+ export function hasExpectedBuildOutput(packageDir, outdir, entryPoints) {
18
+ const emitsOutput = entryPoints.some((entry) => !entry.endsWith('.d.ts'));
19
+ if (!emitsOutput) {
20
+ return true;
21
+ }
22
+ const outputDir = path.resolve(packageDir, outdir);
23
+ return existsSync(outputDir) && readdirSync(outputDir).length > 0;
24
+ }
25
+ export async function readBuildDigest(packageDir) {
26
+ return readCacheEntry(resolveBuildCachePath(packageDir));
27
+ }
28
+ export function resolveBuildCachePath(packageDir) {
29
+ const absolutePackageDir = path.resolve(packageDir);
30
+ return resolveCacheEntryPath({
31
+ tool: BUILD_CACHE_TOOL,
32
+ scopeDir: absolutePackageDir,
33
+ slug: path.basename(absolutePackageDir),
34
+ extension: '.hash',
35
+ });
36
+ }
@@ -1,9 +1,4 @@
1
- export interface BuildOptions {
2
- entryGlobs?: string[];
3
- ignore?: string[];
4
- outdir?: string;
5
- }
1
+ import type { BuildOptions } from './build-output.js';
6
2
  export declare function buildPackage(packageDir: string, options?: BuildOptions): Promise<void>;
7
3
  export declare function computeBuildHash(packageDir: string, files: string[], emitConfig: object, compilerVersion: string): Promise<string>;
8
4
  export declare function resolveTsconfigChain(packageDir: string, configFileName?: string): string[];
9
- export declare function resolveBuildCachePath(packageDir: string): string;
@@ -1,14 +1,14 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
3
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { readFile, rm } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
+ import { readCacheEntry, writeCacheEntry } from '@williamthorsen/nmr-core';
5
6
  import { glob } from 'glob';
6
7
  import * as ts from 'typescript';
8
+ import { resolveConfigPath } from "../config.js";
9
+ import { DEFAULT_ENTRY_GLOBS, DEFAULT_IGNORE_PATTERNS, DEFAULT_OUTDIR, hasExpectedBuildOutput, resolveBuildCachePath, } from "./build-output.js";
7
10
  const PACKAGE_ICON = '📦';
8
11
  const SKIPPED_ICON = '⏭️';
9
- const DEFAULT_ENTRY_GLOBS = ['src/**/*.ts'];
10
- const DEFAULT_IGNORE = ['**/__tests__/**'];
11
- const DEFAULT_OUTDIR = 'dist/esm/';
12
12
  const SOURCE_ROOT = 'src';
13
13
  const MINIMUM_TYPESCRIPT_MAJOR = 5;
14
14
  const MINIMUM_TYPESCRIPT_MINOR = 7;
@@ -18,22 +18,28 @@ export async function buildPackage(packageDir, options = {}) {
18
18
  assertSupportedTypeScript();
19
19
  const cachePath = resolveBuildCachePath(packageDir);
20
20
  const outdir = options.outdir ?? DEFAULT_OUTDIR;
21
+ resolveEmitDir(packageDir, outdir);
21
22
  const emitConfig = { outdir, declaration: true, rewriteRelativeImportExtensions: true };
22
23
  const entryPoints = await glob(options.entryGlobs ?? DEFAULT_ENTRY_GLOBS, {
23
24
  cwd: packageDir,
24
- ignore: options.ignore ?? DEFAULT_IGNORE,
25
+ ignore: [...(options.ignorePatterns ?? DEFAULT_IGNORE_PATTERNS), ...(options.extraIgnorePatterns ?? [])],
25
26
  });
26
- const dependencies = ['package.json', ...resolveTsconfigChain(packageDir)];
27
+ const configPath = resolveConfigPath(packageDir);
28
+ const dependencies = [
29
+ 'package.json',
30
+ ...(existsSync(configPath) ? [path.relative(packageDir, configPath)] : []),
31
+ ...resolveTsconfigChain(packageDir),
32
+ ];
27
33
  const { changed, currentHash } = await detectBuildChanges(packageDir, [...entryPoints, ...dependencies], emitConfig, ts.version, cachePath, hasExpectedBuildOutput(packageDir, outdir, entryPoints));
28
34
  if (!changed) {
29
35
  return;
30
36
  }
31
- emitPackage(packageDir, entryPoints, outdir);
32
- await writeBuildCache(cachePath, currentHash);
37
+ await emitPackage(packageDir, entryPoints, outdir);
38
+ await writeCacheEntry(cachePath, currentHash);
33
39
  }
34
40
  export async function computeBuildHash(packageDir, files, emitConfig, compilerVersion) {
35
41
  const hash = createHash('sha256');
36
- for (const file of [...files].sort()) {
42
+ for (const file of files.toSorted()) {
37
43
  hash.update(file);
38
44
  hash.update('\0');
39
45
  hash.update(await readFile(path.join(packageDir, file)));
@@ -67,11 +73,12 @@ export function resolveTsconfigChain(packageDir, configFileName = 'tsconfig.json
67
73
  walk(path.resolve(packageDir, configFileName));
68
74
  return resolvedChain.map((absolute) => path.relative(packageDir, absolute));
69
75
  }
70
- function emitPackage(packageDir, entryPoints, outdir) {
76
+ async function emitPackage(packageDir, entryPoints, outdir) {
71
77
  const compilerOptions = synthesizeCompilerOptions(packageDir, outdir);
72
78
  const rootNames = entryPoints.map((entry) => path.resolve(packageDir, entry));
73
79
  const sourceRoot = path.resolve(packageDir, SOURCE_ROOT);
74
80
  const program = ts.createProgram(rootNames, compilerOptions);
81
+ await rm(resolveEmitDir(packageDir, outdir), { force: true, recursive: true });
75
82
  const emittedFiles = [];
76
83
  const emitResult = program.emit(undefined, (fileName, text, writeByteOrderMark) => {
77
84
  ts.sys.writeFile(fileName, text, writeByteOrderMark);
@@ -84,6 +91,15 @@ function emitPackage(packageDir, entryPoints, outdir) {
84
91
  rewriteOutputSpecifiers(file, compilerOptions, sourceRoot);
85
92
  }
86
93
  }
94
+ function resolveEmitDir(packageDir, outdir) {
95
+ const resolved = path.resolve(packageDir, outdir);
96
+ const relative = path.relative(packageDir, resolved);
97
+ if (relative === '' || relative.startsWith('..') || path.isAbsolute(relative)) {
98
+ throw new Error(`nmr-compile: refusing to build into '${outdir}', which does not resolve inside the package. ` +
99
+ 'The build wipes its output directory before each emit, so the directory must sit below the package root.');
100
+ }
101
+ return resolved;
102
+ }
87
103
  function synthesizeCompilerOptions(packageDir, outdir) {
88
104
  const configPath = path.join(packageDir, 'tsconfig.json');
89
105
  const configFile = ts.readConfigFile(configPath, (fileName) => ts.sys.readFile(fileName));
@@ -190,28 +206,9 @@ function getModuleSpecifier(node) {
190
206
  }
191
207
  return undefined;
192
208
  }
193
- export function resolveBuildCachePath(packageDir) {
194
- const absolutePackageDir = path.resolve(packageDir);
195
- const home = findNearestNodeModulesHost(absolutePackageDir) ?? absolutePackageDir;
196
- const digest = createHash('sha256').update(absolutePackageDir).digest('hex').slice(0, 8);
197
- const key = `${path.basename(absolutePackageDir)}-${digest}.hash`;
198
- return path.join(home, 'node_modules', '.cache', 'nmr-compile', key);
199
- }
200
- function findNearestNodeModulesHost(startDir) {
201
- let current = startDir;
202
- let parent = path.dirname(current);
203
- while (!existsSync(path.join(current, 'node_modules'))) {
204
- if (parent === current) {
205
- return undefined;
206
- }
207
- current = parent;
208
- parent = path.dirname(current);
209
- }
210
- return current;
211
- }
212
209
  async function detectBuildChanges(packageDir, files, emitConfig, compilerVersion, cachePath, outputPresent) {
213
210
  const packageName = path.basename(packageDir);
214
- const previousHash = existsSync(cachePath) ? readFileSync(cachePath, 'utf8') : undefined;
211
+ const previousHash = await readCacheEntry(cachePath);
215
212
  const currentHash = await computeBuildHash(packageDir, files, emitConfig, compilerVersion);
216
213
  if (previousHash === currentHash) {
217
214
  if (outputPresent) {
@@ -224,18 +221,6 @@ async function detectBuildChanges(packageDir, files, emitConfig, compilerVersion
224
221
  console.info(`${PACKAGE_ICON} ${packageName}: Changes detected.`);
225
222
  return { changed: true, currentHash };
226
223
  }
227
- function hasExpectedBuildOutput(packageDir, outdir, entryPoints) {
228
- const emitsOutput = entryPoints.some((entry) => !entry.endsWith('.d.ts'));
229
- if (!emitsOutput) {
230
- return true;
231
- }
232
- const outputDir = path.resolve(packageDir, outdir);
233
- return existsSync(outputDir) && readdirSync(outputDir).length > 0;
234
- }
235
- async function writeBuildCache(cachePath, hash) {
236
- await mkdir(path.dirname(cachePath), { recursive: true });
237
- await writeFile(cachePath, hash);
238
- }
239
224
  function assertSupportedTypeScript() {
240
225
  const [majorPart, minorPart] = ts.versionMajorMinor.split('.', 2);
241
226
  const major = majorPart === undefined ? 0 : Number(majorPart);
@@ -1,11 +1,12 @@
1
1
  import { rm } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { loadConfig } from "../config.js";
3
+ import { removeCheckCache } from "../check-cache.js";
4
+ import { loadRootConfig } from "../config.js";
4
5
  import { findContainingPackageDir } from "../context.js";
5
6
  import { applyDevBin, buildWorkspaceRegistry, resolveScript } from "../resolver.js";
6
7
  import { runCommand } from "../runner.js";
7
8
  import { findMonorepoRoot, getWorkspacePackageDirs } from "../workspace.js";
8
- import { resolveBuildCachePath } from "./build.js";
9
+ import { resolveBuildCachePath } from "./build-output.js";
9
10
  const CLEAN_ICON = '🧹';
10
11
  const CLEAN_COMMAND = 'clean';
11
12
  const BUILT_IN_CLEAN = 'nmr-clean';
@@ -21,9 +22,11 @@ export async function runClean(cwd = process.cwd()) {
21
22
  monorepoRoot = findMonorepoRoot(cwd);
22
23
  }
23
24
  catch {
25
+ await clearCheckCache(cwd);
24
26
  await cleanPackage(cwd);
25
27
  return;
26
28
  }
29
+ await clearCheckCache(monorepoRoot);
27
30
  const workspacePackageDirs = getWorkspacePackageDirs(monorepoRoot);
28
31
  const packageDir = findContainingPackageDir(cwd, workspacePackageDirs);
29
32
  if (packageDir !== undefined) {
@@ -32,8 +35,12 @@ export async function runClean(cwd = process.cwd()) {
32
35
  }
33
36
  await sweepWorkspace(monorepoRoot, workspacePackageDirs);
34
37
  }
38
+ async function clearCheckCache(scopeDir) {
39
+ await removeCheckCache(scopeDir);
40
+ console.info(`${CLEAN_ICON} Removed all recorded check results.`);
41
+ }
35
42
  async function sweepWorkspace(monorepoRoot, workspacePackageDirs) {
36
- const config = await loadConfig(monorepoRoot);
43
+ const config = await loadRootConfig(monorepoRoot);
37
44
  const registry = buildWorkspaceRegistry(config);
38
45
  for (const packageDir of workspacePackageDirs) {
39
46
  const resolved = resolveScript(CLEAN_COMMAND, registry, packageDir, false);
@@ -1,7 +1,5 @@
1
- export interface NmrConfig {
2
- devBin?: Record<string, string>;
3
- workspaceScripts?: Record<string, string | string[]>;
4
- rootScripts?: Record<string, string | string[]>;
5
- }
6
- export declare function defineConfig(config: NmrConfig): NmrConfig;
7
- export declare function loadConfig(monorepoRoot: string): Promise<NmrConfig>;
1
+ import type { NmrConfig } from './types.js';
2
+ export declare function resolveConfigPath(baseDir: string): string;
3
+ export declare function loadConfig(baseDir: string): Promise<NmrConfig>;
4
+ export declare function loadRootConfig(monorepoRoot: string): Promise<NmrConfig>;
5
+ export declare function loadWorkspaceConfig(packageDir: string): Promise<NmrConfig>;