@tyndall/dynamic-graph 0.0.1
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/README.md +27 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/manager.d.ts +41 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +433 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @tyndall/dynamic-graph
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
Dynamic module graph manager package for lazy entry expansion and impact analysis.
|
|
5
|
+
|
|
6
|
+
## Responsibilities
|
|
7
|
+
- Maintain global module DAG and entry views
|
|
8
|
+
- Compute impacted entries from file changes
|
|
9
|
+
- Support shared subgraph convergence and graph hygiene
|
|
10
|
+
|
|
11
|
+
## Public API Highlights
|
|
12
|
+
- createDynamicModuleGraphManager
|
|
13
|
+
- DynamicModuleGraphManager interface
|
|
14
|
+
|
|
15
|
+
## Development
|
|
16
|
+
- Build: bun run --filter @tyndall/dynamic-graph build
|
|
17
|
+
- Test (from workspace root): bun test
|
|
18
|
+
|
|
19
|
+
## Documentation
|
|
20
|
+
- Package specification: [spec.md](./spec.md)
|
|
21
|
+
- Package architecture: [architecture.md](./architecture.md)
|
|
22
|
+
- Package changes: [CHANGELOG.md](./CHANGELOG.md)
|
|
23
|
+
|
|
24
|
+
## Maintenance Rules
|
|
25
|
+
- Keep this document aligned with implemented package behavior.
|
|
26
|
+
- Update spec.md and architecture.md whenever package contracts or design boundaries change.
|
|
27
|
+
- Record user-visible package changes in CHANGELOG.md.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { createDynamicModuleGraphManager, DEFAULT_IMPORT_EXTENSIONS, } from "./manager.js";
|
|
2
|
+
export type { DynamicModuleGraphManager, DynamicModuleGraphEntry, DynamicModuleGraphSnapshot, DynamicModuleGraphStats, DynamicModuleGraphManagerOptions, DynamicModuleGraphChangeResult, } from "./manager.js";
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,+BAA+B,EAC/B,yBAAyB,GAC1B,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,yBAAyB,EACzB,uBAAuB,EACvB,0BAA0B,EAC1B,uBAAuB,EACvB,gCAAgC,EAChC,8BAA8B,GAC/B,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createDynamicModuleGraphManager, DEFAULT_IMPORT_EXTENSIONS, } from "./manager.js";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export declare const DEFAULT_IMPORT_EXTENSIONS: string[];
|
|
2
|
+
export interface DynamicModuleGraphEntry {
|
|
3
|
+
entryId: string;
|
|
4
|
+
filePath: string;
|
|
5
|
+
}
|
|
6
|
+
export interface DynamicModuleGraphSnapshot {
|
|
7
|
+
moduleGraph: Record<string, string[]>;
|
|
8
|
+
entryToModules: Record<string, string[]>;
|
|
9
|
+
moduleToEntries: Record<string, string[]>;
|
|
10
|
+
}
|
|
11
|
+
export interface DynamicModuleGraphStats {
|
|
12
|
+
nodeCount: number;
|
|
13
|
+
entryCount: number;
|
|
14
|
+
visitGeneration: number;
|
|
15
|
+
}
|
|
16
|
+
export interface DynamicModuleGraphChangeResult {
|
|
17
|
+
changedModules: string[];
|
|
18
|
+
impactedEntryIds: string[];
|
|
19
|
+
unknownModules: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface DynamicModuleGraphManagerOptions {
|
|
22
|
+
extensions?: string[];
|
|
23
|
+
now?: () => number;
|
|
24
|
+
maxEntries?: number;
|
|
25
|
+
maxNodes?: number;
|
|
26
|
+
entryTtlMs?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface DynamicModuleGraphManager {
|
|
29
|
+
registerEntry: (entry: DynamicModuleGraphEntry) => void;
|
|
30
|
+
removeEntry: (entryId: string) => void;
|
|
31
|
+
ensureEntryGraph: (entry: DynamicModuleGraphEntry) => string[];
|
|
32
|
+
rebuildEntry: (entryId: string) => string[];
|
|
33
|
+
getEntryModules: (entryId: string) => string[];
|
|
34
|
+
hasEntry: (entryId: string) => boolean;
|
|
35
|
+
applyFileChanges: (paths: string[]) => DynamicModuleGraphChangeResult;
|
|
36
|
+
collectGarbage: () => void;
|
|
37
|
+
snapshot: () => DynamicModuleGraphSnapshot;
|
|
38
|
+
stats: () => DynamicModuleGraphStats;
|
|
39
|
+
}
|
|
40
|
+
export declare const createDynamicModuleGraphManager: (options?: DynamicModuleGraphManagerOptions) => DynamicModuleGraphManager;
|
|
41
|
+
//# sourceMappingURL=manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,yBAAyB,UASrC,CAAC;AAEF,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACtC,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACzC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,8BAA8B;IAC7C,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,gCAAgC;IAC/C,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,IAAI,CAAC;IACxD,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,gBAAgB,EAAE,CAAC,KAAK,EAAE,uBAAuB,KAAK,MAAM,EAAE,CAAC;IAC/D,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IAC5C,eAAe,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IAC/C,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACvC,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,8BAA8B,CAAC;IACtE,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,0BAA0B,CAAC;IAC3C,KAAK,EAAE,MAAM,uBAAuB,CAAC;CACtC;AA2gBD,eAAO,MAAM,+BAA+B,GAC1C,UAAS,gCAAqC,KAC7C,yBAAuE,CAAC"}
|
package/dist/manager.js
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, extname, join, resolve } from "node:path";
|
|
3
|
+
import { hash, normalizePath } from "@tyndall/shared";
|
|
4
|
+
export const DEFAULT_IMPORT_EXTENSIONS = [
|
|
5
|
+
".ts",
|
|
6
|
+
".tsx",
|
|
7
|
+
".js",
|
|
8
|
+
".jsx",
|
|
9
|
+
".mjs",
|
|
10
|
+
".cjs",
|
|
11
|
+
".mts",
|
|
12
|
+
".cts",
|
|
13
|
+
];
|
|
14
|
+
const normalizeModuleId = (filePath) => normalizePath(resolve(filePath));
|
|
15
|
+
const sortStrings = (values) => Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
|
|
16
|
+
const extractImportSpecifiers = (source) => {
|
|
17
|
+
const specifiers = [];
|
|
18
|
+
const regex = /(?:import|export)\s+[^'"]*?from\s*['"]([^'"]+)['"]|import\s*['"]([^'"]+)['"]|import\(\s*['"]([^'"]+)['"]\s*\)|require\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
19
|
+
let match;
|
|
20
|
+
while ((match = regex.exec(source)) !== null) {
|
|
21
|
+
const specifier = match[1] ?? match[2] ?? match[3] ?? match[4];
|
|
22
|
+
if (specifier) {
|
|
23
|
+
specifiers.push(specifier);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return specifiers;
|
|
27
|
+
};
|
|
28
|
+
const resolveModulePath = (baseDir, specifier, extensions) => {
|
|
29
|
+
const candidate = resolve(baseDir, specifier);
|
|
30
|
+
const extension = extname(candidate);
|
|
31
|
+
const tryFile = (filePath) => {
|
|
32
|
+
try {
|
|
33
|
+
const stats = statSync(filePath);
|
|
34
|
+
if (stats.isFile()) {
|
|
35
|
+
return filePath;
|
|
36
|
+
}
|
|
37
|
+
if (stats.isDirectory()) {
|
|
38
|
+
for (const ext of extensions) {
|
|
39
|
+
const indexPath = join(filePath, `index${ext}`);
|
|
40
|
+
try {
|
|
41
|
+
const indexStats = statSync(indexPath);
|
|
42
|
+
if (indexStats.isFile()) {
|
|
43
|
+
return indexPath;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
};
|
|
57
|
+
if (extension) {
|
|
58
|
+
return tryFile(candidate);
|
|
59
|
+
}
|
|
60
|
+
for (const ext of extensions) {
|
|
61
|
+
const resolved = tryFile(`${candidate}${ext}`);
|
|
62
|
+
if (resolved) {
|
|
63
|
+
return resolved;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return tryFile(candidate);
|
|
67
|
+
};
|
|
68
|
+
class DynamicModuleGraphManagerImpl {
|
|
69
|
+
constructor(options) {
|
|
70
|
+
this.nodes = new Map();
|
|
71
|
+
this.entries = new Map();
|
|
72
|
+
this.entryRoots = new Map();
|
|
73
|
+
this.visitGeneration = 0;
|
|
74
|
+
this.extensions = options.extensions ?? DEFAULT_IMPORT_EXTENSIONS;
|
|
75
|
+
this.now = options.now ?? (() => Date.now());
|
|
76
|
+
this.maxEntries = options.maxEntries ?? Number.POSITIVE_INFINITY;
|
|
77
|
+
this.maxNodes = options.maxNodes ?? Number.POSITIVE_INFINITY;
|
|
78
|
+
this.entryTtlMs = options.entryTtlMs ?? Number.POSITIVE_INFINITY;
|
|
79
|
+
}
|
|
80
|
+
registerEntry(entry) {
|
|
81
|
+
const entryId = entry.entryId;
|
|
82
|
+
const rootModuleId = normalizeModuleId(entry.filePath);
|
|
83
|
+
const now = this.now();
|
|
84
|
+
const existing = this.entries.get(entryId);
|
|
85
|
+
if (!existing) {
|
|
86
|
+
this.entries.set(entryId, {
|
|
87
|
+
id: entryId,
|
|
88
|
+
rootModuleId,
|
|
89
|
+
reachableModules: new Set(),
|
|
90
|
+
lastAccessAt: now,
|
|
91
|
+
version: 0,
|
|
92
|
+
});
|
|
93
|
+
this.indexEntryRoot(entryId, rootModuleId);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
existing.lastAccessAt = now;
|
|
97
|
+
if (existing.rootModuleId === rootModuleId) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.unindexEntryRoot(entryId, existing.rootModuleId);
|
|
101
|
+
const removedModules = sortStrings(existing.reachableModules);
|
|
102
|
+
this.detachEntryRefs(entryId, existing.reachableModules);
|
|
103
|
+
existing.rootModuleId = rootModuleId;
|
|
104
|
+
existing.reachableModules = new Set();
|
|
105
|
+
existing.version += 1;
|
|
106
|
+
this.indexEntryRoot(entryId, rootModuleId);
|
|
107
|
+
this.pruneOrphans(removedModules);
|
|
108
|
+
}
|
|
109
|
+
removeEntry(entryId) {
|
|
110
|
+
const existing = this.entries.get(entryId);
|
|
111
|
+
if (!existing) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.unindexEntryRoot(entryId, existing.rootModuleId);
|
|
115
|
+
const removedModules = sortStrings(existing.reachableModules);
|
|
116
|
+
this.detachEntryRefs(entryId, existing.reachableModules);
|
|
117
|
+
this.entries.delete(entryId);
|
|
118
|
+
this.pruneOrphans(removedModules);
|
|
119
|
+
}
|
|
120
|
+
ensureEntryGraph(entry) {
|
|
121
|
+
this.registerEntry(entry);
|
|
122
|
+
return this.rebuildEntry(entry.entryId);
|
|
123
|
+
}
|
|
124
|
+
rebuildEntry(entryId) {
|
|
125
|
+
const view = this.entries.get(entryId);
|
|
126
|
+
if (!view) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
view.lastAccessAt = this.now();
|
|
130
|
+
const reachableModules = this.collectReachableModules(view.rootModuleId);
|
|
131
|
+
const previousModules = view.reachableModules;
|
|
132
|
+
const removedModules = [];
|
|
133
|
+
let changed = previousModules.size !== reachableModules.size;
|
|
134
|
+
for (const moduleId of previousModules) {
|
|
135
|
+
if (reachableModules.has(moduleId)) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
removedModules.push(moduleId);
|
|
139
|
+
const node = this.nodes.get(moduleId);
|
|
140
|
+
node?.entryRefs.delete(entryId);
|
|
141
|
+
changed = true;
|
|
142
|
+
}
|
|
143
|
+
for (const moduleId of reachableModules) {
|
|
144
|
+
const node = this.ensureNode(moduleId);
|
|
145
|
+
if (!previousModules.has(moduleId)) {
|
|
146
|
+
changed = true;
|
|
147
|
+
}
|
|
148
|
+
node.entryRefs.add(entryId);
|
|
149
|
+
}
|
|
150
|
+
view.reachableModules = reachableModules;
|
|
151
|
+
if (changed) {
|
|
152
|
+
view.version += 1;
|
|
153
|
+
}
|
|
154
|
+
this.pruneOrphans(removedModules);
|
|
155
|
+
this.collectGarbage();
|
|
156
|
+
return sortStrings(reachableModules);
|
|
157
|
+
}
|
|
158
|
+
getEntryModules(entryId) {
|
|
159
|
+
const view = this.entries.get(entryId);
|
|
160
|
+
if (!view) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
return sortStrings(view.reachableModules);
|
|
164
|
+
}
|
|
165
|
+
hasEntry(entryId) {
|
|
166
|
+
return this.entries.has(entryId);
|
|
167
|
+
}
|
|
168
|
+
applyFileChanges(paths) {
|
|
169
|
+
const changedModules = sortStrings(paths.map((path) => normalizeModuleId(path)));
|
|
170
|
+
const impactedEntryIds = new Set();
|
|
171
|
+
const unknownModules = [];
|
|
172
|
+
for (const moduleId of changedModules) {
|
|
173
|
+
const node = this.nodes.get(moduleId);
|
|
174
|
+
if (!node) {
|
|
175
|
+
unknownModules.push(moduleId);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
this.markDirtyUpstream(moduleId, impactedEntryIds);
|
|
179
|
+
}
|
|
180
|
+
const rootedEntries = this.entryRoots.get(moduleId);
|
|
181
|
+
if (!rootedEntries) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
for (const entryId of rootedEntries) {
|
|
185
|
+
impactedEntryIds.add(entryId);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
this.collectGarbage();
|
|
189
|
+
return {
|
|
190
|
+
changedModules,
|
|
191
|
+
impactedEntryIds: sortStrings(impactedEntryIds),
|
|
192
|
+
unknownModules: sortStrings(unknownModules),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
collectGarbage() {
|
|
196
|
+
const now = this.now();
|
|
197
|
+
if (Number.isFinite(this.entryTtlMs)) {
|
|
198
|
+
const staleEntries = Array.from(this.entries.values())
|
|
199
|
+
.filter((entry) => now - entry.lastAccessAt > this.entryTtlMs)
|
|
200
|
+
.map((entry) => entry.id);
|
|
201
|
+
for (const entryId of staleEntries) {
|
|
202
|
+
this.removeEntry(entryId);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (Number.isFinite(this.maxEntries) && this.entries.size > this.maxEntries) {
|
|
206
|
+
const coldEntries = Array.from(this.entries.values())
|
|
207
|
+
.sort((left, right) => left.lastAccessAt - right.lastAccessAt)
|
|
208
|
+
.map((entry) => entry.id);
|
|
209
|
+
for (const entryId of coldEntries) {
|
|
210
|
+
if (this.entries.size <= this.maxEntries) {
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
this.removeEntry(entryId);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (Number.isFinite(this.maxNodes) && this.nodes.size > this.maxNodes) {
|
|
217
|
+
const coldEntries = Array.from(this.entries.values())
|
|
218
|
+
.sort((left, right) => left.lastAccessAt - right.lastAccessAt)
|
|
219
|
+
.map((entry) => entry.id);
|
|
220
|
+
for (const entryId of coldEntries) {
|
|
221
|
+
if (this.nodes.size <= this.maxNodes) {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
this.removeEntry(entryId);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
snapshot() {
|
|
229
|
+
const moduleGraph = {};
|
|
230
|
+
const entryToModules = {};
|
|
231
|
+
const moduleToEntries = {};
|
|
232
|
+
const moduleIds = sortStrings(this.nodes.keys());
|
|
233
|
+
for (const moduleId of moduleIds) {
|
|
234
|
+
const node = this.nodes.get(moduleId);
|
|
235
|
+
if (!node) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
moduleGraph[moduleId] = sortStrings(node.deps);
|
|
239
|
+
moduleToEntries[moduleId] = sortStrings(node.entryRefs);
|
|
240
|
+
}
|
|
241
|
+
const entryIds = sortStrings(this.entries.keys());
|
|
242
|
+
for (const entryId of entryIds) {
|
|
243
|
+
const view = this.entries.get(entryId);
|
|
244
|
+
if (!view) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
entryToModules[entryId] = sortStrings(view.reachableModules);
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
moduleGraph,
|
|
251
|
+
entryToModules,
|
|
252
|
+
moduleToEntries,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
stats() {
|
|
256
|
+
return {
|
|
257
|
+
nodeCount: this.nodes.size,
|
|
258
|
+
entryCount: this.entries.size,
|
|
259
|
+
visitGeneration: this.visitGeneration,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
ensureNode(moduleId) {
|
|
263
|
+
const existing = this.nodes.get(moduleId);
|
|
264
|
+
if (existing) {
|
|
265
|
+
return existing;
|
|
266
|
+
}
|
|
267
|
+
const created = {
|
|
268
|
+
id: moduleId,
|
|
269
|
+
deps: new Set(),
|
|
270
|
+
parents: new Set(),
|
|
271
|
+
entryRefs: new Set(),
|
|
272
|
+
contentHash: null,
|
|
273
|
+
parsed: false,
|
|
274
|
+
dirty: true,
|
|
275
|
+
lastVisitedGeneration: 0,
|
|
276
|
+
lastAccessAt: this.now(),
|
|
277
|
+
};
|
|
278
|
+
this.nodes.set(moduleId, created);
|
|
279
|
+
return created;
|
|
280
|
+
}
|
|
281
|
+
collectReachableModules(rootModuleId) {
|
|
282
|
+
this.visitGeneration += 1;
|
|
283
|
+
const generation = this.visitGeneration;
|
|
284
|
+
const reachable = new Set();
|
|
285
|
+
const stack = [rootModuleId];
|
|
286
|
+
while (stack.length > 0) {
|
|
287
|
+
const moduleId = stack.pop();
|
|
288
|
+
if (!moduleId) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const node = this.ensureNode(moduleId);
|
|
292
|
+
// Deduplicate traversal inside the same rebuild batch without allocating an extra visited set.
|
|
293
|
+
if (node.lastVisitedGeneration === generation) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
node.lastVisitedGeneration = generation;
|
|
297
|
+
node.lastAccessAt = this.now();
|
|
298
|
+
reachable.add(moduleId);
|
|
299
|
+
this.refreshNode(node);
|
|
300
|
+
for (const depId of node.deps) {
|
|
301
|
+
stack.push(depId);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return reachable;
|
|
305
|
+
}
|
|
306
|
+
refreshNode(node) {
|
|
307
|
+
if (node.parsed && !node.dirty) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const source = this.readSource(node.id);
|
|
311
|
+
const nextHash = source === null ? null : hash(source);
|
|
312
|
+
// Keep existing edges when content hash is identical even if the node was marked dirty by upstream propagation.
|
|
313
|
+
if (node.parsed && node.contentHash === nextHash) {
|
|
314
|
+
node.dirty = false;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const nextDeps = source === null ? [] : this.resolveDependencies(node.id, source);
|
|
318
|
+
this.reconcileNodeDeps(node, nextDeps);
|
|
319
|
+
node.contentHash = nextHash;
|
|
320
|
+
node.parsed = true;
|
|
321
|
+
node.dirty = false;
|
|
322
|
+
}
|
|
323
|
+
resolveDependencies(moduleId, source) {
|
|
324
|
+
const baseDir = dirname(moduleId);
|
|
325
|
+
const dependencies = [];
|
|
326
|
+
for (const specifier of extractImportSpecifiers(source)) {
|
|
327
|
+
if (!specifier.startsWith(".")) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const resolved = resolveModulePath(baseDir, specifier, this.extensions);
|
|
331
|
+
if (!resolved) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
dependencies.push(normalizeModuleId(resolved));
|
|
335
|
+
}
|
|
336
|
+
return sortStrings(dependencies);
|
|
337
|
+
}
|
|
338
|
+
reconcileNodeDeps(node, nextDeps) {
|
|
339
|
+
const next = new Set(nextDeps);
|
|
340
|
+
for (const depId of node.deps) {
|
|
341
|
+
if (next.has(depId)) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const depNode = this.nodes.get(depId);
|
|
345
|
+
depNode?.parents.delete(node.id);
|
|
346
|
+
}
|
|
347
|
+
for (const depId of next) {
|
|
348
|
+
if (node.deps.has(depId)) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
const depNode = this.ensureNode(depId);
|
|
352
|
+
depNode.parents.add(node.id);
|
|
353
|
+
}
|
|
354
|
+
node.deps = next;
|
|
355
|
+
}
|
|
356
|
+
readSource(moduleId) {
|
|
357
|
+
try {
|
|
358
|
+
return readFileSync(moduleId, "utf-8");
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
markDirtyUpstream(moduleId, impactedEntryIds) {
|
|
365
|
+
const stack = [moduleId];
|
|
366
|
+
const visited = new Set();
|
|
367
|
+
while (stack.length > 0) {
|
|
368
|
+
const currentId = stack.pop();
|
|
369
|
+
if (!currentId || visited.has(currentId)) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
visited.add(currentId);
|
|
373
|
+
const node = this.nodes.get(currentId);
|
|
374
|
+
if (!node) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
node.dirty = true;
|
|
378
|
+
for (const entryId of node.entryRefs) {
|
|
379
|
+
impactedEntryIds.add(entryId);
|
|
380
|
+
}
|
|
381
|
+
// Parent propagation lets us conservatively invalidate every entry path that can reach the changed module.
|
|
382
|
+
for (const parentId of node.parents) {
|
|
383
|
+
stack.push(parentId);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
detachEntryRefs(entryId, moduleIds) {
|
|
388
|
+
for (const moduleId of moduleIds) {
|
|
389
|
+
const node = this.nodes.get(moduleId);
|
|
390
|
+
node?.entryRefs.delete(entryId);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
pruneOrphans(seedModuleIds) {
|
|
394
|
+
const stack = Array.from(seedModuleIds);
|
|
395
|
+
const visited = new Set();
|
|
396
|
+
while (stack.length > 0) {
|
|
397
|
+
const moduleId = stack.pop();
|
|
398
|
+
if (!moduleId || visited.has(moduleId)) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
visited.add(moduleId);
|
|
402
|
+
const node = this.nodes.get(moduleId);
|
|
403
|
+
if (!node) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (node.entryRefs.size > 0 || node.parents.size > 0) {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
this.nodes.delete(moduleId);
|
|
410
|
+
for (const depId of node.deps) {
|
|
411
|
+
const depNode = this.nodes.get(depId);
|
|
412
|
+
depNode?.parents.delete(moduleId);
|
|
413
|
+
stack.push(depId);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
indexEntryRoot(entryId, rootModuleId) {
|
|
418
|
+
const rootedEntries = this.entryRoots.get(rootModuleId) ?? new Set();
|
|
419
|
+
rootedEntries.add(entryId);
|
|
420
|
+
this.entryRoots.set(rootModuleId, rootedEntries);
|
|
421
|
+
}
|
|
422
|
+
unindexEntryRoot(entryId, rootModuleId) {
|
|
423
|
+
const rootedEntries = this.entryRoots.get(rootModuleId);
|
|
424
|
+
if (!rootedEntries) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
rootedEntries.delete(entryId);
|
|
428
|
+
if (rootedEntries.size === 0) {
|
|
429
|
+
this.entryRoots.delete(rootModuleId);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
export const createDynamicModuleGraphManager = (options = {}) => new DynamicModuleGraphManagerImpl(options);
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tyndall/dynamic-graph",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"bun": "./src/index.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.json"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@tyndall/shared": "workspace:*"
|
|
25
|
+
}
|
|
26
|
+
}
|