@mandujs/core 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/package.json +3 -1
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -0
- package/src/bundler/build.ts +26 -3
- package/src/bundler/dev.ts +306 -4
- package/src/bundler/reverse-import-graph.ts +339 -0
- package/src/bundler/safe-build.test.ts +54 -0
- package/src/bundler/safe-build.ts +33 -7
- package/src/client/globals.ts +61 -44
- package/src/client/prefetch-helper.ts +55 -0
- package/src/client/router.ts +93 -15
- package/src/client/use-fetch.ts +243 -239
- package/src/config/mandu.ts +280 -172
- package/src/config/validate.ts +411 -357
- package/src/content/collection.ts +506 -0
- package/src/content/frontmatter.ts +189 -0
- package/src/content/generate-types.ts +168 -0
- package/src/content/index.ts +206 -168
- package/src/content/llms-txt.ts +196 -0
- package/src/content/prebuild.test.ts +249 -0
- package/src/content/prebuild.ts +400 -0
- package/src/content/schema.ts +20 -0
- package/src/content/sidebar.ts +212 -0
- package/src/content/slug.ts +110 -0
- package/src/guard/check.ts +5 -2
- package/src/observability/index.ts +37 -18
- package/src/observability/metrics.ts +334 -0
- package/src/runtime/adapter-bun.ts +64 -62
- package/src/runtime/index.ts +11 -0
- package/src/runtime/registry.ts +171 -0
- package/src/runtime/server.ts +214 -8
- package/src/runtime/ssr.ts +264 -8
- package/src/runtime/streaming-ssr.ts +100 -4
- package/src/utils/__tests__/lru-cache.test.ts +186 -0
- package/src/utils/lru-cache.ts +172 -75
package/src/bundler/dev.ts
CHANGED
|
@@ -15,8 +15,15 @@ import type {
|
|
|
15
15
|
HMRReplayEnvelope,
|
|
16
16
|
} from "./hmr-types";
|
|
17
17
|
import { MAX_REPLAY_BUFFER, REPLAY_MAX_AGE_MS } from "./hmr-types";
|
|
18
|
+
import {
|
|
19
|
+
ReverseImportGraph,
|
|
20
|
+
scanFileImports,
|
|
21
|
+
DEFAULT_MAX_CLOSURE_DEPTH,
|
|
22
|
+
} from "./reverse-import-graph";
|
|
18
23
|
import path from "path";
|
|
19
24
|
import fs from "fs";
|
|
25
|
+
import { LRUCache } from "../utils/lru-cache";
|
|
26
|
+
import { registerCacheSize, unregisterCacheSize } from "../observability/metrics";
|
|
20
27
|
|
|
21
28
|
/**
|
|
22
29
|
* #184: 공통 디렉토리 변경 시 사용하는 sentinel.
|
|
@@ -312,6 +319,9 @@ export const _testOnly_DEFAULT_COMMON_DIRS = DEFAULT_COMMON_DIRS;
|
|
|
312
319
|
/** Test-only accessor for the watch exclude segments (B1 coverage). */
|
|
313
320
|
export const _testOnly_WATCH_EXCLUDE_SEGMENTS = WATCH_EXCLUDE_SEGMENTS;
|
|
314
321
|
|
|
322
|
+
/** Test-only re-export for #189 reverse import-graph coverage. */
|
|
323
|
+
export { ReverseImportGraph, scanFileImports } from "./reverse-import-graph";
|
|
324
|
+
|
|
315
325
|
/**
|
|
316
326
|
* Phase 7.0 R2 Agent D — classification helper mirroring the in-bundler
|
|
317
327
|
* `classifyBatch` priority rules WITHOUT the project-specific maps
|
|
@@ -570,6 +580,99 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
|
|
|
570
580
|
await addCommonDir(dir);
|
|
571
581
|
}
|
|
572
582
|
|
|
583
|
+
// #189 — reverse import-graph for transitive invalidation.
|
|
584
|
+
//
|
|
585
|
+
// Built-time index of `importee -> Set<importer>` edges for every
|
|
586
|
+
// known SSR / API / client / common-dir root. On an unknown-file
|
|
587
|
+
// change (a leaf utility that none of our sets recognize) we walk
|
|
588
|
+
// the transitive importer closure and re-dispatch the change
|
|
589
|
+
// against every ancestor that IS known — so a deep edit to
|
|
590
|
+
// `app/_utils/translations/ko.ts` still re-evaluates the barrel
|
|
591
|
+
// in `app/_utils/translations/index.ts` that imported it.
|
|
592
|
+
//
|
|
593
|
+
// The initial population is best-effort (filesystem scan may
|
|
594
|
+
// surface typed-only stubs that have no source yet); misses simply
|
|
595
|
+
// keep the legacy "silent drop" behavior so no project is made
|
|
596
|
+
// worse by turning this on.
|
|
597
|
+
const reverseGraph = new ReverseImportGraph();
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Re-scan a single file's imports and update its row in the
|
|
601
|
+
* reverse graph. Called on startup for every known root, and
|
|
602
|
+
* again each time a file change is dispatched so the graph
|
|
603
|
+
* tracks refactors (an import being added / removed) in real
|
|
604
|
+
* time.
|
|
605
|
+
*
|
|
606
|
+
* Errors are swallowed — a missing file / permission issue must
|
|
607
|
+
* not break the HMR dispatch for the rest of the project.
|
|
608
|
+
*/
|
|
609
|
+
const refreshReverseGraphEdges = async (filePath: string): Promise<void> => {
|
|
610
|
+
try {
|
|
611
|
+
const imports = await scanFileImports(filePath);
|
|
612
|
+
reverseGraph.update(filePath, imports);
|
|
613
|
+
} catch {
|
|
614
|
+
// scanFileImports already swallows fs errors; this extra guard
|
|
615
|
+
// catches anything pathological (e.g. a symlink loop).
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Populate the reverse graph transitively from every known root.
|
|
621
|
+
*
|
|
622
|
+
* The BFS walks each root's outgoing imports, scans every
|
|
623
|
+
* discovered intermediate, and keeps going until the frontier is
|
|
624
|
+
* empty or we hit the depth cap. Without this transitive seed,
|
|
625
|
+
* a deep edit (root -> barrel -> leaf) would see
|
|
626
|
+
* `reverseGraph.directImporters(leaf) === {}` because only the
|
|
627
|
+
* root's edges got recorded — exactly the bug #189 describes.
|
|
628
|
+
*
|
|
629
|
+
* The walk is I/O-bound but bounded: each node is visited at most
|
|
630
|
+
* once (`visited` set) and we cap the seeding at
|
|
631
|
+
* `DEFAULT_MAX_CLOSURE_DEPTH` hops to keep startup cost predictable
|
|
632
|
+
* on projects with dense import graphs. Subsequent edits still
|
|
633
|
+
* extend the graph via `refreshReverseGraphEdges` in `_doBuild`.
|
|
634
|
+
*/
|
|
635
|
+
const seedReverseGraph = async (): Promise<void> => {
|
|
636
|
+
const roots = new Set<string>();
|
|
637
|
+
for (const rootPath of serverModuleSet) roots.add(rootPath);
|
|
638
|
+
for (const rootPath of apiModuleSet) roots.add(rootPath);
|
|
639
|
+
for (const rootPath of clientModuleToRoute.keys()) roots.add(rootPath);
|
|
640
|
+
|
|
641
|
+
const visited = new Set<string>();
|
|
642
|
+
let frontier = Array.from(roots);
|
|
643
|
+
|
|
644
|
+
for (
|
|
645
|
+
let depth = 0;
|
|
646
|
+
depth < DEFAULT_MAX_CLOSURE_DEPTH && frontier.length > 0;
|
|
647
|
+
depth++
|
|
648
|
+
) {
|
|
649
|
+
// Scan the current frontier in parallel — these are independent
|
|
650
|
+
// file reads. The scan returns the RESOLVED absolute importee
|
|
651
|
+
// paths, which become the next frontier.
|
|
652
|
+
const scans = await Promise.all(
|
|
653
|
+
frontier.map(async (filePath) => {
|
|
654
|
+
if (visited.has(filePath)) return [];
|
|
655
|
+
visited.add(filePath);
|
|
656
|
+
try {
|
|
657
|
+
const imports = await scanFileImports(filePath);
|
|
658
|
+
reverseGraph.update(filePath, imports);
|
|
659
|
+
return imports;
|
|
660
|
+
} catch {
|
|
661
|
+
return [];
|
|
662
|
+
}
|
|
663
|
+
}),
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
const next: string[] = [];
|
|
667
|
+
for (const group of scans) {
|
|
668
|
+
for (const dep of group) {
|
|
669
|
+
if (!visited.has(dep)) next.push(dep);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
frontier = next;
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
|
|
573
676
|
// 파일 감시 설정
|
|
574
677
|
const watchers: fs.FSWatcher[] = [];
|
|
575
678
|
|
|
@@ -584,8 +687,19 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
|
|
|
584
687
|
* Lifecycle: timers are created by `scheduleFileChange`, cleared on flush or
|
|
585
688
|
* on `close()`. We call `.delete(key)` on flush to keep the Map bounded —
|
|
586
689
|
* no leak from editing a single file repeatedly.
|
|
690
|
+
*
|
|
691
|
+
* Phase 17 — upgraded to `LRUCache` with an `onEvict` hook so
|
|
692
|
+
* (a) a pathological watcher (millions of distinct files) cannot leak
|
|
693
|
+
* (b) evicted entries still get `clearTimeout` called (no dangling refs)
|
|
694
|
+
* (c) the size is registered with `/_mandu/metrics`
|
|
695
|
+
* Max 2000 entries is generous for even large monorepos (each entry is a
|
|
696
|
+
* single in-flight debounce token; flush cycle is 100 ms).
|
|
587
697
|
*/
|
|
588
|
-
const perFileTimers = new
|
|
698
|
+
const perFileTimers = new LRUCache<string, ReturnType<typeof setTimeout>>({
|
|
699
|
+
maxSize: 2000,
|
|
700
|
+
onEvict: (_key, timer) => clearTimeout(timer),
|
|
701
|
+
});
|
|
702
|
+
registerCacheSize("perFileTimers", () => perFileTimers.size);
|
|
589
703
|
|
|
590
704
|
/**
|
|
591
705
|
* B2 fix — multi-file pending build queue (Phase 7.0 R1 Agent A).
|
|
@@ -974,9 +1088,157 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
|
|
|
974
1088
|
perFileTimers.set(key, timer);
|
|
975
1089
|
};
|
|
976
1090
|
|
|
1091
|
+
/**
|
|
1092
|
+
* #189 — dispatch an unknown-file change against every known root
|
|
1093
|
+
* that transitively imports it.
|
|
1094
|
+
*
|
|
1095
|
+
* The reverse graph answers "which SSR / API / client modules end
|
|
1096
|
+
* up depending on this file?" via a bounded BFS closure. Each
|
|
1097
|
+
* matched root is re-dispatched through the SAME callbacks a
|
|
1098
|
+
* direct change would fire (`onSSRChange`, `onAPIChange`, or an
|
|
1099
|
+
* island rebuild), so downstream behavior is identical to a
|
|
1100
|
+
* direct edit. This closes the "transitive ESM cache" gap
|
|
1101
|
+
* described in issue #189 by making sure every ancestor that
|
|
1102
|
+
* consumed the leaf gets its bundle rebuilt / handler re-
|
|
1103
|
+
* registered.
|
|
1104
|
+
*
|
|
1105
|
+
* Returns the number of roots actually dispatched so the caller
|
|
1106
|
+
* can decide whether to log a "no transitive importers" line or
|
|
1107
|
+
* stay silent. Dispatch is idempotent — each root is touched at
|
|
1108
|
+
* most once per change via the `dispatched` set.
|
|
1109
|
+
*/
|
|
1110
|
+
const dispatchByTransitiveImporters = async (
|
|
1111
|
+
changedFile: string,
|
|
1112
|
+
): Promise<number> => {
|
|
1113
|
+
const absChanged = path.resolve(rootDir, changedFile);
|
|
1114
|
+
// Refresh the changed file's OWN imports first. A leaf edit can
|
|
1115
|
+
// legitimately add or remove imports (e.g. switching a barrel
|
|
1116
|
+
// from `./en` to `./ko`); the reverse graph must track that so
|
|
1117
|
+
// the NEXT unknown-file change uses the correct set.
|
|
1118
|
+
await refreshReverseGraphEdges(absChanged);
|
|
1119
|
+
|
|
1120
|
+
const importers = reverseGraph.transitiveImporters(
|
|
1121
|
+
absChanged,
|
|
1122
|
+
DEFAULT_MAX_CLOSURE_DEPTH,
|
|
1123
|
+
);
|
|
1124
|
+
if (importers.size === 0) return 0;
|
|
1125
|
+
|
|
1126
|
+
// Partition importers by which known-root bucket they belong
|
|
1127
|
+
// to. A single file can be in multiple buckets (e.g. a shared
|
|
1128
|
+
// `.client.tsx` that is also referenced by a server module) —
|
|
1129
|
+
// iterate in priority order: SSR first (heaviest signal), then
|
|
1130
|
+
// API, then client islands.
|
|
1131
|
+
const ssrRoots: string[] = [];
|
|
1132
|
+
const apiRoots: string[] = [];
|
|
1133
|
+
const islandRoots: Array<{ routeId: string; path: string }> = [];
|
|
1134
|
+
|
|
1135
|
+
for (const importerAbs of importers) {
|
|
1136
|
+
if (serverModuleSet.has(importerAbs)) {
|
|
1137
|
+
ssrRoots.push(importerAbs);
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
if (apiModuleSet.has(importerAbs)) {
|
|
1141
|
+
apiRoots.push(importerAbs);
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
const islandRouteId = clientModuleToRoute.get(importerAbs);
|
|
1145
|
+
if (islandRouteId) {
|
|
1146
|
+
islandRoots.push({ routeId: islandRouteId, path: importerAbs });
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const totalMatched = ssrRoots.length + apiRoots.length + islandRoots.length;
|
|
1151
|
+
if (totalMatched === 0) return 0;
|
|
1152
|
+
|
|
1153
|
+
const relFile = path.relative(rootDir, absChanged).replace(/\\/g, "/");
|
|
1154
|
+
console.log(
|
|
1155
|
+
`\n🔄 ${path.basename(changedFile)} changed — invalidating ${totalMatched} transitive importer(s) (${relFile})`,
|
|
1156
|
+
);
|
|
1157
|
+
|
|
1158
|
+
// SSR: fire once per distinct importer path.
|
|
1159
|
+
if (onSSRChange) {
|
|
1160
|
+
for (const ssrRoot of ssrRoots) {
|
|
1161
|
+
try {
|
|
1162
|
+
await Promise.resolve(onSSRChange(ssrRoot));
|
|
1163
|
+
} catch (err) {
|
|
1164
|
+
console.error(
|
|
1165
|
+
"[Mandu HMR] transitive onSSRChange threw:",
|
|
1166
|
+
err instanceof Error ? err.message : String(err),
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// API: same pattern.
|
|
1173
|
+
if (onAPIChange) {
|
|
1174
|
+
for (const apiRoot of apiRoots) {
|
|
1175
|
+
try {
|
|
1176
|
+
await Promise.resolve(onAPIChange(apiRoot));
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
console.error(
|
|
1179
|
+
"[Mandu HMR] transitive onAPIChange threw:",
|
|
1180
|
+
err instanceof Error ? err.message : String(err),
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Islands: coalesce to a single build call when multiple routes
|
|
1187
|
+
// share the changed file. `buildClientBundles` with a
|
|
1188
|
+
// `targetRouteIds` list already dedupes internally.
|
|
1189
|
+
if (islandRoots.length > 0) {
|
|
1190
|
+
const targetIds = Array.from(new Set(islandRoots.map((r) => r.routeId)));
|
|
1191
|
+
const startTime = performance.now();
|
|
1192
|
+
try {
|
|
1193
|
+
const result = await buildClientBundles(manifest, rootDir, {
|
|
1194
|
+
minify: false,
|
|
1195
|
+
sourcemap: true,
|
|
1196
|
+
targetRouteIds: targetIds,
|
|
1197
|
+
});
|
|
1198
|
+
const buildTime = performance.now() - startTime;
|
|
1199
|
+
if (result.success) {
|
|
1200
|
+
console.log(
|
|
1201
|
+
`✅ Rebuilt ${targetIds.length} island(s) in ${buildTime.toFixed(0)}ms`,
|
|
1202
|
+
);
|
|
1203
|
+
for (const targetId of targetIds) {
|
|
1204
|
+
onRebuild?.({
|
|
1205
|
+
routeId: targetId,
|
|
1206
|
+
success: true,
|
|
1207
|
+
buildTime,
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
} else {
|
|
1211
|
+
console.error("❌ Transitive island rebuild failed:", result.errors);
|
|
1212
|
+
for (const targetId of targetIds) {
|
|
1213
|
+
onRebuild?.({
|
|
1214
|
+
routeId: targetId,
|
|
1215
|
+
success: false,
|
|
1216
|
+
buildTime,
|
|
1217
|
+
error: result.errors.join(", "),
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1223
|
+
console.error("❌ Transitive island rebuild error:", err.message);
|
|
1224
|
+
for (const { routeId: targetId } of islandRoots) {
|
|
1225
|
+
onError?.(err, targetId);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
return totalMatched;
|
|
1231
|
+
};
|
|
1232
|
+
|
|
977
1233
|
const _doBuild = async (changedFile: string) => {
|
|
978
1234
|
const normalizedPath = normalizeFsPath(changedFile);
|
|
979
1235
|
|
|
1236
|
+
// #189 — refresh the changed file's imports so the reverse graph
|
|
1237
|
+
// tracks refactors regardless of which dispatch path fires below.
|
|
1238
|
+
// Fire-and-forget because the scan is I/O-bound and the dispatch
|
|
1239
|
+
// path cannot stall on it.
|
|
1240
|
+
void refreshReverseGraphEdges(path.resolve(rootDir, changedFile));
|
|
1241
|
+
|
|
980
1242
|
// 공통 컴포넌트 디렉토리 변경 → Island만 재빌드 + SSR 레지스트리 invalidate (#184, #185)
|
|
981
1243
|
if (isInCommonDir(changedFile)) {
|
|
982
1244
|
console.log(`\n🔄 Common file changed: ${path.basename(changedFile)}`);
|
|
@@ -1053,12 +1315,16 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
|
|
|
1053
1315
|
// SSR 모듈 변경 감지 (page.tsx, layout.tsx) — #151
|
|
1054
1316
|
if (onSSRChange && serverModuleSet.has(normalizedPath)) {
|
|
1055
1317
|
console.log(`\n🔄 SSR file changed: ${path.basename(changedFile)}`);
|
|
1318
|
+
// Refresh the changed file's imports so the reverse graph
|
|
1319
|
+
// tracks refactors (a new import added to an SSR module).
|
|
1320
|
+
await refreshReverseGraphEdges(path.resolve(rootDir, changedFile));
|
|
1056
1321
|
onSSRChange(normalizedPath);
|
|
1057
1322
|
return;
|
|
1058
1323
|
}
|
|
1059
1324
|
// API 모듈 변경 감지 (route.ts)
|
|
1060
1325
|
if (onAPIChange && apiModuleSet.has(normalizedPath)) {
|
|
1061
1326
|
console.log(`\n🔄 API route changed: ${path.basename(changedFile)}`);
|
|
1327
|
+
await refreshReverseGraphEdges(path.resolve(rootDir, changedFile));
|
|
1062
1328
|
onAPIChange(normalizedPath);
|
|
1063
1329
|
return;
|
|
1064
1330
|
}
|
|
@@ -1070,7 +1336,23 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
|
|
|
1070
1336
|
// reuse its existing `handleAPIChange` plumbing.
|
|
1071
1337
|
if (onAPIChange && isRouteMiddlewareFile(normalizedPath)) {
|
|
1072
1338
|
console.log(`\n🔄 Middleware changed: ${path.basename(changedFile)}`);
|
|
1339
|
+
await refreshReverseGraphEdges(path.resolve(rootDir, changedFile));
|
|
1073
1340
|
onAPIChange(normalizedPath);
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
// #189 — reverse import-graph fallback.
|
|
1344
|
+
//
|
|
1345
|
+
// The changed file matched none of our direct dispatch sets.
|
|
1346
|
+
// Before silently dropping (legacy behavior), walk the
|
|
1347
|
+
// reverse graph to see which known roots transitively import
|
|
1348
|
+
// this file and re-dispatch against each one. This is what
|
|
1349
|
+
// makes a deep leaf edit (e.g. a barrel's static-map entry)
|
|
1350
|
+
// propagate without a manual restart.
|
|
1351
|
+
const dispatched = await dispatchByTransitiveImporters(changedFile);
|
|
1352
|
+
if (dispatched === 0) {
|
|
1353
|
+
// No known importer — preserve the legacy silent-drop path
|
|
1354
|
+
// so truly unrelated file changes (editor backups, tmp
|
|
1355
|
+
// files that slipped through the filter) stay cheap.
|
|
1074
1356
|
}
|
|
1075
1357
|
return;
|
|
1076
1358
|
}
|
|
@@ -1078,6 +1360,12 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
|
|
|
1078
1360
|
const route = manifest.routes.find((r) => r.id === routeId);
|
|
1079
1361
|
if (!route || !route.clientModule) return;
|
|
1080
1362
|
|
|
1363
|
+
// #189 — refresh the client island's outgoing imports so the
|
|
1364
|
+
// reverse graph tracks newly added / removed imports in the
|
|
1365
|
+
// island file itself. Fire-and-forget so it cannot stall the
|
|
1366
|
+
// rebuild.
|
|
1367
|
+
void refreshReverseGraphEdges(path.resolve(rootDir, changedFile));
|
|
1368
|
+
|
|
1081
1369
|
console.log(`\n🔄 Rebuilding island: ${routeId}`);
|
|
1082
1370
|
const startTime = performance.now();
|
|
1083
1371
|
|
|
@@ -1223,14 +1511,28 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
|
|
|
1223
1511
|
}
|
|
1224
1512
|
}
|
|
1225
1513
|
|
|
1514
|
+
// #189 — seed the reverse import-graph AFTER watchers are wired so
|
|
1515
|
+
// any edit that lands during the scan still triggers `handleFileChange`.
|
|
1516
|
+
// The scan is fire-and-forget; the initial build is already complete
|
|
1517
|
+
// and the first user edit can't race the seed (first event goes
|
|
1518
|
+
// through the 100 ms debounce, by which point the scan has finished
|
|
1519
|
+
// for any realistic project size).
|
|
1520
|
+
seedReverseGraph().catch((err) => {
|
|
1521
|
+
console.warn(
|
|
1522
|
+
"[Mandu HMR] reverse import-graph seed skipped:",
|
|
1523
|
+
err instanceof Error ? err.message : String(err),
|
|
1524
|
+
);
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1226
1527
|
return {
|
|
1227
1528
|
initialBuild,
|
|
1228
1529
|
close: () => {
|
|
1229
1530
|
// B6: clear all per-file timers to release event-loop refs.
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1531
|
+
// Phase 17 — `LRUCache.clear()` fires the registered `onEvict`
|
|
1532
|
+
// (`clearTimeout(timer)`) for every entry before dropping them,
|
|
1533
|
+
// so we no longer need an explicit loop.
|
|
1233
1534
|
perFileTimers.clear();
|
|
1535
|
+
unregisterCacheSize("perFileTimers");
|
|
1234
1536
|
for (const watcher of watchers) {
|
|
1235
1537
|
watcher.close();
|
|
1236
1538
|
}
|