@williamthorsen/nmr 0.23.0 → 0.25.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/.readyup/kits/default.js +479 -0
- package/.readyup/manifest.json +16 -0
- package/AGENTS.md +3 -1
- package/README.md +343 -114
- package/dist/esm/check-cache.d.ts +76 -0
- package/dist/esm/check-cache.js +224 -0
- package/dist/esm/cli-build.js +8 -2
- package/dist/esm/commands/build-output.d.ts +13 -0
- package/dist/esm/commands/build-output.js +36 -0
- package/dist/esm/commands/build.d.ts +1 -6
- package/dist/esm/commands/build.js +27 -42
- package/dist/esm/commands/clean.js +10 -3
- package/dist/esm/config.d.ts +5 -7
- package/dist/esm/config.js +115 -15
- package/dist/esm/context.d.ts +1 -1
- package/dist/esm/context.js +2 -2
- package/dist/esm/default-scripts.js +14 -9
- package/dist/esm/defineConfig.d.ts +3 -0
- package/dist/esm/defineConfig.js +3 -0
- package/dist/esm/help.d.ts +1 -1
- package/dist/esm/help.js +1 -0
- package/dist/esm/helpers/duration.d.ts +2 -0
- package/dist/esm/helpers/duration.js +21 -0
- package/dist/esm/helpers/type-guards.d.ts +1 -0
- package/dist/esm/helpers/type-guards.js +3 -0
- package/dist/esm/resolver.d.ts +1 -1
- package/dist/esm/runCli.js +229 -27
- package/dist/esm/tiers.d.ts +7 -0
- package/dist/esm/tiers.js +35 -0
- package/dist/esm/types.d.ts +15 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/vitest.d.ts +6 -2
- package/dist/esm/vitest.js +51 -31
- package/package.json +9 -7
- package/dist/esm/index.d.ts +0 -2
- package/dist/esm/index.js +0 -1
|
@@ -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,224 @@
|
|
|
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 { formatDuration, formatSaving } from "./helpers/duration.js";
|
|
9
|
+
import { isObject, isStringRecord } from "./helpers/type-guards.js";
|
|
10
|
+
import { getDefaultWorkspaceScripts } from "./resolve-scripts.js";
|
|
11
|
+
import { buildWorkspaceRegistry, resolveScript } from "./resolver.js";
|
|
12
|
+
import { getWorkspacePackageDirs } from "./workspace.js";
|
|
13
|
+
export const CURRENT_RUNTIME = {
|
|
14
|
+
arch: process.arch,
|
|
15
|
+
nodeVersion: process.version,
|
|
16
|
+
platform: process.platform,
|
|
17
|
+
};
|
|
18
|
+
export const DEBUG_ENV_VAR = 'NMR_DEBUG';
|
|
19
|
+
export const DEFAULT_CACHEABLE_COMMANDS = [
|
|
20
|
+
'check',
|
|
21
|
+
'check:agent-files',
|
|
22
|
+
'check:strict',
|
|
23
|
+
'ci',
|
|
24
|
+
'fix:check',
|
|
25
|
+
'fmt:check',
|
|
26
|
+
'lint:check',
|
|
27
|
+
'lint:strict',
|
|
28
|
+
'root:check',
|
|
29
|
+
'root:lint:check',
|
|
30
|
+
'root:lint:strict',
|
|
31
|
+
'root:test',
|
|
32
|
+
'root:test:tool',
|
|
33
|
+
'root:test:unit',
|
|
34
|
+
'root:typecheck',
|
|
35
|
+
'test',
|
|
36
|
+
'test:coverage',
|
|
37
|
+
'test:tool',
|
|
38
|
+
'test:unit',
|
|
39
|
+
'typecheck',
|
|
40
|
+
];
|
|
41
|
+
export const NO_CACHE_ENV_VAR = 'NMR_NO_CACHE';
|
|
42
|
+
export const TREE_SNAPSHOT_ENV_VAR = 'NMR_TREE_SNAPSHOT';
|
|
43
|
+
const BUILT_IN_COMPILE = 'nmr-compile';
|
|
44
|
+
const CACHE_TOOL = 'nmr-check';
|
|
45
|
+
const INSTALL_FINGERPRINT_FILES = [
|
|
46
|
+
path.join('node_modules', '.modules.yaml'),
|
|
47
|
+
path.join('node_modules', '.pnpm', 'lock.yaml'),
|
|
48
|
+
];
|
|
49
|
+
const KEY_FORMAT = 'nmr-check-cache-v1';
|
|
50
|
+
const KEYED_ENV_VARS = ['LANG', 'LC_ALL', 'NODE_OPTIONS', 'TZ'];
|
|
51
|
+
const UNSAFE_SLUG_CHARACTERS = /[^\w.-]+/g;
|
|
52
|
+
export function computeCacheKey(options) {
|
|
53
|
+
const fingerprint = resolveInstallFingerprint(options.monorepoRoot);
|
|
54
|
+
if (!fingerprint.ok) {
|
|
55
|
+
return fingerprint;
|
|
56
|
+
}
|
|
57
|
+
const runtime = options.runtime ?? CURRENT_RUNTIME;
|
|
58
|
+
const scope = path.relative(options.monorepoRoot, options.anchorDir);
|
|
59
|
+
const parts = [
|
|
60
|
+
KEY_FORMAT,
|
|
61
|
+
options.snapshot.hash,
|
|
62
|
+
scope === '' ? '.' : scope,
|
|
63
|
+
options.command,
|
|
64
|
+
options.commandString,
|
|
65
|
+
options.nmrVersion,
|
|
66
|
+
runtime.nodeVersion,
|
|
67
|
+
runtime.platform,
|
|
68
|
+
runtime.arch,
|
|
69
|
+
fingerprint.fingerprint,
|
|
70
|
+
];
|
|
71
|
+
for (const name of KEYED_ENV_VARS) {
|
|
72
|
+
const value = options.env[name];
|
|
73
|
+
parts.push(name, value === undefined ? 'unset' : 'set', value ?? '');
|
|
74
|
+
}
|
|
75
|
+
const hash = createHash('sha256');
|
|
76
|
+
for (const part of parts) {
|
|
77
|
+
hash.update(part);
|
|
78
|
+
hash.update('\0');
|
|
79
|
+
}
|
|
80
|
+
return { ok: true, key: hash.digest('hex') };
|
|
81
|
+
}
|
|
82
|
+
export function encodeTreeSnapshot(snapshot) {
|
|
83
|
+
return `${snapshot.hash} ${snapshot.headSha}`;
|
|
84
|
+
}
|
|
85
|
+
export function findStaleBuildOutput(recorded, current) {
|
|
86
|
+
const names = [...new Set([...Object.keys(recorded), ...Object.keys(current)])].toSorted();
|
|
87
|
+
return names.find((name) => recorded[name] !== current[name]);
|
|
88
|
+
}
|
|
89
|
+
export function formatMisplacedNoCacheWarning(command) {
|
|
90
|
+
return (`⚠️ --no-cache after the command name is passed to \`${command}\`, not read by nmr. ` +
|
|
91
|
+
`Did you mean \`nmr --no-cache ${command}\`?`);
|
|
92
|
+
}
|
|
93
|
+
export function formatSkipLine(command, entry, now) {
|
|
94
|
+
const age = formatDuration(Math.max(0, now - Date.parse(entry.recordedAt)));
|
|
95
|
+
const saving = formatSaving(entry.durationMs);
|
|
96
|
+
const savingClause = saving === undefined ? '' : ` (${saving})`;
|
|
97
|
+
return `⏭️ ${command}: passed ${age} ago on this tree${savingClause}. Re-run with --no-cache.`;
|
|
98
|
+
}
|
|
99
|
+
export async function readBuildOutputState(monorepoRoot, config) {
|
|
100
|
+
const state = { missing: [], digests: {} };
|
|
101
|
+
let packageDirs;
|
|
102
|
+
try {
|
|
103
|
+
packageDirs = getWorkspacePackageDirs(monorepoRoot);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return state;
|
|
107
|
+
}
|
|
108
|
+
const registry = buildWorkspaceRegistry(config);
|
|
109
|
+
if (JSON.stringify(registry.build) !== JSON.stringify(getDefaultWorkspaceScripts().build)) {
|
|
110
|
+
return state;
|
|
111
|
+
}
|
|
112
|
+
for (const packageDir of packageDirs) {
|
|
113
|
+
if (!isProbeSubject(packageDir, registry)) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const { build } = await loadWorkspaceConfig(packageDir);
|
|
117
|
+
const options = build?.extraIgnorePatterns === undefined ? {} : { extraIgnorePatterns: build.extraIgnorePatterns };
|
|
118
|
+
const name = path.relative(monorepoRoot, packageDir);
|
|
119
|
+
if (await hasBuildOutput(packageDir, options)) {
|
|
120
|
+
state.digests[name] = (await readBuildDigest(packageDir)) ?? '';
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
state.missing.push(name);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return state;
|
|
127
|
+
}
|
|
128
|
+
export async function readCheckCacheEntry(options) {
|
|
129
|
+
return readJsonCacheEntry(resolveEntryPath(options), isCheckCacheEntry);
|
|
130
|
+
}
|
|
131
|
+
export async function removeCheckCache(scopeDir) {
|
|
132
|
+
await removeCacheDir({ tool: CACHE_TOOL, scopeDir });
|
|
133
|
+
}
|
|
134
|
+
export function resolveCacheableCommands(checkCache) {
|
|
135
|
+
const commands = new Set([...DEFAULT_CACHEABLE_COMMANDS, ...(checkCache?.extraCommands ?? [])]);
|
|
136
|
+
for (const excluded of checkCache?.excludeCommands ?? []) {
|
|
137
|
+
commands.delete(excluded);
|
|
138
|
+
}
|
|
139
|
+
return commands;
|
|
140
|
+
}
|
|
141
|
+
export function resolveTreeSnapshot(options) {
|
|
142
|
+
const inherited = decodeTreeSnapshot(options.env[TREE_SNAPSHOT_ENV_VAR]);
|
|
143
|
+
if (inherited !== undefined && readHeadSha(options.monorepoRoot) === inherited.headSha) {
|
|
144
|
+
return { ok: true, snapshot: inherited };
|
|
145
|
+
}
|
|
146
|
+
const hashed = hashWorkingTree(options.monorepoRoot);
|
|
147
|
+
if (!hashed.ok) {
|
|
148
|
+
return hashed;
|
|
149
|
+
}
|
|
150
|
+
let physicalRoot;
|
|
151
|
+
try {
|
|
152
|
+
physicalRoot = realpathSync(options.monorepoRoot);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return { ok: false, reason: `could not resolve ${options.monorepoRoot}` };
|
|
156
|
+
}
|
|
157
|
+
if (hashed.toplevel !== physicalRoot) {
|
|
158
|
+
return { ok: false, reason: `the monorepo root is not the git toplevel (${hashed.toplevel})` };
|
|
159
|
+
}
|
|
160
|
+
return { ok: true, snapshot: { hash: hashed.hash, headSha: hashed.headSha } };
|
|
161
|
+
}
|
|
162
|
+
export async function writeCheckCacheEntry(options) {
|
|
163
|
+
await writeCacheEntry(resolveEntryPath(options), JSON.stringify(options.entry, undefined, 2));
|
|
164
|
+
}
|
|
165
|
+
export function writeDebugNote(message, env, stderr) {
|
|
166
|
+
if (env[DEBUG_ENV_VAR] === '1') {
|
|
167
|
+
stderr.write(`nmr check-cache: ${message}\n`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function decodeTreeSnapshot(encoded) {
|
|
171
|
+
if (encoded === undefined) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
const [hash, headSha] = encoded.split(' ', 2);
|
|
175
|
+
if (hash === undefined || headSha === undefined || hash === '' || headSha === '') {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
return { hash, headSha };
|
|
179
|
+
}
|
|
180
|
+
function isCheckCacheEntry(value) {
|
|
181
|
+
if (!isObject(value)) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
const stringFields = ['key', 'treeHash', 'headSha', 'commandString', 'nmrVersion', 'nodeVersion', 'recordedAt'];
|
|
185
|
+
if (stringFields.some((field) => typeof value[field] !== 'string')) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
return (typeof value.durationMs === 'number' &&
|
|
189
|
+
Number.isFinite(value.durationMs) &&
|
|
190
|
+
!Number.isNaN(Date.parse(String(value.recordedAt))) &&
|
|
191
|
+
isStringRecord(value.buildDigests));
|
|
192
|
+
}
|
|
193
|
+
function isProbeSubject(packageDir, registry) {
|
|
194
|
+
const build = resolveScript('build', registry, packageDir, false);
|
|
195
|
+
if (build === undefined || build.source === 'package') {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
const compile = resolveScript('compile', registry, packageDir, false);
|
|
199
|
+
return compile !== undefined && compile.command === BUILT_IN_COMPILE;
|
|
200
|
+
}
|
|
201
|
+
function resolveEntryPath(options) {
|
|
202
|
+
const anchorDir = path.resolve(options.anchorDir);
|
|
203
|
+
const ref = {
|
|
204
|
+
tool: CACHE_TOOL,
|
|
205
|
+
scopeDir: options.monorepoRoot,
|
|
206
|
+
slug: `${path.basename(anchorDir)}-${options.command.replaceAll(UNSAFE_SLUG_CHARACTERS, '-')}`,
|
|
207
|
+
extension: '.json',
|
|
208
|
+
discriminators: [anchorDir, options.command],
|
|
209
|
+
};
|
|
210
|
+
return resolveCacheEntryPath(ref);
|
|
211
|
+
}
|
|
212
|
+
function resolveInstallFingerprint(monorepoRoot) {
|
|
213
|
+
const hash = createHash('sha256');
|
|
214
|
+
for (const relativePath of INSTALL_FINGERPRINT_FILES) {
|
|
215
|
+
try {
|
|
216
|
+
hash.update(readFileSync(path.join(monorepoRoot, relativePath)));
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return { ok: false, reason: `no install fingerprint: ${relativePath} is unreadable` };
|
|
220
|
+
}
|
|
221
|
+
hash.update('\0');
|
|
222
|
+
}
|
|
223
|
+
return { ok: true, fingerprint: hash.digest('hex') };
|
|
224
|
+
}
|
package/dist/esm/cli-build.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
3
|
-
import {
|
|
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,18 +18,24 @@ 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.
|
|
25
|
+
ignore: [...(options.ignorePatterns ?? DEFAULT_IGNORE_PATTERNS), ...(options.extraIgnorePatterns ?? [])],
|
|
25
26
|
});
|
|
26
|
-
const
|
|
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
|
|
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');
|
|
@@ -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 =
|
|
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 {
|
|
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
|
|
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);
|
package/dist/esm/config.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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>;
|