forked from LinkedSoftwareDependencies/Components.js
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathModuleStateCache.ts
More file actions
128 lines (119 loc) · 4.27 KB
/
Copy pathModuleStateCache.ts
File metadata and controls
128 lines (119 loc) · 4.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import { promises as fs } from 'node:fs';
import * as Path from 'node:path';
import type { Logger } from 'winston';
// eslint-disable-next-line import/extensions
import packageJson from '../../package.json';
import type { IModuleState } from './ModuleStateBuilder';
/**
* The version of the cache file format.
* Increment when the shape of the persisted data (or of {@link IModuleState}) changes.
*/
const CACHE_FORMAT_VERSION = 1;
/**
* The files (relative to the main module path) whose modification time and size
* are included in the staleness fingerprint of a persisted module state.
*/
const FINGERPRINT_PATHS = [
'package.json',
'package-lock.json',
'yarn.lock',
'pnpm-lock.yaml',
'npm-shrinkwrap.json',
'node_modules',
];
/**
* Persists an {@link IModuleState} (the result of component discovery over the
* dependency tree) to a file, so that subsequent invocations can skip the
* discovery scan entirely.
*
* Staleness handling: the cache stores a fingerprint of the cache format version,
* the componentsjs version, the main module path, and the modification time and
* size of the main module's package.json, lock files, and node_modules directory.
* A fingerprint mismatch (or any read/parse failure) makes {@link ModuleStateCache.load}
* return `undefined`, after which the caller is expected to run a fresh discovery
* scan and {@link ModuleStateCache.save} its result.
*
* This is a heuristic: package installations and removals touch a lock file and
* the node_modules directory, and are detected. In-place modifications deep
* inside node_modules (such as manually editing an installed package's component
* files) are NOT detected; in such cases the cache file must be removed manually
* (or the option not be used).
*/
export class ModuleStateCache {
private readonly path: string;
private readonly mainModulePath: string;
private readonly logger?: Logger;
public constructor(options: IModuleStateCacheOptions) {
this.path = options.path;
this.mainModulePath = options.mainModulePath;
this.logger = options.logger;
}
/**
* Compute the current staleness fingerprint for the main module path.
*/
public async fingerprint(): Promise<string> {
const entries = await Promise.all(FINGERPRINT_PATHS.map(async(subPath) => {
try {
const stat = await fs.stat(Path.posix.join(this.mainModulePath, subPath));
return [ subPath, stat.mtimeMs, stat.size ];
} catch {
return [ subPath, null, null ];
}
}));
return JSON.stringify([ CACHE_FORMAT_VERSION, packageJson.version, this.mainModulePath, entries ]);
}
/**
* Load the persisted module state, if it exists and is fresh.
* @returns The module state, or `undefined` if there is no (fresh, readable) cache entry.
*/
public async load(): Promise<IModuleState | undefined> {
let payload: any;
try {
payload = JSON.parse(await fs.readFile(this.path, 'utf8'));
} catch {
// No (readable) cache file
return;
}
if (!payload || typeof payload !== 'object' || payload.fingerprint !== await this.fingerprint()) {
if (this.logger) {
this.logger.info(`Ignoring stale module state cache at ${this.path}`);
}
return;
}
return payload.moduleState;
}
/**
* Persist the given module state (best-effort: failures are logged, not thrown).
* @param moduleState A module state.
*/
public async save(moduleState: IModuleState): Promise<void> {
try {
const payload = JSON.stringify({
fingerprint: await this.fingerprint(),
moduleState,
});
// Write-then-rename, so concurrent invocations never observe a partial cache file.
const temporaryPath = `${this.path}.${process.pid}.tmp`;
await fs.writeFile(temporaryPath, payload, 'utf8');
await fs.rename(temporaryPath, this.path);
} catch (error: unknown) {
if (this.logger) {
this.logger.warn(`Failed to save module state cache to ${this.path}: ${(<Error> error).message}`);
}
}
}
}
export interface IModuleStateCacheOptions {
/**
* The file path to persist the module state to.
*/
path: string;
/**
* Absolute path to the package root from which module resolution starts.
*/
mainModulePath: string;
/**
* An optional logger.
*/
logger?: Logger;
}