@react-native-harness/cache 1.4.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/LICENSE +20 -0
- package/README.md +26 -0
- package/dist/__tests__/boundary.test.d.ts +2 -0
- package/dist/__tests__/boundary.test.d.ts.map +1 -0
- package/dist/__tests__/boundary.test.js +77 -0
- package/dist/__tests__/domains/metro.test.d.ts +2 -0
- package/dist/__tests__/domains/metro.test.d.ts.map +1 -0
- package/dist/__tests__/domains/metro.test.js +234 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.js +40 -0
- package/dist/__tests__/paths.test.d.ts +2 -0
- package/dist/__tests__/paths.test.d.ts.map +1 -0
- package/dist/__tests__/paths.test.js +12 -0
- package/dist/__tests__/plan.test.d.ts +2 -0
- package/dist/__tests__/plan.test.d.ts.map +1 -0
- package/dist/__tests__/plan.test.js +197 -0
- package/dist/__tests__/warmth.test.d.ts +2 -0
- package/dist/__tests__/warmth.test.d.ts.map +1 -0
- package/dist/__tests__/warmth.test.js +36 -0
- package/dist/domains/metro.d.ts +8 -0
- package/dist/domains/metro.d.ts.map +1 -0
- package/dist/domains/metro.js +151 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/paths.d.ts +14 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +26 -0
- package/dist/plan.d.ts +54 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +177 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/warmth.d.ts +8 -0
- package/dist/warmth.d.ts.map +1 -0
- package/dist/warmth.js +40 -0
- package/eslint.config.mjs +20 -0
- package/package.json +22 -0
- package/src/__tests__/boundary.test.ts +95 -0
- package/src/__tests__/domains/metro.test.ts +298 -0
- package/src/__tests__/index.test.ts +54 -0
- package/src/__tests__/paths.test.ts +18 -0
- package/src/__tests__/plan.test.ts +330 -0
- package/src/__tests__/warmth.test.ts +47 -0
- package/src/domains/metro.ts +202 -0
- package/src/index.ts +84 -0
- package/src/paths.ts +43 -0
- package/src/plan.ts +285 -0
- package/src/warmth.ts +59 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +18 -0
- package/vite.config.ts +18 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { logger } from '@react-native-harness/tools';
|
|
3
|
+
import {
|
|
4
|
+
createHarnessCachePaths,
|
|
5
|
+
getDomainDirectories,
|
|
6
|
+
type CacheDomainId,
|
|
7
|
+
type HarnessCachePaths,
|
|
8
|
+
} from './paths.js';
|
|
9
|
+
import {
|
|
10
|
+
planRestore,
|
|
11
|
+
planSave,
|
|
12
|
+
snapshot,
|
|
13
|
+
writeKeysFile,
|
|
14
|
+
type CacheKeyInputs,
|
|
15
|
+
type CacheSnapshot,
|
|
16
|
+
type CacheSnapshotEntry,
|
|
17
|
+
type DomainRestorePlan,
|
|
18
|
+
type DomainSavePlan,
|
|
19
|
+
type RestorePlan,
|
|
20
|
+
type SavePlan,
|
|
21
|
+
type SavePolicy,
|
|
22
|
+
} from './plan.js';
|
|
23
|
+
import { isDomainWarm } from './warmth.js';
|
|
24
|
+
import { computeMetroStaticInputs, METRO_KEY_FILENAMES } from './domains/metro.js';
|
|
25
|
+
|
|
26
|
+
export { computeMetroStaticInputs, METRO_KEY_FILENAMES };
|
|
27
|
+
|
|
28
|
+
export type { CacheDomainId, HarnessCachePaths };
|
|
29
|
+
export type {
|
|
30
|
+
CacheKeyInputs,
|
|
31
|
+
CacheSnapshot,
|
|
32
|
+
CacheSnapshotEntry,
|
|
33
|
+
DomainRestorePlan,
|
|
34
|
+
DomainSavePlan,
|
|
35
|
+
RestorePlan,
|
|
36
|
+
SavePlan,
|
|
37
|
+
SavePolicy,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export interface HarnessCache {
|
|
41
|
+
readonly paths: HarnessCachePaths;
|
|
42
|
+
isWarm(domain: CacheDomainId): boolean;
|
|
43
|
+
ensureDomainDirectories(domain: CacheDomainId): void;
|
|
44
|
+
planRestore(
|
|
45
|
+
inputs: Partial<Record<CacheDomainId, CacheKeyInputs>>
|
|
46
|
+
): RestorePlan;
|
|
47
|
+
snapshot(): CacheSnapshot;
|
|
48
|
+
planSave(
|
|
49
|
+
before: CacheSnapshot,
|
|
50
|
+
policy: SavePolicy,
|
|
51
|
+
inputs: Partial<Record<CacheDomainId, CacheKeyInputs>>
|
|
52
|
+
): SavePlan;
|
|
53
|
+
writeKeysFile(plan: RestorePlan | SavePlan): void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const cacheLogger = logger.child('cache');
|
|
57
|
+
|
|
58
|
+
export const createHarnessCache = (options: {
|
|
59
|
+
projectRoot: string;
|
|
60
|
+
}): HarnessCache => {
|
|
61
|
+
const paths = createHarnessCachePaths(options.projectRoot);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
paths,
|
|
65
|
+
isWarm: (domain) => isDomainWarm(domain, paths),
|
|
66
|
+
ensureDomainDirectories: (domain) => {
|
|
67
|
+
for (const directory of getDomainDirectories(domain, paths)) {
|
|
68
|
+
try {
|
|
69
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
cacheLogger.warn(
|
|
72
|
+
`Failed to create cache directory "${directory}". Continuing with a cold cache.`,
|
|
73
|
+
error
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
planRestore: (inputs) => planRestore(paths, inputs),
|
|
79
|
+
snapshot: () => snapshot(paths),
|
|
80
|
+
planSave: (before, policy, inputs) =>
|
|
81
|
+
planSave(paths, before, policy, inputs),
|
|
82
|
+
writeKeysFile: (plan) => writeKeysFile(paths, plan),
|
|
83
|
+
};
|
|
84
|
+
};
|
package/src/paths.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
const HARNESS_DIRNAME = '.harness';
|
|
4
|
+
const CACHE_DIRNAME = 'cache';
|
|
5
|
+
const METRO_TRANSFORM_DIRNAME = 'metro';
|
|
6
|
+
const METRO_FILE_MAP_DIRNAME = 'metro-file-map';
|
|
7
|
+
|
|
8
|
+
export type CacheDomainId = 'metro';
|
|
9
|
+
|
|
10
|
+
export interface HarnessCachePaths {
|
|
11
|
+
readonly root: string;
|
|
12
|
+
readonly metroTransform: string;
|
|
13
|
+
readonly metroFileMap: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const createHarnessCachePaths = (
|
|
17
|
+
projectRoot: string
|
|
18
|
+
): HarnessCachePaths => {
|
|
19
|
+
const root = path.join(projectRoot, HARNESS_DIRNAME, CACHE_DIRNAME);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
root,
|
|
23
|
+
metroTransform: path.join(root, METRO_TRANSFORM_DIRNAME),
|
|
24
|
+
metroFileMap: path.join(root, METRO_FILE_MAP_DIRNAME),
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Directories that make up a cache domain. `metro` covers both the transform
|
|
30
|
+
* cache and the file-map (haste) cache so `ensureDomainDirectories` prepares
|
|
31
|
+
* both ahead of a run.
|
|
32
|
+
*/
|
|
33
|
+
export const getDomainDirectories = (
|
|
34
|
+
domain: CacheDomainId,
|
|
35
|
+
paths: HarnessCachePaths
|
|
36
|
+
): readonly string[] => {
|
|
37
|
+
switch (domain) {
|
|
38
|
+
case 'metro':
|
|
39
|
+
return [paths.metroTransform, paths.metroFileMap];
|
|
40
|
+
default:
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
};
|
package/src/plan.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { logger } from '@react-native-harness/tools';
|
|
5
|
+
import { getDomainDirectories } from './paths.js';
|
|
6
|
+
import type { CacheDomainId, HarnessCachePaths } from './paths.js';
|
|
7
|
+
|
|
8
|
+
const cacheLogger = logger.child('cache');
|
|
9
|
+
|
|
10
|
+
const KEYS_FILENAME = 'keys.json';
|
|
11
|
+
const HASH_LENGTH = 16;
|
|
12
|
+
|
|
13
|
+
export interface CacheKeyInputs {
|
|
14
|
+
os: string;
|
|
15
|
+
staticInputs: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DomainRestorePlan {
|
|
19
|
+
paths: string[];
|
|
20
|
+
restoreKey: string;
|
|
21
|
+
restorePrefixes: string[];
|
|
22
|
+
accretive: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RestorePlan {
|
|
26
|
+
version: 1;
|
|
27
|
+
domains: Partial<Record<CacheDomainId, DomainRestorePlan>>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CacheSnapshotEntry {
|
|
31
|
+
path: string;
|
|
32
|
+
sizeBytes: number;
|
|
33
|
+
mtimeMs: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type CacheSnapshot = Partial<Record<CacheDomainId, CacheSnapshotEntry[]>>;
|
|
37
|
+
|
|
38
|
+
export interface SavePolicy {
|
|
39
|
+
mode: 'default-branch' | 'always' | 'never';
|
|
40
|
+
isDefaultBranch: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DomainSavePlan {
|
|
44
|
+
shouldSave: boolean;
|
|
45
|
+
saveKey: string;
|
|
46
|
+
delta: {
|
|
47
|
+
addedEntries: number;
|
|
48
|
+
removedEntries: number;
|
|
49
|
+
addedBytes: number;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SavePlan {
|
|
54
|
+
version: 1;
|
|
55
|
+
domains: Partial<Record<CacheDomainId, DomainSavePlan>>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ALL_DOMAINS: readonly CacheDomainId[] = ['metro'];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Accretive domains only ever grow (transform/file-map caches keyed by
|
|
62
|
+
* content hash) so a save is always safe to key off the static hash plus a
|
|
63
|
+
* content hash, and prior saves can be found via a prefix match on the
|
|
64
|
+
* static hash. Non-accretive domains would need an exact key match instead.
|
|
65
|
+
* A domain not yet handled here defaults to non-accretive (exact-match),
|
|
66
|
+
* matching the safe-fallback style of `getDomainDirectories`/
|
|
67
|
+
* `getWarmthDirectory`: prefix-matching into an unknown domain's saves would
|
|
68
|
+
* not be a neutral no-op, so the conservative default is to require an exact
|
|
69
|
+
* key match rather than silently opt a future domain into accretive lookup.
|
|
70
|
+
*/
|
|
71
|
+
const isAccretiveDomain = (domain: CacheDomainId): boolean => {
|
|
72
|
+
switch (domain) {
|
|
73
|
+
case 'metro':
|
|
74
|
+
return true;
|
|
75
|
+
default:
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const hashSortedEntries = (entries: Array<[string, string]>): string =>
|
|
81
|
+
createHash('sha256')
|
|
82
|
+
.update(
|
|
83
|
+
entries
|
|
84
|
+
.slice()
|
|
85
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
86
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
87
|
+
.join('\n')
|
|
88
|
+
)
|
|
89
|
+
.digest('hex')
|
|
90
|
+
.slice(0, HASH_LENGTH);
|
|
91
|
+
|
|
92
|
+
const buildStaticHashKey = (
|
|
93
|
+
domain: CacheDomainId,
|
|
94
|
+
inputs: CacheKeyInputs
|
|
95
|
+
): string => {
|
|
96
|
+
const hash = hashSortedEntries(Object.entries(inputs.staticInputs));
|
|
97
|
+
return `harness-${domain}-${inputs.os}-${hash}`;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const buildContentHash = (entries: CacheSnapshotEntry[]): string =>
|
|
101
|
+
hashSortedEntries(
|
|
102
|
+
entries.map((entry) => [entry.path, String(entry.sizeBytes)])
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const walkDirectory = (
|
|
106
|
+
root: string,
|
|
107
|
+
directory: string,
|
|
108
|
+
entries: CacheSnapshotEntry[]
|
|
109
|
+
): void => {
|
|
110
|
+
let dirents: fs.Dirent[];
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
dirents = fs.readdirSync(directory, { withFileTypes: true });
|
|
114
|
+
} catch {
|
|
115
|
+
// Missing/unreadable directory is treated as empty rather than an error.
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (const dirent of dirents) {
|
|
120
|
+
const fullPath = path.join(directory, dirent.name);
|
|
121
|
+
|
|
122
|
+
if (dirent.isDirectory()) {
|
|
123
|
+
walkDirectory(root, fullPath, entries);
|
|
124
|
+
} else if (dirent.isFile()) {
|
|
125
|
+
const stat = fs.statSync(fullPath);
|
|
126
|
+
entries.push({
|
|
127
|
+
path: path.relative(root, fullPath),
|
|
128
|
+
sizeBytes: stat.size,
|
|
129
|
+
mtimeMs: stat.mtimeMs,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export const planRestore = (
|
|
136
|
+
paths: HarnessCachePaths,
|
|
137
|
+
inputs: Partial<Record<CacheDomainId, CacheKeyInputs>>
|
|
138
|
+
): RestorePlan => {
|
|
139
|
+
const domains: RestorePlan['domains'] = {};
|
|
140
|
+
|
|
141
|
+
for (const domain of ALL_DOMAINS) {
|
|
142
|
+
const domainInputs = inputs[domain];
|
|
143
|
+
if (!domainInputs) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const restoreKey = buildStaticHashKey(domain, domainInputs);
|
|
148
|
+
const accretive = isAccretiveDomain(domain);
|
|
149
|
+
|
|
150
|
+
domains[domain] = {
|
|
151
|
+
paths: [...getDomainDirectories(domain, paths)],
|
|
152
|
+
restoreKey,
|
|
153
|
+
restorePrefixes: accretive ? [`${restoreKey}-`] : [],
|
|
154
|
+
accretive,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { version: 1, domains };
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Synchronous snapshot of on-disk cache contents, keyed by domain. Never
|
|
163
|
+
* throws: a domain whose directories cannot be read is reported as empty.
|
|
164
|
+
*/
|
|
165
|
+
export const snapshot = (paths: HarnessCachePaths): CacheSnapshot => {
|
|
166
|
+
const result: CacheSnapshot = {};
|
|
167
|
+
|
|
168
|
+
for (const domain of ALL_DOMAINS) {
|
|
169
|
+
try {
|
|
170
|
+
const entries: CacheSnapshotEntry[] = [];
|
|
171
|
+
for (const directory of getDomainDirectories(domain, paths)) {
|
|
172
|
+
walkDirectory(paths.root, directory, entries);
|
|
173
|
+
}
|
|
174
|
+
result[domain] = entries;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
cacheLogger.warn(
|
|
177
|
+
`Failed to snapshot cache domain "${domain}". Treating as empty.`,
|
|
178
|
+
error
|
|
179
|
+
);
|
|
180
|
+
result[domain] = [];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return result;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const diffSnapshots = (
|
|
188
|
+
before: CacheSnapshotEntry[],
|
|
189
|
+
after: CacheSnapshotEntry[]
|
|
190
|
+
): DomainSavePlan['delta'] => {
|
|
191
|
+
const beforeByPath = new Map(before.map((entry) => [entry.path, entry]));
|
|
192
|
+
const afterByPath = new Map(after.map((entry) => [entry.path, entry]));
|
|
193
|
+
|
|
194
|
+
let addedEntries = 0;
|
|
195
|
+
let removedEntries = 0;
|
|
196
|
+
let addedBytes = 0;
|
|
197
|
+
|
|
198
|
+
for (const [entryPath, afterEntry] of afterByPath) {
|
|
199
|
+
const beforeEntry = beforeByPath.get(entryPath);
|
|
200
|
+
if (!beforeEntry || beforeEntry.sizeBytes !== afterEntry.sizeBytes) {
|
|
201
|
+
addedEntries += 1;
|
|
202
|
+
addedBytes += afterEntry.sizeBytes;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const entryPath of beforeByPath.keys()) {
|
|
207
|
+
if (!afterByPath.has(entryPath)) {
|
|
208
|
+
removedEntries += 1;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { addedEntries, removedEntries, addedBytes };
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const shouldSaveForPolicy = (
|
|
216
|
+
policy: SavePolicy,
|
|
217
|
+
delta: DomainSavePlan['delta']
|
|
218
|
+
): boolean => {
|
|
219
|
+
if (delta.addedEntries + delta.removedEntries === 0) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
switch (policy.mode) {
|
|
224
|
+
case 'always':
|
|
225
|
+
return true;
|
|
226
|
+
case 'default-branch':
|
|
227
|
+
return policy.isDefaultBranch;
|
|
228
|
+
case 'never':
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export const planSave = (
|
|
234
|
+
paths: HarnessCachePaths,
|
|
235
|
+
before: CacheSnapshot,
|
|
236
|
+
policy: SavePolicy,
|
|
237
|
+
inputs: Partial<Record<CacheDomainId, CacheKeyInputs>>
|
|
238
|
+
): SavePlan => {
|
|
239
|
+
const after = snapshot(paths);
|
|
240
|
+
const domains: SavePlan['domains'] = {};
|
|
241
|
+
|
|
242
|
+
for (const domain of ALL_DOMAINS) {
|
|
243
|
+
const domainInputs = inputs[domain];
|
|
244
|
+
if (!domainInputs) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const beforeEntries = before[domain] ?? [];
|
|
249
|
+
const afterEntries = after[domain] ?? [];
|
|
250
|
+
const delta = diffSnapshots(beforeEntries, afterEntries);
|
|
251
|
+
const staticHashKey = buildStaticHashKey(domain, domainInputs);
|
|
252
|
+
const accretive = isAccretiveDomain(domain);
|
|
253
|
+
|
|
254
|
+
domains[domain] = {
|
|
255
|
+
shouldSave: shouldSaveForPolicy(policy, delta),
|
|
256
|
+
saveKey: accretive
|
|
257
|
+
? `${staticHashKey}-${buildContentHash(afterEntries)}`
|
|
258
|
+
: staticHashKey,
|
|
259
|
+
delta,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { version: 1, domains };
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Written to `<cache-root>/keys.json`, outside any domain's cached
|
|
268
|
+
* subdirectories, so it is never itself part of what gets restored/saved.
|
|
269
|
+
* It is a secondary/debug artifact; the actual data flow between CI steps
|
|
270
|
+
* goes through GITHUB_OUTPUT, not by re-reading this file. Cache I/O must
|
|
271
|
+
* never fail a run, so write failures are logged and swallowed.
|
|
272
|
+
*/
|
|
273
|
+
export const writeKeysFile = (
|
|
274
|
+
paths: HarnessCachePaths,
|
|
275
|
+
plan: RestorePlan | SavePlan
|
|
276
|
+
): void => {
|
|
277
|
+
const filePath = path.join(paths.root, KEYS_FILENAME);
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
fs.mkdirSync(paths.root, { recursive: true });
|
|
281
|
+
fs.writeFileSync(filePath, JSON.stringify(plan, null, 2));
|
|
282
|
+
} catch (error) {
|
|
283
|
+
cacheLogger.warn(`Failed to write cache keys file "${filePath}".`, error);
|
|
284
|
+
}
|
|
285
|
+
};
|
package/src/warmth.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { logger } from '@react-native-harness/tools';
|
|
3
|
+
import type { CacheDomainId, HarnessCachePaths } from './paths.js';
|
|
4
|
+
|
|
5
|
+
const cacheLogger = logger.child('cache');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A domain is warm when its cache directory exists and is non-empty. Cache
|
|
9
|
+
* I/O must never fail a run, so any error while inspecting the filesystem is
|
|
10
|
+
* logged and treated as cold.
|
|
11
|
+
*/
|
|
12
|
+
export const isDomainWarm = (
|
|
13
|
+
domain: CacheDomainId,
|
|
14
|
+
paths: HarnessCachePaths
|
|
15
|
+
): boolean => {
|
|
16
|
+
const directory = getWarmthDirectory(domain, paths);
|
|
17
|
+
|
|
18
|
+
if (!directory) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const stat = fs.statSync(directory);
|
|
24
|
+
|
|
25
|
+
if (!stat.isDirectory()) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return fs.readdirSync(directory).length > 0;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (isMissingPathError(error)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
cacheLogger.warn(
|
|
36
|
+
`Failed to check cache warmth for "${domain}" at "${directory}". Treating as cold.`,
|
|
37
|
+
error
|
|
38
|
+
);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const getWarmthDirectory = (
|
|
44
|
+
domain: CacheDomainId,
|
|
45
|
+
paths: HarnessCachePaths
|
|
46
|
+
): string | undefined => {
|
|
47
|
+
switch (domain) {
|
|
48
|
+
case 'metro':
|
|
49
|
+
return paths.metroTransform;
|
|
50
|
+
default:
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const isMissingPathError = (error: unknown): boolean =>
|
|
56
|
+
typeof error === 'object' &&
|
|
57
|
+
error !== null &&
|
|
58
|
+
'code' in error &&
|
|
59
|
+
(error as { code?: unknown }).code === 'ENOENT';
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"baseUrl": ".",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
|
|
8
|
+
"emitDeclarationOnly": false,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"types": ["node"]
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*.ts"],
|
|
13
|
+
"references": [
|
|
14
|
+
{
|
|
15
|
+
"path": "../tools/tsconfig.lib.json"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/// <reference types='vitest' />
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
|
|
4
|
+
export default defineConfig(() => ({
|
|
5
|
+
root: __dirname,
|
|
6
|
+
cacheDir: '../../node_modules/.vite/packages/cache',
|
|
7
|
+
test: {
|
|
8
|
+
watch: false,
|
|
9
|
+
globals: true,
|
|
10
|
+
environment: 'node',
|
|
11
|
+
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
|
12
|
+
reporters: ['default'],
|
|
13
|
+
coverage: {
|
|
14
|
+
reportsDirectory: './test-output/vitest/coverage',
|
|
15
|
+
provider: 'v8' as const,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
}));
|