@animus-ui/extract 0.1.25 → 0.1.27

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.
Files changed (87) hide show
  1. package/crates/extract-v2/animus-extract-v2.darwin-arm64.node +0 -0
  2. package/crates/extract-v2/animus-extract-v2.linux-arm64-gnu.node +0 -0
  3. package/crates/extract-v2/animus-extract-v2.linux-x64-gnu.node +0 -0
  4. package/crates/extract-v2/index.d.ts +101 -6
  5. package/dist/index.cjs +48 -5
  6. package/dist/index.mjs +2 -2
  7. package/dist/pipeline/alias-agreement.d.ts +107 -0
  8. package/dist/pipeline/alias-agreement.d.ts.map +1 -0
  9. package/dist/pipeline/analyze-project-args.d.ts +7 -2
  10. package/dist/pipeline/analyze-project-args.d.ts.map +1 -1
  11. package/dist/pipeline/asset-copies.d.ts +20 -0
  12. package/dist/pipeline/asset-copies.d.ts.map +1 -0
  13. package/dist/pipeline/built-in-asset-types.d.ts +9 -0
  14. package/dist/pipeline/built-in-asset-types.d.ts.map +1 -0
  15. package/dist/pipeline/core-options.d.ts +36 -7
  16. package/dist/pipeline/core-options.d.ts.map +1 -1
  17. package/dist/pipeline/discover-packages.d.ts +98 -12
  18. package/dist/pipeline/discover-packages.d.ts.map +1 -1
  19. package/dist/pipeline/engine-adapter.d.ts +41 -3
  20. package/dist/pipeline/engine-adapter.d.ts.map +1 -1
  21. package/dist/pipeline/engine-retirement.d.ts +7 -1
  22. package/dist/pipeline/engine-retirement.d.ts.map +1 -1
  23. package/dist/pipeline/extension-policy.d.ts +95 -0
  24. package/dist/pipeline/extension-policy.d.ts.map +1 -0
  25. package/dist/pipeline/host-assets.d.ts +32 -0
  26. package/dist/pipeline/host-assets.d.ts.map +1 -0
  27. package/dist/pipeline/host-expansions.d.ts +92 -0
  28. package/dist/pipeline/host-expansions.d.ts.map +1 -0
  29. package/dist/pipeline/host-extensions.d.ts +64 -0
  30. package/dist/pipeline/host-extensions.d.ts.map +1 -0
  31. package/dist/pipeline/host-package-resolver.d.ts +40 -0
  32. package/dist/pipeline/host-package-resolver.d.ts.map +1 -0
  33. package/dist/pipeline/index.d.ts +32 -8
  34. package/dist/pipeline/index.d.ts.map +1 -1
  35. package/dist/pipeline/kit-publication.d.ts.map +1 -1
  36. package/dist/pipeline/manifest-diagnostics.d.ts +25 -4
  37. package/dist/pipeline/manifest-diagnostics.d.ts.map +1 -1
  38. package/dist/pipeline/manifest-schema.d.ts +16 -0
  39. package/dist/pipeline/manifest-schema.d.ts.map +1 -1
  40. package/dist/pipeline/output-targets.d.ts +52 -0
  41. package/dist/pipeline/output-targets.d.ts.map +1 -0
  42. package/dist/pipeline/probe-evidence.d.ts +61 -0
  43. package/dist/pipeline/probe-evidence.d.ts.map +1 -0
  44. package/dist/pipeline/registry-packages.d.ts +27 -0
  45. package/dist/pipeline/registry-packages.d.ts.map +1 -0
  46. package/dist/pipeline/run-analysis.d.ts +26 -0
  47. package/dist/pipeline/run-analysis.d.ts.map +1 -1
  48. package/dist/pipeline/runtime-import.d.ts +10 -0
  49. package/dist/pipeline/runtime-import.d.ts.map +1 -0
  50. package/dist/pipeline/served-files.d.ts +16 -0
  51. package/dist/pipeline/served-files.d.ts.map +1 -0
  52. package/dist/pipeline/source-corpus.d.ts +4 -0
  53. package/dist/pipeline/source-corpus.d.ts.map +1 -1
  54. package/dist/pipeline/source-ingestion.d.ts +37 -5
  55. package/dist/pipeline/source-ingestion.d.ts.map +1 -1
  56. package/dist/pipeline/system-config.d.ts +18 -0
  57. package/dist/pipeline/system-config.d.ts.map +1 -1
  58. package/dist/pipeline/system-instances.d.ts +27 -0
  59. package/dist/pipeline/system-instances.d.ts.map +1 -0
  60. package/dist/pipeline/tsconfig-paths.d.ts +4 -0
  61. package/dist/pipeline/tsconfig-paths.d.ts.map +1 -1
  62. package/dist/pipeline/vite-expansions.d.ts +40 -0
  63. package/dist/pipeline/vite-expansions.d.ts.map +1 -0
  64. package/dist/pipeline/webpack-expansions.d.ts +55 -0
  65. package/dist/pipeline/webpack-expansions.d.ts.map +1 -0
  66. package/dist/pipeline/workspace-packages.d.ts +13 -0
  67. package/dist/pipeline/workspace-packages.d.ts.map +1 -0
  68. package/dist/{pipeline-YGWpNr_Z.mjs → pipeline-C0AcBXuQ.mjs} +4244 -2595
  69. package/dist/{pipeline-DOgKR4VZ.cjs → pipeline-CPSPw522.cjs} +4562 -2637
  70. package/dist/session/coalescing-window.d.ts +34 -0
  71. package/dist/session/coalescing-window.d.ts.map +1 -0
  72. package/dist/session/environment-builds.d.ts +26 -0
  73. package/dist/session/environment-builds.d.ts.map +1 -0
  74. package/dist/session/extraction-session.d.ts +338 -38
  75. package/dist/session/extraction-session.d.ts.map +1 -1
  76. package/dist/session/index.d.ts +9 -3
  77. package/dist/session/index.d.ts.map +1 -1
  78. package/dist/session/published-set.d.ts +6 -1
  79. package/dist/session/published-set.d.ts.map +1 -1
  80. package/dist/session/resolve-packages.d.ts +9 -5
  81. package/dist/session/resolve-packages.d.ts.map +1 -1
  82. package/dist/session/singleton.d.ts +216 -54
  83. package/dist/session/singleton.d.ts.map +1 -1
  84. package/dist/session/turbopack-orchestrator.d.ts.map +1 -1
  85. package/dist/session.cjs +1539 -525
  86. package/dist/session.mjs +1505 -504
  87. package/package.json +4 -4
package/dist/session.mjs CHANGED
@@ -1,7 +1,150 @@
1
- import { $ as discoverFiles, At as surfaceManifestDiagnostics, B as unresolvableIncludesMessage, Bt as substituteSheetAssets, C as resolveAssetFile, D as engineModuleParser, Dt as isDeletedSource, E as collectExternalPackageSources, Gt as assembleStylesheet, H as createSourceIdentity, Ht as contentHash, It as findSheetAssetSpecifiers, Jt as __require, Lt as generatedModuleCode, M as firstOwners, Mt as unreadableSourceDiagnostic, N as importedKitDiagnostics, O as excludeCollectedPackages, P as isExcludedPackageRelativePath, Rt as reportSurvivingAssetPlaceholders, S as enforceExternalTokenContracts, T as createSourceCorpus, U as isPathWithinRoot, V as walkPackageSources, Vt as parseInternalWire, W as sharesVolumeRoot, X as isJsonString, Y as isJsonBlock, _ as runProjectAnalysis, b as resolveLightningTargets, ct as resolveMode, f as serializeStaticCss, ft as createV2EngineApi, g as clearEngineCache, gt as projectExternalFileOwners, i as snapshotFilePlans, j as findPackageRoot, k as extractSystemFilePackages, lt as resolveVerbosity, r as hashReplacementPlans, s as buildSystemPropsModule, st as createExcludeMatcher, t as toWatchKeys, u as loadSystemConfig, vt as DEFAULT_EXTENSIONS, y as postProcessCss, z as staleDistIncludesMessage } from "./pipeline-YGWpNr_Z.mjs";
1
+ import { $ as writeAssetCopy, $t as createExcludeMatcher, A as postProcessCss, At as declaredWorkspacePackages, Bn as __require, C as serializeStaticCss, Ct as nativePackageLocator, D as clearEngineCache, Dn as findSheetAssetSpecifiers, Et as staleDistIncludesMessage, Ft as readTsconfigAliasPairs, G as discoveryResolver, Gt as discoverFiles, I as createSourceCorpus, It as tsconfigChainPaths, J as packageInstanceFinding, K as AliasAgreementCache, Ln as assembleStylesheet, Lt as BUILT_IN_EXTENSION_POLICY, M as buildPathAliasesJson, N as enforceExternalTokenContracts, Nn as contentHash, O as runProjectAnalysis, On as generatedModuleCode, Ot as unresolvableIncludesMessage, P as resolveAssetFile, Pn as parseInternalWire, Q as pruneStaleAssets, Sn as surfaceManifestDiagnostics, St as nativeKitSourceRoot, T as stableStringify, Y as packageReadRecord, Z as assertRuntimeImportSuppliesTerminals, _ as buildSystemPropsModule, _t as importedKitDiagnostics, an as unmatchedExcludeLines, b as loadSystemConfig, cn as sharesVolumeRoot, ct as adaptedOriginals, d as toWatchKeys, dn as isEngineTransformExtension, f as diffFilePlans, ft as excludeCollectedPackages, gn as SYSTEM_INSTANCE_MISMATCH, gt as firstOwners, ht as findPackageRoot, in as resolveVerbosity, j as resolveLightningTargets, jn as substituteSheetAssets, kn as reportSurvivingAssetPlaceholders, kt as walkPackageSources, ln as DEFAULT_EXTENSIONS, lt as bareSpecifierPackageName, m as snapshotFilePlans, nn as outputSourceMessage, nt as createV2EngineApi, on as createSourceIdentity, ot as projectExternalFileOwners, p as hashReplacementPlans, pt as extractSystemFilePackages, q as compareAliases, rn as resolveMode, s as builtInAssetClassification, sn as isPathWithinRoot, tn as outputSourceGuard, ut as collectExternalPackageSources, vt as isExcludedPackageRelativePath, wn as unreadableSourceDiagnostic, x as systemLoadReads, xn as severityFor, yn as isDeletedSource, yt as isPackageSpecifier, zt as extensionPolicyJson } from "./pipeline-C0AcBXuQ.mjs";
2
2
  import { randomUUID } from "crypto";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "fs";
4
- import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "path";
4
+ import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from "path";
5
+ //#region session/coalescing-window.ts
6
+ /**
7
+ * The one coalescing window (SA19). It opens on the first pending event and
8
+ * fires a fixed interval later: a later event joins the open window and never
9
+ * pushes it back, so a steady stream of edits cannot starve the work it
10
+ * coalesces. Builds, startup and explicit flushes bypass it through `flush`.
11
+ * Hosts add no timers of their own.
12
+ */
13
+ const COALESCING_WINDOW_MS = 75;
14
+ const SYSTEM_CLOCK = {
15
+ schedule: (fn, ms) => {
16
+ const timer = setTimeout(fn, ms);
17
+ timer.unref?.();
18
+ return timer;
19
+ },
20
+ cancel: (timer) => clearTimeout(timer)
21
+ };
22
+ var CoalescingWindow = class {
23
+ fire;
24
+ windowMs;
25
+ clock;
26
+ timer = null;
27
+ constructor(fire, windowMs = 75, clock = SYSTEM_CLOCK) {
28
+ this.fire = fire;
29
+ this.windowMs = windowMs;
30
+ this.clock = clock;
31
+ }
32
+ /** A pending event: opens the window, or joins the open one. */
33
+ note() {
34
+ if (this.timer !== null) return;
35
+ this.timer = this.clock.schedule(() => {
36
+ this.timer = null;
37
+ return this.fire();
38
+ }, this.windowMs);
39
+ }
40
+ /** Fires now, closing any open window: the bypass. */
41
+ flush() {
42
+ this.cancel();
43
+ return this.fire();
44
+ }
45
+ /** Closes an open window without firing it. */
46
+ cancel() {
47
+ if (this.timer === null) return;
48
+ this.clock.cancel(this.timer);
49
+ this.timer = null;
50
+ }
51
+ };
52
+ //#endregion
53
+ //#region session/environment-builds.ts
54
+ /**
55
+ * Each environment build that started and has not ended, by name. A host
56
+ * that builds a shared plugin's environments in parallel (a Vite `buildApp`)
57
+ * shares one build among them, retired by the last to end. A build under a
58
+ * name whose build is open (a reused builder, or a second environment under
59
+ * one name) waits for that build's end, so each build ends only its own
60
+ * generation, once.
61
+ */
62
+ var EnvironmentBuilds = class {
63
+ /** What a host without environments builds as. */
64
+ single = { name: "build" };
65
+ builds = /* @__PURE__ */ new Map();
66
+ /** Opens `environment`'s build once no build under its name is open;
67
+ * whether another environment's build is open beside it. */
68
+ async open(environment) {
69
+ for (;;) {
70
+ const open = this.builds.get(environment.name);
71
+ if (open === void 0) break;
72
+ await open.ended;
73
+ }
74
+ let end = () => {};
75
+ const ended = new Promise((settle) => {
76
+ end = settle;
77
+ });
78
+ this.builds.set(environment.name, {
79
+ environment,
80
+ ended,
81
+ end
82
+ });
83
+ return this.builds.size > 1;
84
+ }
85
+ /** Ends `environment`'s build if this environment opened it; whether that
86
+ * left none open. A host can end an environment it never opened (a dev
87
+ * server ends every environment, started or not), which ends nothing. */
88
+ end(environment) {
89
+ const open = this.builds.get(environment.name);
90
+ if (open === void 0 || open.environment !== environment) return false;
91
+ this.builds.delete(environment.name);
92
+ open.end();
93
+ return this.builds.size === 0;
94
+ }
95
+ };
96
+ //#endregion
97
+ //#region pipeline/system-instances.ts
98
+ /**
99
+ * The system and the analysis read one instance of each kit (spec §2.2
100
+ * A4). Discovery asks the host which instance a kit specifier names; the
101
+ * system loader resolves the system file's imports itself. Until the loader
102
+ * takes the host's instance, a kit whose analysed instance differs from the
103
+ * one the system evaluated is refused: its components would be extracted
104
+ * against another instance's vocabulary.
105
+ */
106
+ function realPath(path) {
107
+ try {
108
+ return realpathSync(path);
109
+ } catch {
110
+ return path;
111
+ }
112
+ }
113
+ /**
114
+ * One error for each system-file kit specifier whose analysed package
115
+ * instance is not the one the system load evaluated: the file the loader's
116
+ * own resolution names must be among the files the load read, and lie in
117
+ * another package root than the instance discovery read for its package.
118
+ */
119
+ function systemInstanceMismatches(input) {
120
+ const evaluated = new Set(input.evaluated.map(realPath));
121
+ const shown = (path) => relative(input.rootDir, path).split(sep).join("/");
122
+ const diagnostics = [];
123
+ for (const specifier of input.specifiers) {
124
+ if (!isPackageSpecifier(specifier)) continue;
125
+ const analysed = input.instances[bareSpecifierPackageName(specifier)];
126
+ if (analysed === void 0) continue;
127
+ let file;
128
+ try {
129
+ file = input.loaderResolve(specifier);
130
+ } catch {
131
+ continue;
132
+ }
133
+ if (file === null || !evaluated.has(realPath(file))) continue;
134
+ const system = realPath(findPackageRoot(realPath(file)));
135
+ if (system === analysed) continue;
136
+ diagnostics.push({
137
+ file: specifier,
138
+ component: "kit",
139
+ kind: "error",
140
+ message: `the system evaluates '${specifier}' from ${shown(system)}, but the host bundler resolves it to ${shown(analysed)}, which extraction analyses, so its components would be extracted against another instance's vocabulary — make the bundler and the system resolve '${specifier}' to one instance`,
141
+ code: SYSTEM_INSTANCE_MISMATCH,
142
+ severity: severityFor(SYSTEM_INSTANCE_MISMATCH)
143
+ });
144
+ }
145
+ return diagnostics;
146
+ }
147
+ //#endregion
5
148
  //#region session/session-paths.ts
6
149
  /**
7
150
  * Session artifact names, path derivations, and artifact shapes. Imports no
@@ -168,9 +311,11 @@ function readCliLockRecord(dir) {
168
311
  if (!isPublishedSetRecord(candidate) || !isPublishedSetNumber(candidate.pid)) return { kind: "indeterminate" };
169
312
  const startedAt = candidate.startedAt;
170
313
  const heartbeatAt = candidate.heartbeatAt;
314
+ const build = candidate.build;
171
315
  const record = { pid: candidate.pid };
172
316
  if (isPublishedSetString(startedAt)) record.startedAt = startedAt;
173
317
  if (isPublishedSetString(heartbeatAt)) record.heartbeatAt = heartbeatAt;
318
+ if (isPublishedSetString(build)) record.build = build;
174
319
  return {
175
320
  kind: "held",
176
321
  record
@@ -201,12 +346,14 @@ function checkLockLiveness(record) {
201
346
  reason: `holder pid ${record.pid} is running but its lock heartbeat (${record.heartbeatAt}) is ${Math.round(ageMs / 1e3)}s old, past the ${CLI_LOCK_STALE_AFTER_MS / 1e3}s staleness window — the pid names a different process now`
202
347
  };
203
348
  }
204
- function lockRecordBytes(startedAt) {
205
- return JSON.stringify({
349
+ function lockRecordBytes(startedAt, build) {
350
+ const record = {
206
351
  pid: process.pid,
207
352
  startedAt,
208
353
  heartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
209
- });
354
+ };
355
+ if (build !== void 0) record.build = build;
356
+ return JSON.stringify(record);
210
357
  }
211
358
  const HEARTBEAT_TEMP_PREFIX = ".lock-heartbeat-";
212
359
  function holdDirectoryClaim(dir, options = {}) {
@@ -224,9 +371,9 @@ function holdDirectoryClaim(dir, options = {}) {
224
371
  const stamp = () => {
225
372
  try {
226
373
  if (rename) {
227
- writeFileSync(tempPath, lockRecordBytes(startedAt));
374
+ writeFileSync(tempPath, lockRecordBytes(startedAt, options.build));
228
375
  renameSync(tempPath, path);
229
- } else writeFileSync(path, lockRecordBytes(startedAt));
376
+ } else writeFileSync(path, lockRecordBytes(startedAt, options.build));
230
377
  } catch (error) {
231
378
  stop();
232
379
  if (isMissingFile(error)) return;
@@ -256,320 +403,395 @@ function collectSessionAssets(sessionDir) {
256
403
  }));
257
404
  }
258
405
  //#endregion
259
- //#region session/resolve-packages.ts
260
- /** An exports target under the `import` condition, then `default`, as the
261
- * system loader reads it; an array offers its targets in order. */
262
- function importTarget(value) {
263
- if (isJsonString(value)) return value;
264
- if (Array.isArray(value)) {
265
- for (const entry of value) {
266
- const target = importTarget(entry);
267
- if (target !== null) return target;
268
- }
269
- return null;
270
- }
271
- if (!isJsonBlock(value)) return null;
272
- return importTarget(value.import) ?? importTarget(value.default);
273
- }
274
- /** The exports entry for `subpath`: an exact key answers alone; otherwise
275
- * the pattern with the longest prefix, then the longest suffix, wins. */
276
- function exportsTarget(exports, subpath) {
277
- const map = isJsonBlock(exports) && Object.keys(exports).some((k) => k.startsWith(".")) ? exports : { ".": exports };
278
- if (subpath in map) return importTarget(map[subpath]);
279
- let best = null;
280
- for (const [pattern, value] of Object.entries(map)) {
281
- const star = pattern.indexOf("*");
282
- if (star === -1 || pattern.indexOf("*", star + 1) !== -1) continue;
283
- const prefix = pattern.slice(0, star);
284
- const suffix = pattern.slice(star + 1);
285
- if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix) || subpath.length < prefix.length + suffix.length) continue;
286
- if (!best || prefix.length > best.prefix.length || prefix.length === best.prefix.length && suffix.length > best.suffix.length) best = {
287
- prefix,
288
- suffix,
289
- value
290
- };
291
- }
292
- if (!best) return null;
293
- const matched = subpath.slice(best.prefix.length, subpath.length - best.suffix.length);
294
- return importTarget(best.value)?.replaceAll("*", matched) ?? null;
295
- }
296
- /** The entry an ES import of `name` resolves to through the package's
297
- * `exports`, under the `import` condition, then `default`: what the Vite
298
- * host and the system loader resolve. Null without an `exports` field, or
299
- * when no target exists. */
300
- function resolveImportEntry(rootDir, name) {
301
- const packageName = name.split("/").slice(0, name.startsWith("@") ? 2 : 1).join("/");
302
- const subpath = `.${name.slice(packageName.length)}`;
303
- for (let dir = rootDir;; dir = dirname(dir)) {
304
- const manifest = join(dir, "node_modules", packageName, "package.json");
305
- if (existsSync(manifest)) {
306
- let exports;
307
- try {
308
- const parsed = parseInternalWire(readFileSync(manifest, "utf-8"), `${manifest} (a package manifest)`);
309
- exports = isJsonBlock(parsed) ? parsed.exports : void 0;
310
- } catch {
311
- return null;
312
- }
313
- if (exports === void 0 || exports === null) return null;
314
- const target = exportsTarget(exports, subpath);
315
- if (target === null) return null;
316
- const entryPath = resolve(dirname(manifest), target);
317
- return existsSync(entryPath) ? realpathSync(entryPath) : null;
318
- }
319
- if (dir === dirname(dir)) return null;
320
- }
321
- }
322
- function resolvePackagesByName(rootDir, names) {
323
- if (names.length === 0) return {};
324
- const nameSet = new Set(names);
325
- const resolved = /* @__PURE__ */ new Set();
326
- const packageMap = {};
327
- try {
328
- const rootPkg = JSON.parse(readFileSync(join(rootDir, "package.json"), "utf-8"));
329
- const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages ?? [];
330
- for (const ws of workspaces) {
331
- const wsDir = resolve(rootDir, ws);
332
- if (!existsSync(wsDir)) continue;
333
- try {
334
- const pkg = JSON.parse(readFileSync(join(wsDir, "package.json"), "utf-8"));
335
- const name = pkg.name || "";
336
- if (nameSet.has(name)) {
337
- const main = pkg.main || pkg.module || "index.ts";
338
- const entryPath = resolve(wsDir, main);
339
- if (existsSync(entryPath)) {
340
- packageMap[name] = relative(rootDir, entryPath);
341
- resolved.add(name);
342
- }
343
- }
344
- } catch {}
345
- }
346
- } catch {}
347
- for (const name of nameSet) {
348
- if (resolved.has(name)) continue;
349
- const importEntry = resolveImportEntry(rootDir, name);
350
- if (importEntry !== null) {
351
- packageMap[name] = relative(rootDir, importEntry);
352
- continue;
353
- }
354
- try {
355
- const entryPath = __require.resolve(name, { paths: [rootDir] });
356
- packageMap[name] = relative(rootDir, entryPath);
357
- } catch {}
358
- }
359
- return packageMap;
360
- }
361
- //#endregion
362
406
  //#region session/singleton.ts
363
407
  /**
364
- * globalThis-backed so the ESM plugin copy and the CJS webpack-loader copy
365
- * share one store; module scope alone gives each copy its own instance.
408
+ * A build's shared state lives in one registry on `globalThis`, keyed by its
409
+ * `SessionKey`, so the ESM plugin copy and the CJS webpack-loader copy of
410
+ * this module find the same entry, and two builds of one root never share
411
+ * one. A reader of a build no participant has joined gets the empty value;
412
+ * a writer must have joined it.
366
413
  */
367
- const MANIFEST_KEY = "__animus_manifest_json__";
368
- const ANALYSIS_STARTED_KEY = "__animus_analysis_started__";
369
- const SHARED_CSS_KEY = "__animus_shared_css__";
370
- const SHARED_SYSTEM_PROPS_KEY = "__animus_shared_system_props__";
371
- const SHARED_EXTERNAL_DIRS_KEY = "__animus_external_pkg_dirs__";
372
- const SHARED_EXTERNAL_ENTRIES_KEY = "__animus_external_source_entries__";
373
- const SHARED_EXTERNAL_SIDE_EFFECTS_KEY = "__animus_external_side_effects__";
374
- const ANALYZED_HASHES_KEY = "__animus_analyzed_hashes__";
375
- const REPLACEMENT_EPOCH_KEY = "__animus_replacement_epoch__";
376
- const WATCH_TRANSACTION_KEY = "__animus_watch_transaction__";
377
- const PROCESS_SESSION_ID_KEY = "__animus_process_session_id__";
378
- const SESSION_ARTIFACT_DIR_KEY = "__animus_session_artifact_dir__";
379
- const OWNING_WATCH_SESSION_KEY = "__animus_owning_watch_session__";
380
- const EXCLUSIVE_SESSION_OWNER_KEY = "__animus_exclusive_session_owner__";
381
- const ENGINE_KEY = "__animus_engine__";
382
- const V2_ENGINE_KEY = "__animus_v2_engine__";
383
- const V2_SENT_SOURCES_KEY = "__animus_v2_sent_sources__";
384
- const V2_DRIFT_WARNED_KEY = "__animus_v2_drift_warned__";
385
- const ENGINE_API_OVERRIDE_KEY = "__animus_engine_api_override__";
386
- const singletonStore = globalThis;
387
- const isSingletonString = (value) => {
388
- if (Object(value) === value) return false;
389
- try {
390
- String.prototype.valueOf.call(value);
391
- return true;
392
- } catch {
393
- return false;
394
- }
395
- };
396
- function getManifestJson() {
397
- return singletonStore[MANIFEST_KEY];
414
+ function joined(key) {
415
+ const state = findSession(key);
416
+ if (state === void 0) throw new Error(`[animus] no participant has joined build ${key.build} of ${key.root}`);
417
+ return state;
418
+ }
419
+ /** Commits the build's publication, the one write of everything below. */
420
+ function setPublication(key, publication) {
421
+ joined(key).publication = publication;
422
+ }
423
+ function publicationOf(key) {
424
+ return findSession(key)?.publication ?? null;
398
425
  }
399
- function setManifestJson(json) {
400
- singletonStore[MANIFEST_KEY] = json;
426
+ function getManifestJson(key) {
427
+ return publicationOf(key)?.manifestJson ?? null;
401
428
  }
402
429
  /**
403
430
  * A latch over the most recently STARTED analysis, never cleared on settle:
404
431
  * non-null means one was scheduled. In-flight lives in getWatchTransaction.
405
432
  */
406
- function getAnalysisStartedPromise() {
407
- return singletonStore[ANALYSIS_STARTED_KEY];
408
- }
409
- function setAnalysisStartedPromise(promise) {
410
- singletonStore[ANALYSIS_STARTED_KEY] = promise;
411
- }
412
- function resetAnalysisStartedPromise() {
413
- singletonStore[ANALYSIS_STARTED_KEY] = null;
433
+ function getAnalysisStartedPromise(key) {
434
+ return findSession(key)?.analysisStarted ?? null;
414
435
  }
415
- function getSharedCss() {
416
- return singletonStore[SHARED_CSS_KEY] || "";
436
+ function setAnalysisStartedPromise(key, promise) {
437
+ joined(key).analysisStarted = promise;
417
438
  }
418
- function setSharedCss(css) {
419
- singletonStore[SHARED_CSS_KEY] = css;
439
+ function resetAnalysisStartedPromise(key) {
440
+ const state = findSession(key);
441
+ if (state !== void 0) state.analysisStarted = null;
420
442
  }
421
- function getSharedSystemProps() {
422
- return singletonStore[SHARED_SYSTEM_PROPS_KEY] || "";
443
+ /** True for the build's first caller only: the one that reports its
444
+ * unmatched user excludes. */
445
+ function claimUnmatchedExcludeReport(key) {
446
+ const state = joined(key);
447
+ if (state.unmatchedExcludesReported) return false;
448
+ state.unmatchedExcludesReported = true;
449
+ return true;
423
450
  }
424
- function setSharedSystemProps(content) {
425
- singletonStore[SHARED_SYSTEM_PROPS_KEY] = content;
451
+ function getSharedCss(key) {
452
+ return publicationOf(key)?.styles ?? "";
426
453
  }
427
- function getSharedExternalDirs() {
428
- return singletonStore[SHARED_EXTERNAL_DIRS_KEY] || [];
454
+ function getSharedSystemProps(key) {
455
+ return publicationOf(key)?.systemPropsModule ?? "";
429
456
  }
430
- function setSharedExternalDirs(dirs) {
431
- singletonStore[SHARED_EXTERNAL_DIRS_KEY] = dirs;
457
+ function getSharedExternalDirs(key) {
458
+ return publicationOf(key)?.externalPackageDirs ?? [];
432
459
  }
433
- function getSharedExternalEntries() {
434
- return singletonStore[SHARED_EXTERNAL_ENTRIES_KEY] || /* @__PURE__ */ new Map();
435
- }
436
- function setSharedExternalEntries(entries) {
437
- singletonStore[SHARED_EXTERNAL_ENTRIES_KEY] = entries;
460
+ function getSharedExternalEntries(key) {
461
+ return publicationOf(key)?.externalSourceEntries ?? /* @__PURE__ */ new Map();
438
462
  }
439
463
  /** Specifier → its source entry's side effects, published with the shared
440
464
  * entries; absent leaves the bundler's own classification. */
441
- function getSharedExternalSideEffects() {
442
- return singletonStore[SHARED_EXTERNAL_SIDE_EFFECTS_KEY] || /* @__PURE__ */ new Map();
443
- }
444
- function setSharedExternalSideEffects(sideEffects) {
445
- singletonStore[SHARED_EXTERNAL_SIDE_EFFECTS_KEY] = sideEffects;
465
+ function getSharedExternalSideEffects(key) {
466
+ return publicationOf(key)?.externalSourceSideEffects ?? /* @__PURE__ */ new Map();
446
467
  }
447
- function globalSlot(key) {
448
- return {
449
- get: () => singletonStore[key] ?? null,
450
- set: (value) => {
451
- singletonStore[key] = value;
452
- }
453
- };
454
- }
455
- const analyzedHashesSlot = globalSlot(ANALYZED_HASHES_KEY);
456
- const replacementEpochSlot = globalSlot(REPLACEMENT_EPOCH_KEY);
457
- const watchTransactionSlot = globalSlot(WATCH_TRANSACTION_KEY);
458
- const sessionArtifactDirSlot = globalSlot(SESSION_ARTIFACT_DIR_KEY);
459
- const owningWatchSessionSlot = globalSlot(OWNING_WATCH_SESSION_KEY);
460
468
  /**
461
- * The only session that can run watch analysis. A non-owning session forwards
462
- * its batch here; dropping it loses files only that watcher sees.
469
+ * The only session of a build that can run watch analysis. A non-owning
470
+ * session's observations queue for it; dropping them loses files only that
471
+ * watcher sees.
463
472
  */
464
- function getOwningWatchSession() {
465
- return owningWatchSessionSlot.get();
473
+ function getOwningWatchSession(key) {
474
+ return findSession(key)?.authority ?? null;
466
475
  }
467
- function setOwningWatchSession(session) {
468
- owningWatchSessionSlot.set(session);
476
+ function setOwningWatchSession(key, session) {
477
+ joined(key).authority = session;
469
478
  }
470
479
  /**
471
480
  * Per-file content hashes of the last published analysis (relPath → hash of
472
481
  * the exact bytes analyzed), including files that produced zero entries.
473
482
  */
474
- function getAnalyzedHashes() {
475
- return analyzedHashesSlot.get();
476
- }
477
- function setAnalyzedHashes(hashes) {
478
- analyzedHashesSlot.set(hashes);
483
+ function getAnalyzedHashes(key) {
484
+ return publicationOf(key)?.analyzedHashes ?? null;
479
485
  }
480
- /**
481
- * Session identity is per process: the first caller claims a fresh id and
482
- * every later one adopts it, so all compilers share one artifact tree.
483
- */
484
- function claimProcessSessionId() {
485
- const existing = singletonStore[PROCESS_SESSION_ID_KEY];
486
- if (isSingletonString(existing) && existing.length > 0) return existing;
487
- const fresh = randomUUID();
488
- singletonStore[PROCESS_SESSION_ID_KEY] = fresh;
489
- return fresh;
486
+ /** What the build's last published analysis read of the discovered
487
+ * packages (spec §2.2 A4); null before one publishes. */
488
+ function getSharedPackageReadRecord(key) {
489
+ return publicationOf(key)?.packageReadRecord ?? null;
490
490
  }
491
491
  /**
492
- * Two CONCURRENT drive loops in one process would share one session tree and
493
- * clobber each other; sequential claim/release cycles are legal.
492
+ * One participant publishes a build's analysis, holding this claim until it
493
+ * closes: a second publisher would clobber its artifacts. Two builds never
494
+ * exclude each other; sequential claim and release cycles are legal.
494
495
  */
495
- function claimExclusiveSessionOwner(label) {
496
- const active = singletonStore[EXCLUSIVE_SESSION_OWNER_KEY];
497
- if (isSingletonString(active)) throw new Error(`[animus] a second Animus host ("${label}") started while "${active}" is still active in this process. The extraction session is process-global (one session tree, one manifest), so concurrent hosts would clobber each other's analysis. Run one Animus-enabled config per process, or make the builds sequential.`);
498
- singletonStore[EXCLUSIVE_SESSION_OWNER_KEY] = label;
496
+ function claimExclusiveSessionOwner(key, label) {
497
+ const state = joined(key);
498
+ const active = state.activeAttempt;
499
+ if (active !== null) throw new Error(`[animus] "${label}" started an analysis of build ${key.build} of ${key.root}, which "${active}" publishes. A build's participants share one analysis; run it once and let the others read its results.`);
500
+ state.activeAttempt = label;
499
501
  return () => {
500
- if (singletonStore[EXCLUSIVE_SESSION_OWNER_KEY] === label) delete singletonStore[EXCLUSIVE_SESSION_OWNER_KEY];
502
+ if (state.activeAttempt === label) state.activeAttempt = null;
501
503
  };
502
504
  }
503
505
  /**
504
- * Absolute artifact directory of the OWNING session's last publication. The
506
+ * Absolute artifact directory of the build's last publication. The
505
507
  * Turbopack loader runs out of process and receives it via options instead.
506
508
  */
507
- function getSessionArtifactDir() {
508
- return sessionArtifactDirSlot.get();
509
+ function getSessionArtifactDir(key) {
510
+ return findSession(key)?.artifactDir ?? null;
509
511
  }
510
- function setSessionArtifactDir(dir) {
511
- sessionArtifactDirSlot.set(dir);
512
+ function setSessionArtifactDir(key, dir) {
513
+ joined(key).artifactDir = dir;
512
514
  }
513
- /**
514
- * Replacement epoch of the last published analysis, written AFTER the
515
- * manifest so an epoch reader always sees at least that generation's manifest.
516
- */
517
- function getReplacementEpoch() {
518
- return replacementEpochSlot.get();
519
- }
520
- function setReplacementEpoch(epoch) {
521
- replacementEpochSlot.set(epoch);
515
+ /** Replacement epoch of the last published analysis: committed in the same
516
+ * record as its manifest, so an epoch reader sees that generation's. */
517
+ function getReplacementEpoch(key) {
518
+ return publicationOf(key)?.replacementEpoch ?? null;
522
519
  }
523
520
  /**
524
- * The one in-flight watch-analysis transaction: a compiler that did not start
525
- * it joins this promise rather than read a pre-transaction generation.
521
+ * The build's one in-flight watch-analysis transaction: a participant that
522
+ * did not start it joins this promise rather than read a pre-transaction
523
+ * generation.
526
524
  */
527
- function getWatchTransaction() {
528
- return watchTransactionSlot.get();
525
+ function getWatchTransaction(key) {
526
+ return findSession(key)?.watchTransaction ?? null;
529
527
  }
530
- function setWatchTransaction(transaction) {
531
- watchTransactionSlot.set(transaction);
528
+ function observationQueue(key) {
529
+ return joined(key).observations;
532
530
  }
533
- /** Engine selection travels through the singleton so non-owning compilers
534
- * and the webpack loader honor the owner's choice. */
535
- function setSharedEngine(engine) {
536
- singletonStore[ENGINE_KEY] = engine;
531
+ function getSystemAuthority(key) {
532
+ return findSession(key)?.systemAuthority ?? null;
537
533
  }
538
- function getSharedEngine() {
539
- return singletonStore[ENGINE_KEY] || "v2";
534
+ function setSystemAuthority(key, authority) {
535
+ joined(key).systemAuthority = authority;
536
+ }
537
+ function setWatchTransaction(key, transaction) {
538
+ joined(key).watchTransaction = transaction;
540
539
  }
541
540
  /** Single choke-point for every native extraction call; the NAPI module's
542
541
  * own `.d.ts` is the authoritative surface for what it returns. */
543
542
  function requireEngine() {
544
543
  return __require("@animus-ui/extract");
545
544
  }
545
+ const ENGINE_API_OVERRIDE_KEY = "__animus_engine_api_override__";
546
+ const overrideGlobal = globalThis;
547
+ /** The native engine module could not be loaded: an environment failure,
548
+ * never answered by another engine. */
549
+ var EnvironmentFailure = class extends Error {
550
+ code = "EnvironmentFailure";
551
+ constructor(cause) {
552
+ super(`[animus] EnvironmentFailure: the native extraction engine could not be loaded: ${String(cause)}`, { cause });
553
+ this.name = "EnvironmentFailure";
554
+ }
555
+ };
556
+ let loadEngineFactory = requireEngine;
557
+ /** The native factory, loaded once for every build in the process. A load
558
+ * failure is an EnvironmentFailure, and is not cached: the next use tries
559
+ * the load again. */
560
+ function engineFactory() {
561
+ const registry = sessionRegistry();
562
+ if (registry.factory === null) try {
563
+ registry.factory = loadEngineFactory();
564
+ } catch (error) {
565
+ throw new EnvironmentFailure(error instanceof Error ? error : String(error));
566
+ }
567
+ return registry.factory;
568
+ }
546
569
  /**
547
- * The one engine API for this process: the instance, its analyze-time
548
- * sources, and the drift flag live on globalThis so both copies share them.
570
+ * One engine-handle store (SA18): the engine API over `key`'s own engine
571
+ * state, made once per key in the registry every module copy joins. The
572
+ * native factory is shared; each key's engine instance, analyze-time
573
+ * sources and drift flag are its own, and its build's retirement releases
574
+ * them. A key need not be a joined build's: a Vite context or a Turbopack
575
+ * worker keys its own.
549
576
  */
550
- const v2EngineApi = createV2EngineApi({
551
- label: "animus",
552
- isV2: () => getSharedEngine() === "v2",
553
- loadNativeEngine: requireEngine,
554
- passThroughUnknownPaths: true,
555
- store: {
556
- getEngine: () => singletonStore[V2_ENGINE_KEY],
557
- setEngine: (engine) => {
558
- singletonStore[V2_ENGINE_KEY] = engine;
559
- },
560
- getSentSources: () => singletonStore[V2_SENT_SOURCES_KEY] ?? null,
561
- setSentSources: (sources) => {
562
- singletonStore[V2_SENT_SOURCES_KEY] = sources;
563
- },
564
- getDriftWarned: () => Boolean(singletonStore[V2_DRIFT_WARNED_KEY]),
565
- setDriftWarned: (value) => {
566
- singletonStore[V2_DRIFT_WARNED_KEY] = value;
577
+ function engineHandle(key, options) {
578
+ const engines = sessionRegistry().engines;
579
+ const id = sessionKeyId(key);
580
+ let api = engines.get(id);
581
+ if (api === void 0) {
582
+ let engine = null;
583
+ let sentSources = null;
584
+ let driftWarned = false;
585
+ api = createV2EngineApi({
586
+ ...options,
587
+ isV2: () => true,
588
+ loadNativeEngine: engineFactory,
589
+ store: {
590
+ getEngine: () => engine,
591
+ setEngine: (next) => {
592
+ engine = next;
593
+ },
594
+ getSentSources: () => sentSources,
595
+ setSentSources: (sources) => {
596
+ sentSources = sources;
597
+ },
598
+ getDriftWarned: () => driftWarned,
599
+ setDriftWarned: (value) => {
600
+ driftWarned = value;
601
+ }
602
+ }
603
+ });
604
+ engines.set(id, api);
605
+ }
606
+ return api;
607
+ }
608
+ /** Releases a standalone handle, one no joined build retires: its host
609
+ * calls this when it closes. */
610
+ function releaseEngineHandle(key) {
611
+ sessionRegistry().engines.delete(sessionKeyId(key));
612
+ }
613
+ /**
614
+ * The build's engine API, from the one engine-handle store.
615
+ */
616
+ function engineApi(key) {
617
+ const override = overrideGlobal[ENGINE_API_OVERRIDE_KEY];
618
+ if (override) return override();
619
+ joined(key);
620
+ return engineHandle(key, {
621
+ label: "animus",
622
+ passThroughUnknownPaths: true
623
+ })();
624
+ }
625
+ /** A fresh build id, created once by the owning host wrapper. */
626
+ function createBuildId() {
627
+ return randomUUID();
628
+ }
629
+ /** The key of build `build` of `root`, by the root's canonical path. */
630
+ function sessionKey(build, root) {
631
+ const absolute = resolve(root);
632
+ let canonical = absolute;
633
+ try {
634
+ canonical = realpathSync(absolute);
635
+ } catch {}
636
+ return {
637
+ build,
638
+ root: canonical,
639
+ protocol: 1
640
+ };
641
+ }
642
+ function sessionKeyId(key) {
643
+ return JSON.stringify([
644
+ key.protocol,
645
+ key.build,
646
+ key.root
647
+ ]);
648
+ }
649
+ /** A participant joined a session with semantic options that differ from
650
+ * those the session was created with. */
651
+ var SessionConfigurationConflict = class extends Error {
652
+ key;
653
+ established;
654
+ joining;
655
+ differing;
656
+ code = "SessionConfigurationConflict";
657
+ constructor(key, established, joining, differing, requirement = "Every participant of one build must use the same Animus options.") {
658
+ super(`[animus] SessionConfigurationConflict: "${joining}" joined build ${key.build} of ${key.root} with ${differing.join(", ")} differing from "${established}". ${requirement}`);
659
+ this.key = key;
660
+ this.established = established;
661
+ this.joining = joining;
662
+ this.differing = differing;
663
+ this.name = "SessionConfigurationConflict";
664
+ }
665
+ };
666
+ function digestOf(field) {
667
+ return {
668
+ field,
669
+ text: (options) => {
670
+ const value = options[field];
671
+ return value === void 0 ? void 0 : stableStringify(value);
567
672
  }
673
+ };
674
+ }
675
+ /**
676
+ * Every option, classified: one a build's analysis depends on, which every
677
+ * participant must agree on, or one deliberately excluded. `verbose` changes
678
+ * no output, and the root is the key's. The type checker rejects an option
679
+ * added to `AnimusCoreOptions` until it is classified here.
680
+ */
681
+ const OPTION_CLASSES = {
682
+ system: digestOf("system"),
683
+ exclude: digestOf("exclude"),
684
+ extensions: digestOf("extensions"),
685
+ strict: digestOf("strict"),
686
+ diagnostics: digestOf("diagnostics"),
687
+ verbose: "excluded",
688
+ prefix: digestOf("prefix"),
689
+ prefixContextualVars: digestOf("prefixContextualVars"),
690
+ targets: digestOf("targets"),
691
+ minify: digestOf("minify"),
692
+ staticCss: digestOf("staticCss"),
693
+ layers: digestOf("layers"),
694
+ engine: digestOf("engine"),
695
+ mode: digestOf("mode"),
696
+ runtimeImport: digestOf("runtimeImport"),
697
+ root: "excluded"
698
+ };
699
+ /** The options a build's analysis depends on, each as stable text; each is
700
+ * data by its type, so equal text is an equal value. A host's own options
701
+ * are not the build's. */
702
+ function semanticOptions(options) {
703
+ const present = [];
704
+ for (const [name, digest] of Object.entries(OPTION_CLASSES)) {
705
+ if (digest === "excluded") continue;
706
+ const text = digest.text(options);
707
+ if (text !== void 0) present.push([name, text]);
708
+ }
709
+ return new Map(present);
710
+ }
711
+ const SESSION_REGISTRY = Symbol.for("animus.session-registry.v1");
712
+ const registryGlobal = globalThis;
713
+ function sessionRegistry() {
714
+ return registryGlobal[SESSION_REGISTRY] ??= {
715
+ sessions: /* @__PURE__ */ new Map(),
716
+ engines: /* @__PURE__ */ new Map(),
717
+ factory: null
718
+ };
719
+ }
720
+ /**
721
+ * Joins `participant` to the session of `key`, creating it with `options` on
722
+ * the first join. A later participant must bring the same semantic options.
723
+ */
724
+ function joinSession(key, participant, options) {
725
+ const sessions = sessionRegistry().sessions;
726
+ const id = sessionKeyId(key);
727
+ const joining = semanticOptions(options);
728
+ const existing = sessions.get(id);
729
+ if (existing === void 0) {
730
+ const state = {
731
+ key,
732
+ established: participant,
733
+ options: joining,
734
+ participants: /* @__PURE__ */ new Set([participant]),
735
+ activeAttempt: null,
736
+ authority: null,
737
+ analysisStarted: null,
738
+ watchTransaction: null,
739
+ observations: {
740
+ revision: 0,
741
+ rescan: false,
742
+ untaken: /* @__PURE__ */ new Map(),
743
+ suppliedSources: /* @__PURE__ */ new Map(),
744
+ revisions: /* @__PURE__ */ new Map(),
745
+ active: null
746
+ },
747
+ systemAuthority: null,
748
+ claimRecord: null,
749
+ publication: null,
750
+ artifactDir: null,
751
+ unmatchedExcludesReported: false
752
+ };
753
+ sessions.set(id, state);
754
+ return state;
755
+ }
756
+ const differing = [.../* @__PURE__ */ new Set([...existing.options.keys(), ...joining.keys()])].filter((name) => existing.options.get(name) !== joining.get(name));
757
+ if (differing.length > 0) throw new SessionConfigurationConflict(key, existing.established, participant, differing.sort());
758
+ existing.participants.add(participant);
759
+ return existing;
760
+ }
761
+ /** The session of `key`, if a participant has joined it. */
762
+ function findSession(key) {
763
+ return sessionRegistry().sessions.get(sessionKeyId(key));
764
+ }
765
+ /** The build's on-disk claim record, which stops another process pruning
766
+ * its tree: held once by the build, whichever participant publishes, and
767
+ * released only when the build retires. */
768
+ function holdBuildClaimRecord(key, hold) {
769
+ const state = joined(key);
770
+ state.claimRecord ??= hold();
771
+ }
772
+ /** `participant` leaves the build of `key`. The build retires with its
773
+ * last participant, once no attempt is in flight: it leaves the registry
774
+ * and releases its claim record, so its tree is pruned once stale. */
775
+ function leaveSession(key, participant) {
776
+ const state = findSession(key);
777
+ if (state === void 0) return;
778
+ state.participants.delete(participant);
779
+ retireIfUnused(sessionKeyId(key), state);
780
+ }
781
+ function retireIfUnused(id, state) {
782
+ if (state.participants.size > 0) return;
783
+ const inflight = state.watchTransaction;
784
+ if (inflight !== null) {
785
+ const retry = () => retireIfUnused(id, state);
786
+ inflight.then(retry, retry);
787
+ return;
568
788
  }
569
- });
570
- function engineApi() {
571
- const override = singletonStore[ENGINE_API_OVERRIDE_KEY];
572
- return override ? override() : v2EngineApi();
789
+ const sessions = sessionRegistry().sessions;
790
+ if (sessions.get(id) !== state) return;
791
+ sessions.delete(id);
792
+ sessionRegistry().engines.delete(id);
793
+ state.claimRecord?.();
794
+ state.claimRecord = null;
573
795
  }
574
796
  //#endregion
575
797
  //#region session/timing.ts
@@ -605,6 +827,23 @@ function logBuildTimings(bt, rustTiming, log, verbose) {
605
827
  }
606
828
  //#endregion
607
829
  //#region session/extraction-session.ts
830
+ /** An observation's state when its file is gone, or cannot be read; any
831
+ * other state is the content hash it was observed with. */
832
+ const REMOVED_OBSERVATION = "removed";
833
+ const UNREADABLE_OBSERVATION = "unreadable";
834
+ /** A loaded system's identity: its evaluated modules and a fingerprint of
835
+ * the whole load. */
836
+ function systemLoad(system) {
837
+ return {
838
+ identity: contentHash(JSON.stringify(system)),
839
+ dependencies: system.dependencies ?? []
840
+ };
841
+ }
842
+ const CONSOLE_LOGGER = {
843
+ info: (line) => console.error(line),
844
+ warn: (line) => console.warn(line),
845
+ error: (line) => console.error(line)
846
+ };
608
847
  const SESSION_DIR_MAX_AGE_MS = 864e5;
609
848
  const STATUS_WATCHDOG_MS = 2e3;
610
849
  const LEGACY_FLAT_ARTIFACTS = [
@@ -623,29 +862,120 @@ function isDiskString(value) {
623
862
  }
624
863
  /** Rejection of an attempt whose corpus holds an aborted parse. */
625
864
  var AbortedParseRejection = class extends Error {};
865
+ /** A reload refused because its full read would replace host-supplied
866
+ * bytes with the disk's: a system reload a parse hold deferred, or a full
867
+ * read that reaches a host-supplied file only under another spelling (a
868
+ * case-insensitive volume). Like a hold, it keeps the prior publication
869
+ * and the observed sources. */
870
+ var DeferredReloadRefusal = class extends Error {};
871
+ /** The file's canonical path, or null when it cannot be resolved. */
872
+ function canonicalPath(path) {
873
+ try {
874
+ return realpathSync.native(path);
875
+ } catch {
876
+ return null;
877
+ }
878
+ }
879
+ /** The host sources a full read reaches only under another spelling: a key
880
+ * the read did not reach exactly, whose file is one the read reached by
881
+ * another path (a case-insensitive volume, a symlinked alias). A key with
882
+ * no such file is not read either way, and is left alone. */
883
+ function misspelledHostSources(hostSources, readPaths) {
884
+ const misses = [...hostSources.keys()].filter((path) => !readPaths.has(path));
885
+ if (misses.length === 0) return [];
886
+ const readByCanonical = /* @__PURE__ */ new Map();
887
+ for (const readPath of readPaths) {
888
+ const canonical = canonicalPath(readPath);
889
+ if (canonical === null) continue;
890
+ readByCanonical.set(canonical, [...readByCanonical.get(canonical) ?? [], readPath]);
891
+ }
892
+ return misses.flatMap((path) => {
893
+ const canonical = canonicalPath(path);
894
+ const read = canonical === null ? void 0 : readByCanonical.get(canonical);
895
+ return read === void 0 ? [] : [{
896
+ path,
897
+ read
898
+ }];
899
+ });
900
+ }
626
901
  function isDiskNumber(value) {
627
902
  return Object(value) !== value && Object.prototype.toString.call(value) === "[object Number]";
628
903
  }
904
+ /** The session's own system-props file, imported by absolute path: what a
905
+ * host that serves no system-props module (the CLI) delivers. */
906
+ const SESSION_ARTIFACT = Symbol("animus.session-artifact");
907
+ /** A file's content hash, or '' when it cannot be read. */
908
+ function fileHashOrEmpty(path) {
909
+ try {
910
+ return contentHash(readFileSync(path, "utf-8"));
911
+ } catch {
912
+ return "";
913
+ }
914
+ }
915
+ /** The first file whose content no longer has its recorded hash: an
916
+ * unreadable or deleted file reads as '', which mismatches a real one. */
917
+ function firstChangedFile(hashes) {
918
+ for (const [path, hash] of hashes) if (fileHashOrEmpty(path) !== hash) return path;
919
+ }
629
920
  /** Bundler-agnostic extraction pipeline: system load, source ingestion,
630
921
  * analysis, emission, and watch-cycle diffing. Carries no bundler types. */
631
922
  var ExtractionSession = class {
632
923
  host;
633
924
  /** Set by the adapter before any pipeline run. */
634
925
  rootDir = null;
926
+ /** A host that resolves imports itself (Vite's `this.resolve`): package
927
+ * discovery asks it first, the loader only for a declared workspace it
928
+ * cannot resolve. Null resolves through the loader alone. */
929
+ hostResolve = null;
930
+ /** Each file's alias comparison, kept by content hash across analyses. */
931
+ aliasCache = new AliasAgreementCache();
635
932
  /** Serialized path aliases, harvested by the adapter from bundler config. */
636
933
  pathAliasesJson = null;
934
+ /** The host's aliases are the project's tsconfig `paths`: each full pass
935
+ * re-reads them, so a tsconfig edit takes effect. */
936
+ aliasesFromTsconfig = false;
937
+ /** The root package.json and the tsconfig chain, absent files included:
938
+ * a change to any is a configuration change, which runs the full pass. */
939
+ configInputPaths = [];
940
+ configInputKeys = /* @__PURE__ */ new Set();
941
+ /** Each configuration input's content hash at the last full pass, ''
942
+ * for an absent one: the probe's basis when the host reports nothing. */
943
+ configInputHashes = /* @__PURE__ */ new Map();
637
944
  /** The host's export conditions, in its order, for the system loader. */
638
945
  conditions = [];
946
+ /** The host's extension order, as its adapter captured it; the built-in
947
+ * policy where no host resolves. */
948
+ extensionPolicy = BUILT_IN_EXTENSION_POLICY;
949
+ /** Which files the host serves as assets (spec §2.2 A8); the built-in
950
+ * classification where no host's can be read. */
951
+ assetClassification = builtInAssetClassification("noHost");
952
+ /** How the host's bundler expands computed loads (spec §6.3); without
953
+ * one, every computed load keeps its widest reading. */
954
+ loadExpander;
639
955
  assetCopyCache = /* @__PURE__ */ new Map();
640
956
  /** Physical asset files registered with the host watcher. */
641
957
  assetDependencyPaths = /* @__PURE__ */ new Set();
642
958
  assetDependencyKeys = /* @__PURE__ */ new Set();
643
- /** System-props module id override. Turbopack rejects absolute-path
644
- * imports, so its driver sets a virtual id; null injects the abs path. */
645
- systemPropsModuleId = null;
959
+ /** What generated code imports; a host that serves its own modules
960
+ * declares them before planning. */
961
+ delivery = {
962
+ cssModuleId: ANIMUS_CSS_MODULE_ID,
963
+ systemPropsModuleId: SESSION_ARTIFACT
964
+ };
646
965
  /** Whether the analysis-inputs hydration corpus is written to disk. Only
647
966
  * isolated loader workers replay it; an in-process loader reads memory. */
648
967
  persistAnalysisInputs = false;
968
+ /** The host substitutes asset() URLs itself (Vite: dev copies, build
969
+ * emitFile markers): the published sheets keep their placeholders, and
970
+ * the session makes no copy and tracks no asset file. */
971
+ hostSubstitutesAssets = false;
972
+ /** Whether the session keeps its artifacts on disk: status, commit,
973
+ * manifest, styles, the epoch witness, asset copies and its directory
974
+ * claim, with the sibling reconciliation they need. A host that serves
975
+ * every result from memory (Vite) turns it off; the shared state still
976
+ * publishes. Such a host also substitutes assets (above): asset copies
977
+ * are artifacts. */
978
+ persistArtifacts = true;
649
979
  /** Whether this session serves development (a watch), where an error-level
650
980
  * diagnostic is reported and the session keeps running. Only a build
651
981
  * fails on one. */
@@ -693,29 +1023,90 @@ var ExtractionSession = class {
693
1023
  /** Fires exactly once per settled `handleWatchUpdate` — the failure cause,
694
1024
  * or null when clean. Observation never alters delivery. */
695
1025
  onCycleSettled = null;
1026
+ /** Fires once per published generation, and per held attempt. */
1027
+ onOutcome = null;
1028
+ /** The last published generation; null before the first. */
1029
+ publication = null;
1030
+ /** The original sources (raw, before adaptation) the session holds, by
1031
+ * root-relative path: the published generation's, plus any that an
1032
+ * attempt in flight or held by an aborted parse has read. */
1033
+ get heldSources() {
1034
+ return this.fileCache;
1035
+ }
1036
+ /** The generation an emit produced, until it commits with the
1037
+ * ingestion's ownership and the external package state it analysed. */
1038
+ stagedPublication = null;
696
1039
  options;
697
1040
  staticCssJson;
698
1041
  system = null;
699
- /** Configured external files that could not be read. Incremental passes
700
- * re-collect nothing, so they replay these instead of passing strict. */
1042
+ /** Configured files that could not be read. Incremental passes replay
1043
+ * these instead of passing strict, and add a watched change they cannot
1044
+ * read. */
701
1045
  ingestionFailureDiagnostics = [];
702
1046
  /** Whether the corpus being analyzed left a file out, whose renders the
703
1047
  * analysis cannot see. */
704
1048
  skippedSources = [];
1049
+ /** Resolver calls ingestion ended under another spelling. */
1050
+ ingestionOpenings = [];
1051
+ /** The adapted originals of the ingestion being analysed: it publishes
1052
+ * only after its analysis does. */
1053
+ ingestionAdapted = /* @__PURE__ */ new Map();
705
1054
  /** Full package-resolution map from the last full pipeline — replayed by
706
1055
  * incremental passes (sourceEntries alone omits dist-resolved packages). */
707
1056
  lastPackageMap = {};
708
1057
  fileCache = /* @__PURE__ */ new Map();
709
- corpus = createSourceCorpus({
710
- engineApi: () => engineApi(),
711
- prefix: "[animus-next]",
712
- strict: () => !!this.options.strict,
713
- warn: (message) => this.warn(message),
714
- reportErrors: () => this.reportErrors()
715
- });
1058
+ /** Held sources whose bytes the host supplied (WatchChanges.sources),
1059
+ * by root-relative path; a read from disk drops the path. */
1060
+ hostSuppliedKeys = /* @__PURE__ */ new Set();
1061
+ corpus = createSourceCorpus(this.corpusHost());
1062
+ /** The corpus reads the driver label when it reports, so a host that
1063
+ * sets `driverLabel` after construction is named in its messages. */
1064
+ corpusHost() {
1065
+ const label = () => this.driverLabel;
1066
+ return {
1067
+ engineApi: () => this.engine(),
1068
+ get prefix() {
1069
+ return `[${label()}]`;
1070
+ },
1071
+ strict: () => !!this.options.strict,
1072
+ warn: (message) => this.warn(message),
1073
+ reportErrors: () => this.reportErrors(),
1074
+ extensions: () => this.extensionPolicy,
1075
+ rootDir: () => this.rootDir
1076
+ };
1077
+ }
716
1078
  systemDependencyKeys = /* @__PURE__ */ new Set();
1079
+ /** What a host watches for this Session, one list for every host: the
1080
+ * files whose change it acts on (the system module and the modules it
1081
+ * read, asset() sources, the configuration inputs), the paths the system
1082
+ * load found absent, whose creation reloads it, and the external package
1083
+ * roots, watched recursively. An absent path inside `covered` (roots the
1084
+ * host already watches recursively) is left out: a host's recursive
1085
+ * watch reports its creation, and watching an absent file inside a
1086
+ * watched directory can stop that directory's events (inotify). */
1087
+ watchPlan(covered = []) {
1088
+ const system = this.rootDir === null ? [] : [resolve(this.rootDir, this.options.system)];
1089
+ return {
1090
+ files: [.../* @__PURE__ */ new Set([
1091
+ ...system,
1092
+ ...this.systemDependencyPaths,
1093
+ ...this.assetDependencyPaths,
1094
+ ...this.configInputPaths
1095
+ ])],
1096
+ missing: this.systemMissingPaths.filter((path) => !covered.some((root) => isPathWithinRoot(root, path))),
1097
+ directories: [...this.externalWatchRoots]
1098
+ };
1099
+ }
1100
+ /** Whether `path` is the system module, one of its dependencies, or a
1101
+ * path its load found absent: a change to it reloads the system. */
1102
+ isSystemDependency(path) {
1103
+ return toWatchKeys(path).some((key) => this.systemDependencyKeys.has(key));
1104
+ }
717
1105
  /** Loader-reported dependency paths for bundler watch registration. */
718
1106
  systemDependencyPaths = [];
1107
+ /** Paths the system load looked for and found absent: a host registers
1108
+ * them for creation, which reloads the system. */
1109
+ systemMissingPaths = [];
719
1110
  systemDependencyHashes = /* @__PURE__ */ new Map();
720
1111
  lastSystemPropsHash = null;
721
1112
  artifactRecords = {
@@ -729,9 +1120,20 @@ var ExtractionSession = class {
729
1120
  /** Release handle for the in-process owner slot AND the directory's
730
1121
  * on-disk owner record, taken and released as one; null when unheld. */
731
1122
  releasePublicationClaim = null;
732
- /** Session identity, claimed once per PROCESS and adopted by every later
733
- * session instance so all compilers share one artifact tree. */
734
- sessionId = claimProcessSessionId();
1123
+ /** The build this session takes part in: every participant of one build
1124
+ * shares its artifact tree and state; another build of the root never
1125
+ * does. */
1126
+ sessionId;
1127
+ /** The log tier, from the option and `ANIMUS_DEBUG` at construction. */
1128
+ verbosity;
1129
+ /** The emission mode at construction: explicit, else `NODE_ENV`'s. It
1130
+ * defaults minify. */
1131
+ startupMode;
1132
+ joinedKey = null;
1133
+ /** This instance as a participant of its build, once joined. */
1134
+ participant = null;
1135
+ /** Closed: it left its build and starts no attempt until it reattaches. */
1136
+ detached = false;
735
1137
  /** Epoch of the last published analysis; null seeds from disk so a
736
1138
  * same-session restart with unchanged plans rewrites no bytes. */
737
1139
  lastEpochValue = null;
@@ -746,10 +1148,23 @@ var ExtractionSession = class {
746
1148
  * the cache keeps every observed source so the repair publishes them in
747
1149
  * one generation; a successful publication clears it. */
748
1150
  heldRejection = null;
749
- /** A system reload an aborted parse rejected. Its dependency hashes already
750
- * advanced, so the first batch that passes the parse check publishes
751
- * through the full pipeline, which loads the latest system. */
1151
+ /** Reload debt: a full pass that failed with a committed system to
1152
+ * hold, in development or under an aborted parse, owed until one
1153
+ * succeeds. runFullPipeline alone sets and clears it. While it stands, every batch retries the full reload and
1154
+ * nothing publishes against the stale system. Under an aborted parse's
1155
+ * hold, the first batch that passes the parse check pays it instead. */
752
1156
  systemReloadOwed = false;
1157
+ /** The system dependencies the last committed full pass recorded: a
1158
+ * failed reload watches these and its own, never an earlier failure's. */
1159
+ lastGoodSystemDependencies = {
1160
+ keys: /* @__PURE__ */ new Set(),
1161
+ paths: [],
1162
+ missing: [],
1163
+ hashes: /* @__PURE__ */ new Map()
1164
+ };
1165
+ /** Whether the current full pass's system load recorded its
1166
+ * dependencies. */
1167
+ attemptRecordedSystemDependencies = false;
753
1168
  /** Monotonic readiness witness: flips true on the first complete
754
1169
  * publication and never regresses; every later status write carries it. */
755
1170
  firstEmissionComplete = false;
@@ -760,27 +1175,57 @@ var ExtractionSession = class {
760
1175
  constructor(options, host = {}) {
761
1176
  this.host = host;
762
1177
  this.options = options;
1178
+ this.sessionId = host.build ?? createBuildId();
763
1179
  this.staticCssJson = serializeStaticCss(options.staticCss);
1180
+ this.verbosity = resolveVerbosity(options.verbose, process.env.ANIMUS_DEBUG);
1181
+ this.startupMode = resolveMode(options.mode, () => process.env.NODE_ENV === "production" ? "production" : "development").mode;
764
1182
  }
765
1183
  /** Forgets that `relPath` could not be read once an incremental pass reads
766
- * or deletes it, so its cost stops holding pruning off. */
1184
+ * or deletes it, so its cost stops holding pruning off. Whether one was
1185
+ * recorded. */
767
1186
  clearIngestionFailure(relPath) {
1187
+ const before = this.ingestionFailureDiagnostics.length;
768
1188
  this.ingestionFailureDiagnostics = this.ingestionFailureDiagnostics.filter((diagnostic) => diagnostic.file !== relPath);
1189
+ return this.ingestionFailureDiagnostics.length !== before;
1190
+ }
1191
+ /** The module extracted terminals import their factories from. */
1192
+ get runtimeImport() {
1193
+ return this.options.runtimeImport ?? "@animus-ui/system/runtime";
769
1194
  }
770
1195
  get sessionDir() {
771
1196
  return sessionArtifactDir(this.rootDir, this.sessionId);
772
1197
  }
1198
+ /** This session's build of its root, joined on first use: a participant
1199
+ * whose options differ from the build's fails with
1200
+ * `SessionConfigurationConflict`. */
1201
+ get key() {
1202
+ if (this.joinedKey === null) {
1203
+ const key = sessionKey(this.sessionId, this.rootDir);
1204
+ const participant = `${this.driverLabel}#${randomUUID()}`;
1205
+ joinSession(key, participant, this.options);
1206
+ this.participant = participant;
1207
+ this.joinedKey = key;
1208
+ }
1209
+ return this.joinedKey;
1210
+ }
773
1211
  get verbose() {
774
- return resolveVerbosity(this.options.verbose, process.env.ANIMUS_DEBUG) !== "quiet";
1212
+ return this.verbosity !== "quiet";
1213
+ }
1214
+ /** The engine of this session's build. */
1215
+ engine() {
1216
+ return this.host.engine?.() ?? engineApi(this.key);
1217
+ }
1218
+ get logger() {
1219
+ return this.host.logger ?? CONSOLE_LOGGER;
775
1220
  }
776
1221
  log(msg) {
777
- if (this.verbose) console.error(`[animus] ${msg}`);
1222
+ if (this.verbose) this.logger.info(`[animus] ${msg}`);
778
1223
  }
779
1224
  warn(msg) {
780
- console.warn(`[animus] ${msg}`);
1225
+ this.logger.warn(`[animus] ${msg}`);
781
1226
  }
782
1227
  reportErrors() {
783
- return this.development ? (message) => console.error(message) : void 0;
1228
+ return this.development ? (message) => this.logger.error(message) : void 0;
784
1229
  }
785
1230
  /** A build-strictness check that failed: a build throws `message`, and
786
1231
  * development reports it at error level and takes the non-strict path. */
@@ -789,6 +1234,11 @@ var ExtractionSession = class {
789
1234
  if (!report) throw new Error(message);
790
1235
  report(message);
791
1236
  }
1237
+ /** A finding `strict` turns into a strict failure; otherwise a warning. */
1238
+ strictOrWarn(message, strictMessage = message) {
1239
+ if (this.options.strict) this.strictFailure(strictMessage);
1240
+ else this.warn(message);
1241
+ }
792
1242
  now() {
793
1243
  return this.verbose ? performance.now() : 0;
794
1244
  }
@@ -803,6 +1253,11 @@ var ExtractionSession = class {
803
1253
  lastExternalOutcomes = [];
804
1254
  /** The analysed kits' descriptors, from the last discovery. */
805
1255
  kitDescriptors = [];
1256
+ /** Package name → the real root of the instance discovery read. */
1257
+ packageInstances = {};
1258
+ /** What this attempt's analysis read of the discovered packages; the
1259
+ * build's shared record once the analysis publishes. */
1260
+ packageReadRecord = null;
806
1261
  /** Component count of the last published analysis, null before the first —
807
1262
  * read by drivers instead of re-parsing the manifest JSON every cycle. */
808
1263
  lastComponentCount = null;
@@ -813,18 +1268,15 @@ var ExtractionSession = class {
813
1268
  /** When superseded asset copies leave `assets/`: `'full-pipeline'` suits a
814
1269
  * driver serving the dir in place, `'every-cycle'` one republishing it. */
815
1270
  staleAssetPruning = "full-pipeline";
816
- /** Driver-owned STRUCTURAL exclusions, joined to the never-replaceable set
817
- * — never the user list, whose presence flips the replace semantics. */
1271
+ /** The host's output directories (`outputExclusion`): root-relative,
1272
+ * excluded as literal path prefixes from discovery and from package
1273
+ * collection, whatever the user's list. */
818
1274
  structuralExclude = [];
819
1275
  scanConfigMemo = null;
820
- /** Per-pattern exclusion hit counts from this session's matcher —
821
- * driver-consumed reporting surface (dead-pattern visibility). */
822
- getExcludeStats() {
823
- return this.resolveScanConfig().excludeMatcher.stats();
824
- }
825
1276
  /** Resolve the scan configuration from options — the single source of the
826
- * exclude/extension policy shared by the full and incremental pipelines. */
827
- resolveScanConfig() {
1277
+ * exclude/extension policy shared by the full and incremental pipelines,
1278
+ * and by a host classifying its own file events. */
1279
+ get scanConfig() {
828
1280
  if (this.scanConfigMemo === null) {
829
1281
  const extensionsSet = new Set(this.options.extensions ?? DEFAULT_EXTENSIONS);
830
1282
  this.scanConfigMemo = {
@@ -834,8 +1286,8 @@ var ExtractionSession = class {
834
1286
  }
835
1287
  return this.scanConfigMemo;
836
1288
  }
837
- /** Concurrent entries wait for publication, then ingest their own batch;
838
- * content hashes coalesce observations the prior analysis covered. */
1289
+ /** Records the batch before any await, so an attempt that rejects never
1290
+ * erases it, and settles with the attempt that takes it (SA20). */
839
1291
  async handleWatchUpdate(changes) {
840
1292
  try {
841
1293
  await this.routeWatchUpdate(changes);
@@ -846,36 +1298,131 @@ var ExtractionSession = class {
846
1298
  this.onCycleSettled?.(null);
847
1299
  }
848
1300
  async routeWatchUpdate(changes) {
849
- const hasBatch = (changes.modifiedFiles?.size ?? 0) > 0 || (changes.removedFiles?.size ?? 0) > 0;
850
- const inflight = getWatchTransaction();
1301
+ if ((changes.modifiedFiles?.size ?? 0) > 0 || (changes.removedFiles?.size ?? 0) > 0) {
1302
+ await this.awaitObservation(this.recordObservation(changes));
1303
+ return;
1304
+ }
1305
+ const inflight = getWatchTransaction(this.key);
851
1306
  if (inflight) {
852
1307
  await inflight;
853
- if (!hasBatch) return;
854
- }
855
- if (!this.system) {
856
- const owner = getOwningWatchSession();
857
- if (owner && owner !== this && hasBatch) await owner.ingestForwardedBatch(changes);
858
1308
  return;
859
1309
  }
860
- await this.ingestForwardedBatch(changes);
1310
+ if (this.system && !this.detached) await this.runWatchAttempt(changes);
1311
+ }
1312
+ /** Queues the batch's observations under a new revision, synchronously.
1313
+ * A batch equivalent to what the active attempt took joins that attempt:
1314
+ * a second compiler's report of the same edit never analyzes it twice. */
1315
+ recordObservation(changes) {
1316
+ const queue = observationQueue(this.key);
1317
+ const observed = /* @__PURE__ */ new Map();
1318
+ for (const path of changes.modifiedFiles ?? []) {
1319
+ let state;
1320
+ try {
1321
+ state = contentHash(changes.sources?.get(path) ?? readFileSync(path, "utf-8"));
1322
+ } catch {
1323
+ state = UNREADABLE_OBSERVATION;
1324
+ }
1325
+ observed.set(path, state);
1326
+ }
1327
+ for (const path of changes.removedFiles ?? []) observed.set(path, REMOVED_OBSERVATION);
1328
+ const revision = ++queue.revision;
1329
+ const active = queue.active;
1330
+ if (active !== null && [...observed].every(([path, state]) => !queue.untaken.has(path) && active.taken.get(path) === state)) {
1331
+ queue.revisions.set(revision, active.transaction);
1332
+ return revision;
1333
+ }
1334
+ for (const [path, state] of observed) {
1335
+ queue.untaken.set(path, state);
1336
+ const supplied = changes.sources?.get(path);
1337
+ if (supplied === void 0) queue.suppliedSources.delete(path);
1338
+ else queue.suppliedSources.set(path, supplied);
1339
+ }
1340
+ queue.revisions.set(revision, null);
1341
+ return revision;
861
1342
  }
862
- /** The shared owner gate for local and forwarded batches. Every waiter
863
- * rechecks the slot so distinct batches publish in arrival order. */
864
- async ingestForwardedBatch(changes) {
865
- if (!this.system) return;
1343
+ /** Settles with the attempt that takes `revision`. Another attempt's
1344
+ * rejection is its own callers': this observation runs after it. */
1345
+ async awaitObservation(revision) {
1346
+ const queue = observationQueue(this.key);
866
1347
  for (;;) {
867
- const inflight = getWatchTransaction();
868
- if (!inflight) break;
869
- await inflight;
870
- }
871
- const transaction = Promise.resolve().then(() => this.processWatchUpdate(changes));
872
- setWatchTransaction(transaction);
873
- try {
874
- await transaction;
875
- } finally {
876
- setWatchTransaction(null);
1348
+ const taken = queue.revisions.get(revision);
1349
+ if (taken !== null && taken !== void 0) {
1350
+ queue.revisions.delete(revision);
1351
+ await taken;
1352
+ return;
1353
+ }
1354
+ const inflight = getWatchTransaction(this.key);
1355
+ if (inflight) {
1356
+ await inflight.catch(() => void 0);
1357
+ continue;
1358
+ }
1359
+ const owner = this.system && !this.detached ? this : getOwningWatchSession(this.key);
1360
+ if (owner === null && !this.detached && getManifestJson(this.key) !== null) {
1361
+ await this.runFullPipeline();
1362
+ continue;
1363
+ }
1364
+ if (owner === null || !owner.startObservedAttempt()) {
1365
+ queue.revisions.delete(revision);
1366
+ return;
1367
+ }
877
1368
  }
878
1369
  }
1370
+ /** Starts one attempt over every untaken observation of the build. */
1371
+ startObservedAttempt() {
1372
+ if (!this.system || this.detached) return false;
1373
+ const { taken, rescan, sources } = this.takeObservations();
1374
+ const modifiedFiles = /* @__PURE__ */ new Set();
1375
+ const removedFiles = /* @__PURE__ */ new Set();
1376
+ for (const [path, state] of taken) (state === REMOVED_OBSERVATION ? removedFiles : modifiedFiles).add(path);
1377
+ this.settleTaken(taken, this.runWatchAttempt({
1378
+ modifiedFiles,
1379
+ removedFiles,
1380
+ sources
1381
+ }, rescan));
1382
+ return true;
1383
+ }
1384
+ /** Every untaken observation of the build, and whether a full rescan is
1385
+ * requested, which the caller's attempt now covers. */
1386
+ takeObservations() {
1387
+ const queue = observationQueue(this.key);
1388
+ const taken = new Map(queue.untaken);
1389
+ const rescan = queue.rescan;
1390
+ const sources = new Map(queue.suppliedSources);
1391
+ queue.untaken.clear();
1392
+ queue.suppliedSources.clear();
1393
+ queue.rescan = false;
1394
+ return {
1395
+ taken,
1396
+ rescan,
1397
+ sources
1398
+ };
1399
+ }
1400
+ /** `transaction` settles every waiting revision, and is the active
1401
+ * attempt equivalent observations join until it settles. */
1402
+ settleTaken(taken, transaction) {
1403
+ const queue = observationQueue(this.key);
1404
+ for (const [revision, attempt] of queue.revisions) if (attempt === null) queue.revisions.set(revision, transaction);
1405
+ queue.active = {
1406
+ taken,
1407
+ transaction
1408
+ };
1409
+ const settle = () => {
1410
+ if (queue.active?.transaction === transaction) queue.active = null;
1411
+ };
1412
+ transaction.then(settle, settle);
1413
+ }
1414
+ /** Registers one attempt in the build's single-flight slot before it
1415
+ * processes, so a nested full system reload inherits the gate rather
1416
+ * than clear it, and clears the slot when it settles either way. */
1417
+ runWatchAttempt(changes, rescan = false) {
1418
+ const transaction = Promise.resolve().then(() => rescan ? this.runFullPipeline([], changes.sources) : this.processWatchUpdate(changes));
1419
+ setWatchTransaction(this.key, transaction);
1420
+ const settle = () => {
1421
+ if (getWatchTransaction(this.key) === transaction) setWatchTransaction(this.key, null);
1422
+ };
1423
+ transaction.then(settle, settle);
1424
+ return transaction;
1425
+ }
879
1426
  async processWatchUpdate(changes) {
880
1427
  const rootDir = this.rootDir;
881
1428
  try {
@@ -886,31 +1433,25 @@ var ExtractionSession = class {
886
1433
  const changed = [...changes.modifiedFiles ?? [], ...changes.removedFiles ?? []];
887
1434
  let assetChanged = false;
888
1435
  let systemHit;
1436
+ let configHit;
889
1437
  try {
890
- if (!changes.modifiedFiles && !changes.removedFiles) for (const [dep, hash] of this.systemDependencyHashes) {
891
- let current = "";
892
- try {
893
- current = contentHash(readFileSync(dep, "utf-8"));
894
- } catch {}
895
- if (current !== hash) {
896
- systemHit = dep;
897
- break;
898
- }
899
- }
900
- else systemHit = changed.find((path) => toWatchKeys(path).some((key) => this.systemDependencyKeys.has(key)));
1438
+ if (changed.length === 0) {
1439
+ systemHit = firstChangedFile(this.systemDependencyHashes);
1440
+ if (systemHit === void 0) configHit = firstChangedFile(this.configInputHashes);
1441
+ } else systemHit = changed.find((path) => this.isSystemDependency(path));
901
1442
  } catch (err) {
902
1443
  this.warn(`HMR system-reload check failed: ${String(err)}`);
903
1444
  }
904
- if (systemHit) {
905
- this.log(`system reload: system dependency changed (${relative(rootDir, systemHit)})`);
1445
+ configHit ??= changed.find((path) => toWatchKeys(path).some((key) => this.configInputKeys.has(key)));
1446
+ const debtRetry = this.systemReloadOwed && this.heldRejection === null;
1447
+ if (systemHit || configHit || debtRetry) {
1448
+ this.log(systemHit ? `system reload: system dependency changed (${relative(rootDir, systemHit)})` : configHit ? `system reload: configuration changed (${relative(rootDir, configHit)})` : "system reload: retrying the reload a failure left owed");
906
1449
  this.resetForHmr();
907
1450
  try {
908
- const promise = this.runFullPipeline(this.pendingFromBatch(changes));
909
- setAnalysisStartedPromise(promise);
1451
+ const promise = this.runFullPipeline(this.pendingFromBatch(changes), changes.sources);
1452
+ setAnalysisStartedPromise(this.key, promise);
910
1453
  await promise;
911
- this.systemReloadOwed = false;
912
1454
  } catch (err) {
913
- if (err instanceof AbortedParseRejection) this.systemReloadOwed = true;
914
1455
  try {
915
1456
  this.beginStatusAttempt();
916
1457
  this.writeAnalysisStatus("failed", this.pendingFromBatch(changes), String(err));
@@ -925,14 +1466,16 @@ var ExtractionSession = class {
925
1466
  this.assetCopyCache.clear();
926
1467
  assetChanged = true;
927
1468
  }
928
- const { excludeMatcher, extensionsSet } = this.resolveScanConfig();
1469
+ const { excludeMatcher, extensionsSet } = this.scanConfig;
929
1470
  const priorCacheEntries = /* @__PURE__ */ new Map();
930
1471
  const priorExternalFileOwners = { ...this.externalFileOwners };
1472
+ const priorHostSuppliedKeys = new Set(this.hostSuppliedKeys);
931
1473
  const recordPrior = (key) => {
932
1474
  if (!priorCacheEntries.has(key)) priorCacheEntries.set(key, this.fileCache.get(key) ?? null);
933
1475
  };
934
1476
  const inventoryUpdates = [];
935
1477
  const ownerRemovals = [];
1478
+ const removedProjectFiles = /* @__PURE__ */ new Map();
936
1479
  let removedAny = false;
937
1480
  if (changes.removedFiles) for (const removedPath of changes.removedFiles) {
938
1481
  let key;
@@ -946,8 +1489,10 @@ var ExtractionSession = class {
946
1489
  }
947
1490
  recordPrior(key);
948
1491
  this.clearIngestionFailure(key);
1492
+ this.hostSuppliedKeys.delete(key);
949
1493
  if (this.fileCache.delete(key)) {
950
1494
  removedAny = true;
1495
+ if (!owningRoot) removedProjectFiles.set(key, removedPath);
951
1496
  if (owningRoot) {
952
1497
  inventoryUpdates.push({
953
1498
  root: owningRoot,
@@ -978,15 +1523,29 @@ var ExtractionSession = class {
978
1523
  owningRoot: null
979
1524
  }));
980
1525
  const changedPaths = [];
1526
+ const inOutput = outputSourceGuard(rootDir, this.structuralExclude);
1527
+ let readFailuresChanged = false;
981
1528
  for (const target of targets) {
982
1529
  const relPath = target.key;
1530
+ const outputDir = inOutput(target.abs);
1531
+ if (outputDir !== null) {
1532
+ this.warn(outputSourceMessage(relPath, outputDir));
1533
+ continue;
1534
+ }
983
1535
  let source;
984
1536
  try {
985
- source = readFileSync(target.abs, "utf-8");
986
- } catch {
1537
+ const supplied = changes.sources?.get(target.abs);
1538
+ source = supplied ?? readFileSync(target.abs, "utf-8");
1539
+ if (supplied === void 0) this.hostSuppliedKeys.delete(relPath);
1540
+ else this.hostSuppliedKeys.add(relPath);
1541
+ } catch (err) {
1542
+ if (isDeletedSource(err)) continue;
1543
+ this.clearIngestionFailure(relPath);
1544
+ this.ingestionFailureDiagnostics = [...this.ingestionFailureDiagnostics, unreadableSourceDiagnostic(relPath, err)];
1545
+ readFailuresChanged = true;
987
1546
  continue;
988
1547
  }
989
- this.clearIngestionFailure(relPath);
1548
+ if (this.clearIngestionFailure(relPath)) readFailuresChanged = true;
990
1549
  const rawHash = contentHash(source);
991
1550
  const cached = this.fileCache.get(relPath);
992
1551
  const hash = rawHash;
@@ -1009,7 +1568,7 @@ var ExtractionSession = class {
1009
1568
  }
1010
1569
  }
1011
1570
  }
1012
- if (changedPaths.length > 0 || removedAny || assetChanged) {
1571
+ if (changedPaths.length > 0 || removedAny || readFailuresChanged || assetChanged) {
1013
1572
  const pending = changedPaths.map((rel) => [rel, this.fileCache.get(rel).hash]);
1014
1573
  const commitObservedBatch = () => {
1015
1574
  for (const update of inventoryUpdates) {
@@ -1035,24 +1594,32 @@ var ExtractionSession = class {
1035
1594
  });
1036
1595
  this.throwOnAbortedParse(ingested);
1037
1596
  this.skippedSources = ingested.skippedOriginals ?? [];
1597
+ this.ingestionOpenings = ingested.openings ?? [];
1598
+ this.ingestionAdapted = adaptedOriginals(ingested.ownership);
1038
1599
  if (this.systemReloadOwed) {
1039
1600
  this.log("system reload: publishing the reload a parse hold deferred");
1601
+ const stale = this.staleHostSuppliedSources(changes.sources);
1602
+ if (stale.length > 0) throw new DeferredReloadRefusal(`[${this.driverLabel}] system reload not published: its full read would take ${stale.join(", ")} from disk, which does not hold the bytes the host supplied; the last generation stays served, and the reload publishes with the next change once the disk holds them`);
1040
1603
  this.resetForHmr();
1041
- await this.runFullPipeline(pending);
1042
- this.systemReloadOwed = false;
1604
+ await this.runFullPipeline(pending, changes.sources);
1043
1605
  return;
1044
1606
  }
1045
1607
  this.externalFileOwners = projectExternalFileOwners(ingested, this.externalFileOwners);
1046
- resetAnalysisStartedPromise();
1608
+ resetAnalysisStartedPromise(this.key);
1047
1609
  const promise = this.runIncrementalPipeline(ingested.analysisEntries, pending);
1048
- setAnalysisStartedPromise(promise);
1610
+ setAnalysisStartedPromise(this.key, promise);
1049
1611
  await promise;
1050
1612
  } catch (err) {
1051
- if (err instanceof AbortedParseRejection) commitObservedBatch();
1613
+ if (err instanceof AbortedParseRejection || err instanceof DeferredReloadRefusal) commitObservedBatch();
1052
1614
  else {
1053
- for (const [key, prior] of priorCacheEntries) if (prior === null) this.fileCache.delete(key);
1054
- else this.fileCache.set(key, prior);
1615
+ for (const [key, prior] of priorCacheEntries) {
1616
+ const removed = removedProjectFiles.get(key);
1617
+ if (removed !== void 0 && !existsSync(removed)) continue;
1618
+ if (prior === null) this.fileCache.delete(key);
1619
+ else this.fileCache.set(key, prior);
1620
+ }
1055
1621
  this.externalFileOwners = priorExternalFileOwners;
1622
+ this.hostSuppliedKeys = priorHostSuppliedKeys;
1056
1623
  }
1057
1624
  if (this.statusAttemptOpen) {
1058
1625
  this.debouncePending.clear();
@@ -1060,7 +1627,7 @@ var ExtractionSession = class {
1060
1627
  }
1061
1628
  throw err;
1062
1629
  }
1063
- this.publishSourceIngestion(ingested);
1630
+ this.publishSourceIngestion(ingested, "incremental", (path) => this.fileCache.get(path));
1064
1631
  commitObservedBatch();
1065
1632
  } else if (this.statusAttemptOpen) {
1066
1633
  this.debouncePending.clear();
@@ -1075,88 +1642,251 @@ var ExtractionSession = class {
1075
1642
  const rejection = this.corpus.rejection(prepared);
1076
1643
  if (!rejection) return;
1077
1644
  this.heldRejection = rejection.message;
1645
+ this.onOutcome?.({
1646
+ kind: "held",
1647
+ abortedOriginals: rejection.originals
1648
+ });
1078
1649
  throw new AbortedParseRejection(rejection.message);
1079
1650
  }
1080
- publishSourceIngestion(result) {
1651
+ /** Holds a held full read's entries: what it read stays the held bytes,
1652
+ * never overwriting held host bytes with the disk's (a key the host
1653
+ * supplied before that this read took from disk keeps its bytes). */
1654
+ holdObservedEntries(entries, supplied) {
1655
+ for (const entry of entries) {
1656
+ const fromHost = supplied.has(entry.path);
1657
+ if (this.hostSuppliedKeys.has(entry.path) && !fromHost) continue;
1658
+ this.fileCache.set(entry.path, {
1659
+ hash: entry.hash,
1660
+ source: entry.source
1661
+ });
1662
+ if (fromHost) this.hostSuppliedKeys.add(entry.path);
1663
+ }
1664
+ }
1665
+ /** Held sources whose bytes the host supplied and which the disk no
1666
+ * longer matches: a full read not carrying them (`carried`) would
1667
+ * publish the disk's bytes in their place. */
1668
+ staleHostSuppliedSources(carried) {
1669
+ const stale = [];
1670
+ for (const key of this.hostSuppliedKeys) {
1671
+ const held = this.fileCache.get(key);
1672
+ const abs = resolve(this.rootDir, key);
1673
+ if (held === void 0 || carried?.has(abs)) continue;
1674
+ let onDisk;
1675
+ try {
1676
+ onDisk = readFileSync(abs, "utf-8");
1677
+ } catch {
1678
+ onDisk = null;
1679
+ }
1680
+ if (onDisk === null || contentHash(onDisk) !== held.hash) stale.push(key);
1681
+ }
1682
+ return stale.sort();
1683
+ }
1684
+ /** `raw` is the attempt's input by original path, from which a
1685
+ * quarantined original stays held: every later pass re-ingests it, so it
1686
+ * keeps counting as skipped (nothing it renders is pruned) and its fix
1687
+ * edit compares against its bytes. */
1688
+ publishSourceIngestion(result, pass, raw) {
1689
+ const endedHold = this.heldRejection !== null;
1690
+ const previousOwnership = this.corpus.published.ownership;
1081
1691
  this.heldRejection = null;
1082
- this.fileCache = new Map(result.originalEntries.map((entry) => [entry.path, {
1692
+ const held = new Map(result.originalEntries.map((entry) => [entry.path, {
1083
1693
  hash: entry.hash,
1084
1694
  source: entry.source
1085
1695
  }]));
1696
+ for (const path of result.skippedOriginals ?? []) {
1697
+ const entry = raw(path);
1698
+ if (entry) held.set(path, {
1699
+ hash: entry.hash ?? contentHash(entry.source),
1700
+ source: entry.source
1701
+ });
1702
+ }
1703
+ this.fileCache = held;
1086
1704
  this.corpus.publish(result);
1705
+ const staged = this.stagedPublication;
1706
+ if (staged === null) return;
1707
+ this.stagedPublication = null;
1708
+ const previous = this.publication;
1709
+ const publication = {
1710
+ ...staged,
1711
+ ownership: result.ownership,
1712
+ externalPackageDirs: this.externalPackageDirs,
1713
+ externalSourceEntries: this.externalSourceEntries,
1714
+ externalSourceSideEffects: this.externalSourceSideEffects
1715
+ };
1716
+ this.publication = publication;
1717
+ setPublication(this.key, publication);
1718
+ this.onOutcome?.({
1719
+ kind: "published",
1720
+ publication,
1721
+ pass,
1722
+ planChanged: diffFilePlans(snapshotFilePlans(previous?.manifest ?? null), snapshotFilePlans(publication.manifest)),
1723
+ endedHold,
1724
+ redeliver: endedHold ? Object.values(result.ownership).filter((owner) => previousOwnership[owner.originalPath]?.originalHash !== owner.originalHash).map((owner) => owner.originalPath) : []
1725
+ });
1087
1726
  }
1088
1727
  /** Full analysis + publication, registered in the one in-flight
1089
- * transaction slot; a nested reload leaves the enclosing registration. */
1090
- async runFullPipeline(pending = []) {
1728
+ * transaction slot; a nested reload leaves the enclosing registration.
1729
+ * `sources` are bytes a host already read (see WatchChanges.sources):
1730
+ * the full read uses them, and those of the observations it takes, in
1731
+ * place of the disk. */
1732
+ async runFullPipeline(pending = [], sources = /* @__PURE__ */ new Map()) {
1091
1733
  if (this.releasePublicationClaim === null) {
1092
- const releaseOwner = claimExclusiveSessionOwner(`${this.driverLabel}:${this.sessionDir}`);
1093
- mkdirSync(this.sessionDir, { recursive: true });
1094
- const releaseClaimRecord = holdDirectoryClaim(this.sessionDir);
1095
- this.releasePublicationClaim = () => {
1096
- releaseOwner();
1097
- releaseClaimRecord();
1098
- };
1734
+ const releaseOwner = claimExclusiveSessionOwner(this.key, `${this.driverLabel}:${this.sessionDir}`);
1735
+ if (this.persistArtifacts) {
1736
+ mkdirSync(this.sessionDir, { recursive: true });
1737
+ holdBuildClaimRecord(this.key, () => holdDirectoryClaim(this.sessionDir, { build: this.sessionId }));
1738
+ }
1739
+ this.releasePublicationClaim = releaseOwner;
1099
1740
  }
1100
1741
  const priorSystem = this.system;
1101
- const nested = getWatchTransaction() !== null;
1102
- const transaction = this.runPipelineTransaction(pending);
1103
- if (!nested) setWatchTransaction(transaction);
1742
+ const priorRegistration = this.snapshotWatchRegistration();
1743
+ this.attemptRecordedSystemDependencies = false;
1744
+ const nested = getWatchTransaction(this.key) !== null;
1745
+ const observations = nested ? null : this.takeObservations();
1746
+ const taken = observations?.taken ?? null;
1747
+ const transaction = this.runPipelineTransaction(pending, new Map([...observations?.sources ?? [], ...sources]));
1748
+ if (taken !== null) {
1749
+ setWatchTransaction(this.key, transaction);
1750
+ this.settleTaken(taken, transaction);
1751
+ }
1104
1752
  try {
1105
1753
  await transaction;
1106
1754
  } catch (error) {
1107
1755
  this.system = priorSystem;
1756
+ this.restoreWatchRegistration(priorRegistration);
1757
+ if (priorSystem !== null && (this.development || error instanceof AbortedParseRejection)) this.systemReloadOwed = true;
1108
1758
  throw error;
1109
1759
  } finally {
1110
- if (!nested) setWatchTransaction(null);
1760
+ if (!nested) setWatchTransaction(this.key, null);
1761
+ }
1762
+ this.systemReloadOwed = false;
1763
+ this.lastGoodSystemDependencies = {
1764
+ keys: this.systemDependencyKeys,
1765
+ paths: this.systemDependencyPaths,
1766
+ missing: this.systemMissingPaths,
1767
+ hashes: this.systemDependencyHashes
1768
+ };
1769
+ this.commitSystemAuthority();
1770
+ }
1771
+ /** This attempt's system dependencies: the files its load read, and the
1772
+ * paths it found absent, whose creation is a system change. */
1773
+ recordSystemDependencies(systemPath, dependencies, missing) {
1774
+ const keys = /* @__PURE__ */ new Set();
1775
+ for (const path of [
1776
+ systemPath,
1777
+ ...dependencies,
1778
+ ...missing
1779
+ ]) for (const key of toWatchKeys(path)) keys.add(key);
1780
+ const hashes = /* @__PURE__ */ new Map();
1781
+ for (const dep of dependencies.length > 0 ? dependencies : [systemPath]) try {
1782
+ hashes.set(dep, contentHash(readFileSync(dep, "utf-8")));
1783
+ } catch {
1784
+ hashes.set(dep, "");
1785
+ }
1786
+ for (const path of missing) hashes.set(path, "");
1787
+ this.systemDependencyKeys = keys;
1788
+ this.systemDependencyPaths = dependencies;
1789
+ this.systemMissingPaths = missing;
1790
+ this.systemDependencyHashes = hashes;
1791
+ this.attemptRecordedSystemDependencies = true;
1792
+ }
1793
+ /** Records the configuration inputs and, for a tsconfig-aliased host,
1794
+ * re-reads its aliases. */
1795
+ readConfigInputs(rootDir) {
1796
+ this.configInputPaths = [join(rootDir, "package.json"), ...tsconfigChainPaths(rootDir)];
1797
+ this.configInputKeys = new Set(this.configInputPaths.flatMap((path) => toWatchKeys(path)));
1798
+ this.configInputHashes = new Map(this.configInputPaths.map((path) => [path, fileHashOrEmpty(path)]));
1799
+ if (this.aliasesFromTsconfig) this.pathAliasesJson = buildPathAliasesJson(readTsconfigAliasPairs(rootDir), rootDir)?.json ?? null;
1800
+ }
1801
+ /** What hosts watch for assets, and what the last-good generation
1802
+ * reported about its kits and packages, which a failed full pass must
1803
+ * leave in place. */
1804
+ snapshotWatchRegistration() {
1805
+ return {
1806
+ assetDependencyPaths: new Set(this.assetDependencyPaths),
1807
+ assetDependencyKeys: new Set(this.assetDependencyKeys),
1808
+ kitDescriptors: this.kitDescriptors,
1809
+ lastExternalOutcomes: this.lastExternalOutcomes
1810
+ };
1811
+ }
1812
+ /** Restores the last-good registration. The system dependencies are the
1813
+ * last committed pass's and, when its load got that far, this failed
1814
+ * attempt's (spec §2.3): a fix to any of them must reach the owed reload,
1815
+ * and an earlier failure's are replaced, never accumulated. */
1816
+ restoreWatchRegistration(prior) {
1817
+ Object.assign(this, prior);
1818
+ const lastGood = this.lastGoodSystemDependencies;
1819
+ if (!this.attemptRecordedSystemDependencies) {
1820
+ this.systemDependencyKeys = lastGood.keys;
1821
+ this.systemDependencyPaths = lastGood.paths;
1822
+ this.systemMissingPaths = lastGood.missing;
1823
+ this.systemDependencyHashes = lastGood.hashes;
1824
+ return;
1111
1825
  }
1826
+ this.systemMissingPaths = [.../* @__PURE__ */ new Set([...lastGood.missing, ...this.systemMissingPaths])];
1827
+ this.systemDependencyKeys = /* @__PURE__ */ new Set([...lastGood.keys, ...this.systemDependencyKeys]);
1828
+ this.systemDependencyPaths = [.../* @__PURE__ */ new Set([...lastGood.paths, ...this.systemDependencyPaths])];
1829
+ this.systemDependencyHashes = new Map([...this.systemDependencyHashes, ...lastGood.hashes]);
1830
+ }
1831
+ /** The build's authority becomes the system this session committed, with
1832
+ * it and never before: a failed reload leaves the committed one, and the
1833
+ * context proofs made against it. A committed reload discards every
1834
+ * other context's proof, even for an unchanged system: the reload may
1835
+ * have changed what another context resolves, so each loads again. */
1836
+ commitSystemAuthority() {
1837
+ setSystemAuthority(this.key, {
1838
+ ...systemLoad(this.system),
1839
+ context: this.resolutionContextText(),
1840
+ label: this.resolutionContextLabel(),
1841
+ others: /* @__PURE__ */ new Map()
1842
+ });
1112
1843
  }
1113
- async runPipelineTransaction(pending) {
1844
+ async runPipelineTransaction(pending, hostSources) {
1114
1845
  const pipelineStart = this.now();
1115
1846
  const bt = {};
1116
1847
  this.runSessionStartHygiene();
1117
- setSessionArtifactDir(this.sessionDir);
1848
+ setSessionArtifactDir(this.key, this.sessionDir);
1118
1849
  const rootDir = this.rootDir;
1119
1850
  const priorSourceState = this.snapshotSourceState();
1120
1851
  const resolvedSystemPath = resolve(rootDir, this.options.system);
1852
+ this.readConfigInputs(rootDir);
1853
+ const context = {
1854
+ conditions: this.conditions,
1855
+ workspaces: declaredWorkspacePackages(rootDir),
1856
+ extensions: this.extensionPolicy
1857
+ };
1858
+ this.log(`extension order: ${context.extensions.resolverVersion}`);
1121
1859
  let t = this.now();
1122
- this.system = loadSystemConfig(engineApi, {
1123
- systemPath: resolvedSystemPath,
1124
- rootDir,
1125
- prefix: this.options.prefix,
1126
- prefixContextualVars: this.options.prefixContextualVars,
1127
- conditions: this.conditions
1128
- });
1860
+ try {
1861
+ this.system = this.loadSystemUnder(context);
1862
+ } catch (error) {
1863
+ const reads = systemLoadReads(error);
1864
+ this.recordSystemDependencies(resolvedSystemPath, reads.dependencies, reads.missing);
1865
+ throw error;
1866
+ }
1129
1867
  this.assetCopyCache.clear();
1130
1868
  this.assetDependencyPaths.clear();
1131
1869
  this.assetDependencyKeys.clear();
1132
- {
1133
- const deps = this.system.dependencies ?? [];
1134
- const keys = /* @__PURE__ */ new Set();
1135
- for (const key of toWatchKeys(resolvedSystemPath)) keys.add(key);
1136
- for (const dep of deps) for (const key of toWatchKeys(dep)) keys.add(key);
1137
- this.systemDependencyKeys = keys;
1138
- this.systemDependencyPaths = deps;
1139
- const hashes = /* @__PURE__ */ new Map();
1140
- for (const dep of deps.length > 0 ? deps : [resolvedSystemPath]) try {
1141
- hashes.set(dep, contentHash(readFileSync(dep, "utf-8")));
1142
- } catch {
1143
- hashes.set(dep, "");
1144
- }
1145
- this.systemDependencyHashes = hashes;
1146
- }
1870
+ this.recordSystemDependencies(resolvedSystemPath, this.system.dependencies ?? [], this.system.missing ?? []);
1147
1871
  bt.systemLoad = this.elapsed(t);
1148
1872
  t = this.now();
1149
- const { excludeMatcher, extensionsSet } = this.resolveScanConfig();
1873
+ const { excludeMatcher, extensionsSet } = this.scanConfig;
1150
1874
  const files = discoverFiles(rootDir, rootDir, excludeMatcher, extensionsSet);
1875
+ if (claimUnmatchedExcludeReport(this.key)) for (const line of unmatchedExcludeLines(excludeMatcher)) this.logger.error(line);
1151
1876
  bt.fileDiscovery = this.elapsed(t);
1152
1877
  t = this.now();
1153
1878
  const rawEntries = [];
1154
1879
  const ingestionFailures = [];
1880
+ const readPaths = /* @__PURE__ */ new Set();
1881
+ const supplied = /* @__PURE__ */ new Set();
1155
1882
  for (const filePath of files) {
1156
1883
  const relPath = relative(rootDir, filePath);
1884
+ readPaths.add(filePath);
1157
1885
  let source;
1158
1886
  try {
1159
- source = readFileSync(filePath, "utf-8");
1887
+ const hostSource = hostSources.get(filePath);
1888
+ source = hostSource ?? readFileSync(filePath, "utf-8");
1889
+ if (hostSource !== void 0) supplied.add(relPath);
1160
1890
  } catch (err) {
1161
1891
  if (!isDeletedSource(err)) ingestionFailures.push(unreadableSourceDiagnostic(relPath, err));
1162
1892
  continue;
@@ -1168,21 +1898,36 @@ var ExtractionSession = class {
1168
1898
  });
1169
1899
  }
1170
1900
  bt.fileRead = this.elapsed(t);
1901
+ const rawPaths = new Set(rawEntries.map((entry) => entry.path));
1171
1902
  t = this.now();
1172
- const discoveryDiagnostics = [];
1173
- const packageNames = await extractSystemFilePackages(resolvedSystemPath, engineModuleParser(engineApi()), (diagnostic) => discoveryDiagnostics.push(diagnostic), (name) => {
1174
- const entry = resolvePackagesByName(rootDir, [name])[name];
1175
- return entry ? resolve(rootDir, entry) : null;
1903
+ const resolver = discoveryResolver(this.hostResolve, this.engine(), {
1904
+ rootDir,
1905
+ conditions: context.conditions,
1906
+ workspaces: context.workspaces,
1907
+ extensions: context.extensions
1176
1908
  });
1177
- discoveryDiagnostics.push(...importedKitDiagnostics(rawEntries, engineApi(), rootDir, packageNames));
1178
- const preResolved = resolvePackagesByName(rootDir, packageNames);
1909
+ const discoveryDiagnostics = [];
1910
+ const systemPackageRoot = findPackageRoot(resolvedSystemPath);
1911
+ const kitMembers = /* @__PURE__ */ new Map();
1912
+ const packageNames = await extractSystemFilePackages(resolvedSystemPath, { systemMembership: this.engine().systemMembership ?? requireEngine().systemMembership }, (diagnostic) => discoveryDiagnostics.push(diagnostic), (specifier, importer) => findPackageRoot(importer) === systemPackageRoot ? resolver.resolveSpecifier(specifier) : resolver.loaderResolve(specifier, dirname(importer)), context.extensions, kitMembers);
1913
+ discoveryDiagnostics.push(...importedKitDiagnostics(rawEntries, this.engine(), rootDir, packageNames, (entries) => this.corpus.facts(entries)));
1179
1914
  const rawExternalFiles = /* @__PURE__ */ new Map();
1180
1915
  const collected = await collectExternalPackageSources({
1916
+ extensions: context.extensions,
1917
+ kitSourceRoot: nativeKitSourceRoot(this.engine(), rootDir, context.conditions, context.workspaces, extensionPolicyJson(context.extensions)),
1918
+ locatePackage: nativePackageLocator(this.engine(), context.workspaces),
1181
1919
  specifiers: packageNames,
1182
- resolveSpecifier: (name) => preResolved[name] ? resolve(rootDir, preResolved[name]) : null,
1920
+ resolveSpecifier: async (specifier) => kitMembers.get(specifier) ?? await resolver.resolveSpecifier(specifier),
1921
+ locateFrom: (specifier) => {
1922
+ const file = kitMembers.get(specifier);
1923
+ return file === void 0 ? rootDir : dirname(file);
1924
+ },
1925
+ unresolvedReason: resolver.unresolvedReason,
1926
+ requireSourceEntry: resolver.requireSourceEntry,
1183
1927
  rootDir,
1184
1928
  extensionsSet,
1185
- hasEntry: (relPath) => rawEntries.some((e) => e.path === relPath),
1929
+ hasEntry: (relPath) => rawPaths.has(relPath),
1930
+ hostOutputDirs: this.structuralExclude.map((dir) => resolve(rootDir, dir)),
1186
1931
  onSourceRead: (source, _relPath, absPath) => {
1187
1932
  rawExternalFiles.set(absPath, contentHash(source));
1188
1933
  },
@@ -1201,21 +1946,22 @@ var ExtractionSession = class {
1201
1946
  });
1202
1947
  this.lastExternalOutcomes = collected.outcomes;
1203
1948
  this.ingestionFailureDiagnostics = ingestionFailures;
1949
+ if (resolver.hostFirst) discoveryDiagnostics.push(...systemInstanceMismatches({
1950
+ rootDir,
1951
+ specifiers: packageNames,
1952
+ instances: collected.packageInstances,
1953
+ evaluated: this.systemDependencyPaths,
1954
+ loaderResolve: (specifier) => resolver.loaderResolve(specifier, dirname(resolvedSystemPath))
1955
+ }));
1204
1956
  surfaceManifestDiagnostics({ diagnostics: [...discoveryDiagnostics, ...collected.diagnostics] }, (message) => this.warn(message), {
1205
1957
  levels: this.options.diagnostics,
1206
1958
  strict: this.options.strict,
1207
1959
  reportErrors: this.reportErrors()
1208
1960
  });
1209
- const unresolvableMessage = unresolvableIncludesMessage(collected.outcomes);
1210
- if (unresolvableMessage !== null) {
1211
- if (this.options.strict) this.strictFailure(unresolvableMessage);
1212
- else this.warn(unresolvableMessage);
1213
- }
1961
+ const unresolvableMessage = unresolvableIncludesMessage(collected.outcomes, relative(rootDir, resolvedSystemPath));
1962
+ if (unresolvableMessage !== null) this.strictOrWarn(unresolvableMessage);
1214
1963
  const staleDistMessage = staleDistIncludesMessage(collected.outcomes);
1215
- if (staleDistMessage !== null) {
1216
- if (this.options.strict) this.strictFailure(staleDistMessage);
1217
- else this.warn(staleDistMessage);
1218
- }
1964
+ if (staleDistMessage !== null) this.strictOrWarn(staleDistMessage);
1219
1965
  const rejectedSpecifiers = /* @__PURE__ */ new Set();
1220
1966
  const crossVolumeDetails = [];
1221
1967
  for (const [dir, specifiers] of Object.entries(collected.dirOwnerSets)) {
@@ -1232,6 +1978,7 @@ var ExtractionSession = class {
1232
1978
  for (const message of this.stickyDiagnostics.values()) this.warn(message);
1233
1979
  const admitted = excludeCollectedPackages(collected, rejectedSpecifiers, rootDir);
1234
1980
  this.kitDescriptors = admitted.kitDescriptors;
1981
+ this.packageInstances = admitted.packageInstances;
1235
1982
  try {
1236
1983
  const identity = createSourceIdentity(rootDir);
1237
1984
  this.externalRootOwners = /* @__PURE__ */ new Map();
@@ -1267,43 +2014,62 @@ var ExtractionSession = class {
1267
2014
  this.externalSourceEntries = admitted.sourceEntries;
1268
2015
  this.externalSourceSideEffects = admitted.sourceEntrySideEffects;
1269
2016
  for (const entry of admitted.entries) {
1270
- const hash = contentHash(entry.source);
2017
+ const entryPath = resolve(rootDir, entry.path);
2018
+ readPaths.add(entryPath);
2019
+ const hostSource = hostSources.get(entryPath);
2020
+ if (hostSource !== void 0) supplied.add(entry.path);
2021
+ const source = hostSource ?? entry.source;
1271
2022
  rawEntries.push({
1272
2023
  path: entry.path,
1273
- source: entry.source,
1274
- hash
2024
+ source,
2025
+ hash: contentHash(source)
1275
2026
  });
1276
2027
  }
1277
2028
  this.externalPackageDirs = admitted.packageDirs;
1278
2029
  this.externalLinkedDirs = admitted.linkedDirs;
1279
2030
  this.externalOutputDirs = admitted.outputDirs;
1280
2031
  bt.packageResolve = this.elapsed(t);
2032
+ const misspelled = misspelledHostSources(hostSources, readPaths).map(({ path, read }) => `${relative(rootDir, path)} (read as ${read.map((readPath) => relative(rootDir, readPath)).join(", ")})`);
2033
+ if (misspelled.length > 0) throw new DeferredReloadRefusal(`[${this.driverLabel}] reload not published: the host supplied bytes for ${misspelled.join(", ")}, which the full read reaches only under another spelling; the last generation stays served`);
1281
2034
  this.beginStatusAttempt();
1282
2035
  let accepted;
2036
+ const readEntries = this.withoutOutputSources(rawEntries);
1283
2037
  try {
1284
- accepted = await this.corpus.prepare(rawEntries);
2038
+ accepted = await this.corpus.prepare(readEntries);
1285
2039
  this.throwOnAbortedParse(accepted);
1286
2040
  this.skippedSources = accepted.skippedOriginals ?? [];
2041
+ this.ingestionOpenings = accepted.openings ?? [];
2042
+ this.ingestionAdapted = adaptedOriginals(accepted.ownership);
1287
2043
  } catch (err) {
2044
+ if (err instanceof AbortedParseRejection) this.holdObservedEntries(readEntries, supplied);
1288
2045
  this.debouncePending.clear();
1289
2046
  this.writeAnalysisStatus("failed", pending, String(err));
1290
2047
  throw err;
1291
2048
  }
1292
2049
  this.externalFileOwners = projectExternalFileOwners(accepted, this.externalFileOwners);
1293
2050
  bt.fileCount = accepted.analysisEntries.length;
1294
- clearEngineCache(engineApi);
2051
+ clearEngineCache(() => this.engine());
1295
2052
  await this.analyzeAndEmit(accepted.analysisEntries, packageMap, false, bt, pipelineStart, pending);
1296
- this.publishSourceIngestion(accepted);
1297
- setSharedExternalDirs(admitted.packageDirs);
1298
- setSharedExternalEntries(admitted.sourceEntries);
1299
- setSharedExternalSideEffects(admitted.sourceEntrySideEffects);
2053
+ this.publishSourceIngestion(accepted, "full", (path) => rawEntries.find((entry) => entry.path === path));
2054
+ this.hostSuppliedKeys = supplied;
1300
2055
  this.onExternalRootsCommitted?.(this.externalWatchRoots);
1301
- setOwningWatchSession(this);
2056
+ setOwningWatchSession(this.key, this);
1302
2057
  } catch (err) {
1303
2058
  this.restoreSourceState(priorSourceState);
1304
2059
  throw err;
1305
2060
  }
1306
2061
  }
2062
+ /** The full pass's analysis inputs, less any file whose real path lies in
2063
+ * an output directory, each reported. */
2064
+ withoutOutputSources(entries) {
2065
+ const inOutput = outputSourceGuard(this.rootDir, this.structuralExclude);
2066
+ return entries.filter((entry) => {
2067
+ const outputDir = inOutput(resolve(this.rootDir, entry.path));
2068
+ if (outputDir === null) return true;
2069
+ this.warn(outputSourceMessage(entry.path, outputDir));
2070
+ return false;
2071
+ });
2072
+ }
1307
2073
  /** Per-generation source state captured as ONE object so the restore
1308
2074
  * cannot omit a member; deep-copied fields are mutated in place. */
1309
2075
  snapshotSourceState() {
@@ -1325,16 +2091,93 @@ var ExtractionSession = class {
1325
2091
  restoreSourceState(snapshot) {
1326
2092
  Object.assign(this, snapshot);
1327
2093
  }
1328
- /** Release this session's publication claim so a successor may publish.
1329
- * Idempotent, and scoped to this session's own claim. */
2094
+ /** The system under `context`: the one load path for the build's
2095
+ * authority and for a participant checking its own context. */
2096
+ loadSystemUnder(context) {
2097
+ return loadSystemConfig(() => this.engine(), {
2098
+ systemPath: resolve(this.rootDir, this.options.system),
2099
+ rootDir: this.rootDir,
2100
+ prefix: this.options.prefix,
2101
+ prefixContextualVars: this.options.prefixContextualVars,
2102
+ conditions: context.conditions,
2103
+ workspaces: context.workspaces,
2104
+ extensions: context.extensions
2105
+ });
2106
+ }
2107
+ /** What the system loader reads of this participant besides the root, as
2108
+ * stable text: its conditions and extension order. */
2109
+ resolutionContextText() {
2110
+ return JSON.stringify([this.conditions, extensionPolicyJson(this.extensionPolicy)]);
2111
+ }
2112
+ resolutionContextLabel() {
2113
+ return `${this.driverLabel} with conditions [${this.conditions.join(", ")}] and extension order ${this.extensionPolicy.resolverVersion}`;
2114
+ }
2115
+ /** One agreed system authority (SA5): a participant whose resolution
2116
+ * context differs from the one the build's system was loaded under loads
2117
+ * it once under its own, and proceeds only on the same system. Otherwise
2118
+ * it throws, naming both contexts, whichever participant came first. */
2119
+ agreeWithSystemAuthority() {
2120
+ const authority = getSystemAuthority(this.key);
2121
+ if (authority === null) return;
2122
+ const context = this.resolutionContextText();
2123
+ if (context === authority.context) return;
2124
+ let own = authority.others.get(context);
2125
+ if (own === void 0) {
2126
+ own = systemLoad(this.loadSystemUnder({
2127
+ conditions: this.conditions,
2128
+ workspaces: declaredWorkspacePackages(this.rootDir),
2129
+ extensions: this.extensionPolicy
2130
+ }));
2131
+ authority.others.set(context, own);
2132
+ }
2133
+ if (own.identity === authority.identity) return;
2134
+ const shared = new Set(authority.dependencies);
2135
+ const mine = new Set(own.dependencies);
2136
+ const differing = [...authority.dependencies.filter((path) => !mine.has(path)), ...own.dependencies.filter((path) => !shared.has(path))].map((path) => relative(this.rootDir, path));
2137
+ throw new SessionConfigurationConflict(this.key, authority.label, this.resolutionContextLabel(), differing.length > 0 ? [`system modules ${differing.join(", ")}`] : ["the evaluated system"], "Every participant of one build must evaluate the same system: give each compiler conditions that select the same system modules.");
2138
+ }
2139
+ /** Release this session's publication claim so a successor may publish,
2140
+ * and leave the build, which retires with its last participant.
2141
+ * Idempotent; `reattach` joins the build again. */
1330
2142
  close() {
1331
2143
  this.releasePublicationClaim?.();
1332
2144
  this.releasePublicationClaim = null;
2145
+ this.hostSuppliedKeys.clear();
2146
+ if (this.joinedKey === null || this.participant === null) return;
2147
+ if (getOwningWatchSession(this.joinedKey) === this) setOwningWatchSession(this.joinedKey, null);
2148
+ leaveSession(this.joinedKey, this.participant);
2149
+ this.participant = null;
2150
+ this.detached = true;
2151
+ }
2152
+ /** A closed participant attaching again: its private analysis is stale,
2153
+ * so it is discarded, and it joins its build again. Idempotent. */
2154
+ reattach() {
2155
+ if (!this.detached) return;
2156
+ this.detached = false;
2157
+ this.system = null;
2158
+ this.joinedKey = null;
2159
+ }
2160
+ /** Brings a reattached participant up to date with a full rescan of a
2161
+ * live build, which reconciles deletions too; true then. A retired build
2162
+ * is false: the host starts it as at first. */
2163
+ async catchUp() {
2164
+ if (getManifestJson(this.key) === null) return false;
2165
+ await this.awaitObservation(this.recordRescan());
2166
+ return true;
2167
+ }
2168
+ /** Queues a full rescan under a new revision. It is never equivalent to
2169
+ * an active attempt: only a full read covers it. */
2170
+ recordRescan() {
2171
+ const queue = observationQueue(this.key);
2172
+ queue.rescan = true;
2173
+ const revision = ++queue.revision;
2174
+ queue.revisions.set(revision, null);
2175
+ return revision;
1333
2176
  }
1334
2177
  /** Reset analysis state for a system reload: write guards go back to null
1335
2178
  * and reseed from the disk envelopes on the next publication. */
1336
2179
  resetForHmr() {
1337
- resetAnalysisStartedPromise();
2180
+ resetAnalysisStartedPromise(this.key);
1338
2181
  this.artifactRecords = {
1339
2182
  manifest: null,
1340
2183
  inputs: null,
@@ -1345,7 +2188,7 @@ var ExtractionSession = class {
1345
2188
  /** The (sourceKey, observed hash) pairs of a watch batch — the status
1346
2189
  * file's pending set; loaders wait only on observed inputs. */
1347
2190
  pendingFromBatch(changes) {
1348
- const { excludeMatcher, extensionsSet } = this.resolveScanConfig();
2191
+ const { excludeMatcher, extensionsSet } = this.scanConfig;
1349
2192
  const pending = [];
1350
2193
  for (const path of changes.modifiedFiles ?? []) {
1351
2194
  const classified = this.classifyWatchPath(path, {
@@ -1354,7 +2197,7 @@ var ExtractionSession = class {
1354
2197
  });
1355
2198
  if (!classified) continue;
1356
2199
  try {
1357
- pending.push([classified.key, contentHash(readFileSync(path, "utf-8"))]);
2200
+ pending.push([classified.key, contentHash(changes.sources?.get(path) ?? readFileSync(path, "utf-8"))]);
1358
2201
  } catch {}
1359
2202
  }
1360
2203
  return pending;
@@ -1366,7 +2209,7 @@ var ExtractionSession = class {
1366
2209
  if (!identity) return changes;
1367
2210
  if (!changes.modifiedFiles && !changes.removedFiles) return changes;
1368
2211
  if (this.externalWatchRoots.length === 0) return changes;
1369
- const { extensionsSet } = this.resolveScanConfig();
2212
+ const { extensionsSet } = this.scanConfig;
1370
2213
  const dirtyRoots = /* @__PURE__ */ new Set();
1371
2214
  const modified = /* @__PURE__ */ new Set();
1372
2215
  const removed = /* @__PURE__ */ new Set();
@@ -1404,13 +2247,14 @@ var ExtractionSession = class {
1404
2247
  } catch {
1405
2248
  continue;
1406
2249
  }
1407
- if (previous.get(resolved.sourceKey)?.hash !== rawHash) modified.add(abs);
2250
+ if (previous.get(resolved.sourceKey)?.hash !== rawHash || this.ingestionFailureDiagnostics.some((diagnostic) => diagnostic.file === resolved.sourceKey)) modified.add(abs);
1408
2251
  }
1409
2252
  for (const [key, entry] of previous) if (!seen.has(key)) removed.add(entry.abs);
1410
2253
  }
1411
2254
  return {
1412
2255
  modifiedFiles: modified,
1413
- removedFiles: removed
2256
+ removedFiles: removed,
2257
+ sources: changes.sources
1414
2258
  };
1415
2259
  }
1416
2260
  /** Route one watcher path: a project-root member (consumer excludes apply)
@@ -1477,33 +2321,40 @@ var ExtractionSession = class {
1477
2321
  packageMap,
1478
2322
  system,
1479
2323
  emitter: {
1480
- runtimeImport: "@animus-ui/system/runtime",
1481
- cssModuleId: ANIMUS_CSS_MODULE_ID,
1482
- systemPropsModuleId: this.systemPropsModuleId ?? systemPropsPath(this.sessionDir)
2324
+ runtimeImport: this.runtimeImport,
2325
+ cssModuleId: this.delivery.cssModuleId,
2326
+ systemPropsModuleId: this.delivery.systemPropsModuleId === SESSION_ARTIFACT ? systemPropsPath(this.sessionDir) : this.delivery.systemPropsModuleId
1483
2327
  },
1484
2328
  pathAliasesJson: this.pathAliasesJson,
1485
2329
  staticCssJson: this.staticCssJson,
1486
2330
  externalDirs: this.externalPackageDirs.map((dir) => relative(this.rootDir, dir)),
2331
+ rootDir: this.rootDir,
2332
+ assets: this.assetClassification,
2333
+ adapted: this.ingestionAdapted,
2334
+ loadExpander: this.loadExpander,
1487
2335
  devMode: this.engineDevMode(devMode),
1488
2336
  analysisContext: {
1489
2337
  skippedSources: [...this.skippedSources, ...this.ingestionFailureDiagnostics.map((d) => d.file)].map((file) => isAbsolute(file) ? relative(this.rootDir, file) : file),
1490
2338
  unbundledComputedImports: this.host.unbundledComputedImports,
2339
+ openings: [...this.ingestionOpenings],
1491
2340
  packageDirs: this.externalPackageDirs.map((dir) => relative(this.rootDir, dir)),
1492
2341
  linkedDirs: this.externalLinkedDirs.map((dir) => relative(this.rootDir, dir)),
1493
- outputDirs: this.externalOutputDirs.map((dir) => relative(this.rootDir, dir))
2342
+ outputDirs: this.externalOutputDirs.map((dir) => relative(this.rootDir, dir)),
2343
+ extensions: this.extensionPolicy
1494
2344
  }
1495
2345
  };
1496
2346
  this.writeAnalysisStatus("analyzing", pending);
1497
- const result = runProjectAnalysis(engineApi, {
2347
+ const result = runProjectAnalysis(() => this.engine(), {
1498
2348
  ...analysisOptions,
1499
2349
  warn: (message) => this.warn(message),
1500
2350
  info: (message) => this.log(message),
1501
2351
  strict: this.options.strict,
1502
2352
  diagnostics: this.options.diagnostics,
1503
2353
  reportErrors: this.reportErrors(),
1504
- extraDiagnostics: this.ingestionFailureDiagnostics,
2354
+ extraDiagnostics: [...this.ingestionFailureDiagnostics, ...await this.compareAliasesWithHost(fileEntries)],
1505
2355
  kitDescriptors: this.kitDescriptors
1506
2356
  });
2357
+ if (result.manifest) assertRuntimeImportSuppliesTerminals(this.runtimeImport, result.manifest);
1507
2358
  enforceExternalTokenContracts({
1508
2359
  diagnostics: result.manifest?.diagnostics,
1509
2360
  fileOwners: this.externalFileOwners,
@@ -1538,12 +2389,12 @@ var ExtractionSession = class {
1538
2389
  declaration,
1539
2390
  variables,
1540
2391
  postProcessCss(body, {
1541
- minify: this.options.minify ?? resolveMode(this.options.mode, () => process.env.NODE_ENV === "production" ? "production" : "development").mode === "production",
2392
+ minify: this.options.minify ?? this.startupMode === "production",
1542
2393
  targets: this.lcssTargets,
1543
2394
  warnFn: (msg) => this.warn(msg)
1544
2395
  })
1545
2396
  ].filter(Boolean).join("\n");
1546
- reportSurvivingAssetPlaceholders(fullCss, {
2397
+ if (!this.hostSubstitutesAssets) reportSurvivingAssetPlaceholders(fullCss, {
1547
2398
  strict: this.options.strict,
1548
2399
  warn: (message) => this.warn(message),
1549
2400
  reportErrors: this.reportErrors(),
@@ -1556,7 +2407,6 @@ var ExtractionSession = class {
1556
2407
  prefix: `[${this.driverLabel}]`,
1557
2408
  surface: "generated runtime modules"
1558
2409
  });
1559
- setSharedCss(fullCss);
1560
2410
  const systemPropsContent = buildSystemPropsModule({
1561
2411
  systemPropMapJson: JSON.stringify(manifest?.system_prop_map ?? {}),
1562
2412
  groupRegistryJson: system.groupRegistryJson,
@@ -1564,9 +2414,6 @@ var ExtractionSession = class {
1564
2414
  admittedTransforms: manifest?.admitted_transforms ?? {},
1565
2415
  typedSystemProps: manifest?.typed_system_props ?? []
1566
2416
  });
1567
- setSharedSystemProps(systemPropsContent);
1568
- setManifestJson(result.manifestJson);
1569
- setAnalyzedHashes(new Map(fileEntries.map((entry) => [entry.path, entry.hash])));
1570
2417
  this.writeAnalysisStatus("committing", pending);
1571
2418
  const epoch = hashReplacementPlans(snapshotFilePlans(manifest), systemPropsContent);
1572
2419
  if (this.lastCommit === null) this.lastCommit = this.seedCommitFromDisk();
@@ -1586,6 +2433,19 @@ var ExtractionSession = class {
1586
2433
  } else this.writeSessionArtifact(SYSTEM_PROPS_ARTIFACT, systemPropsContent);
1587
2434
  this.publishAnalysisCommit(epoch, generation);
1588
2435
  this.publishReplacementEpoch(epoch);
2436
+ this.stagedPublication = {
2437
+ manifest,
2438
+ manifestJson: result.manifestJson,
2439
+ analysisGlobalCss: sheets.globalCss,
2440
+ analysisComponentCss: sheets.componentCss,
2441
+ styles: fullCss,
2442
+ systemPropsModule: systemPropsContent,
2443
+ system,
2444
+ packageMap,
2445
+ analyzedHashes: new Map(fileEntries.map((entry) => [entry.path, entry.hash])),
2446
+ packageReadRecord: this.packageReadRecord,
2447
+ replacementEpoch: epoch
2448
+ };
1589
2449
  bt.total = this.elapsed(pipelineStart);
1590
2450
  logBuildTimings(bt, manifest?.timing, (msg) => this.log(msg), this.verbose);
1591
2451
  }
@@ -1599,6 +2459,86 @@ var ExtractionSession = class {
1599
2459
  }
1600
2460
  /** Write the session's analysis-status artifact. Terminal states
1601
2461
  * (idle/failed) close the attempt. */
2462
+ /** Where the host resolves a path alias the analysis reads to another
2463
+ * file (spec §2.2 A7, report only), and how the two were compared. */
2464
+ async compareAliasesWithHost(fileEntries) {
2465
+ const record = packageReadRecord({
2466
+ rootDir: this.rootDir,
2467
+ roots: this.packageInstances,
2468
+ redirected: new Set(this.externalSourceEntries.keys()),
2469
+ analysed: fileEntries.map((entry) => entry.path),
2470
+ rendersBuilder: (file) => this.rendersBuilder(file)
2471
+ });
2472
+ this.packageReadRecord = record;
2473
+ const { moduleSpecifiers } = this.engine();
2474
+ if (this.host.noBundler) return [];
2475
+ if (this.hostResolve === null || moduleSpecifiers === void 0) {
2476
+ this.log("path aliases: no host resolver, comparison unavailable");
2477
+ return [];
2478
+ }
2479
+ try {
2480
+ const { findings, unavailable } = await compareAliases({
2481
+ rootDir: this.rootDir,
2482
+ fileEntries,
2483
+ pathAliasesJson: this.pathAliasesJson,
2484
+ order: this.extensionPolicy,
2485
+ readings: {
2486
+ isAsset: this.assetClassification.isAsset,
2487
+ adapted: this.ingestionAdapted
2488
+ },
2489
+ moduleSpecifiers,
2490
+ resolve: this.hostResolve,
2491
+ cache: this.aliasCache,
2492
+ packages: record
2493
+ });
2494
+ for (const finding of findings) record.reported.add(`${finding.file}\0${finding.component}`);
2495
+ if (unavailable > 0) this.log(`path aliases: ${unavailable} aliased import(s) the host's resolver failed on are unavailable, not agreeing`);
2496
+ return findings;
2497
+ } catch (error) {
2498
+ this.log(`path aliases: comparison unavailable (${String(error)})`);
2499
+ return [];
2500
+ }
2501
+ }
2502
+ /** Whether the file at a real path declares builder chains, by the
2503
+ * engine's own parse: a module that does, served unread, renders with
2504
+ * empty classes. One the engine cannot read counts as one that does. */
2505
+ rendersBuilder(file) {
2506
+ if (!isEngineTransformExtension(file)) return false;
2507
+ const { extractFacts } = this.engine();
2508
+ if (extractFacts === void 0) return true;
2509
+ const path = relative(this.rootDir, file);
2510
+ try {
2511
+ const facts = parseInternalWire(extractFacts(JSON.stringify([{
2512
+ path,
2513
+ source: readFileSync(file, "utf-8")
2514
+ }])), "extractFacts").files[path];
2515
+ return facts === void 0 || facts.parsePanicked || facts.chains.length > 0;
2516
+ } catch {
2517
+ return true;
2518
+ }
2519
+ }
2520
+ /**
2521
+ * An import the host resolved while bundling (spec §2.2 A4), reported as
2522
+ * the comparison reports it: webpack resolves each module's requests with
2523
+ * the resolve options of the rules that module matches, which its
2524
+ * resolver, asked outside a compilation, does not apply. `importer` and
2525
+ * `resolved` are absolute. A finding the comparison already reported is
2526
+ * not repeated; an error-level one throws in a build.
2527
+ */
2528
+ reportHostResolution(importer, request, resolved) {
2529
+ const record = getSharedPackageReadRecord(this.key);
2530
+ if (record === null) return;
2531
+ const finding = packageInstanceFinding(record, relative(record.rootDir, importer).split(sep).join(posix.sep), request, resolved);
2532
+ if (finding === null) return;
2533
+ const key = `${finding.file}\0${finding.component}`;
2534
+ if (record.reported.has(key)) return;
2535
+ record.reported.add(key);
2536
+ surfaceManifestDiagnostics({ diagnostics: [finding] }, (message) => this.warn(message), {
2537
+ levels: this.options.diagnostics,
2538
+ strict: this.options.strict,
2539
+ reportErrors: this.reportErrors()
2540
+ });
2541
+ }
1602
2542
  writeAnalysisStatus(state, pending, diagnostic) {
1603
2543
  const status = {
1604
2544
  schema: 2,
@@ -1708,6 +2648,10 @@ var ExtractionSession = class {
1708
2648
  /** Maintain the session's epoch witness, rewritten ONLY when the value
1709
2649
  * changes, then expose it through the singleton. */
1710
2650
  publishReplacementEpoch(epoch) {
2651
+ if (!this.persistArtifacts) {
2652
+ this.lastEpochValue = epoch;
2653
+ return;
2654
+ }
1711
2655
  const onDisk = this.diskEpochValue();
1712
2656
  if (this.lastEpochValue === null) this.lastEpochValue = onDisk;
1713
2657
  if (epoch !== onDisk) this.writeSessionArtifact(REPLACEMENT_EPOCH_ARTIFACT, JSON.stringify({
@@ -1719,7 +2663,6 @@ var ExtractionSession = class {
1719
2663
  this.lastEpochValue = epoch;
1720
2664
  this.reconcileSiblingEpochs(epoch);
1721
2665
  }
1722
- setReplacementEpoch(epoch);
1723
2666
  }
1724
2667
  /** sessions-root listing memo, keyed by the root dir's mtime — a new or
1725
2668
  * pruned sibling DIRECTORY moves it; agreeing siblings stay listed. */
@@ -1764,10 +2707,14 @@ var ExtractionSession = class {
1764
2707
  }
1765
2708
  }
1766
2709
  /** The pid of `siblingDir`'s live owner, or null when nothing proves one.
1767
- * Our own pid is never one — publication is process-exclusive. A missing,
1768
- * undecodable or unreadable claim all mean no owner: the opposite default
1769
- * from the CLI lock check on purpose, so do not unify the two. */
2710
+ * A tree whose build is registered in this process is ours and live,
2711
+ * claim or no claim: a publisher's close() releases the claim while the
2712
+ * build's other participants still serve from it. Otherwise a missing,
2713
+ * undecodable or unreadable claim, or one naming our own pid, means no
2714
+ * owner: the opposite default from the CLI lock check on purpose, so do
2715
+ * not unify the two. */
1770
2716
  liveSiblingOwnerPid(siblingDir) {
2717
+ if (findSession(sessionKey(basename(siblingDir), this.rootDir)) !== void 0) return process.pid;
1771
2718
  let claim;
1772
2719
  try {
1773
2720
  claim = readCliLockRecord(siblingDir);
@@ -1798,7 +2745,7 @@ var ExtractionSession = class {
1798
2745
  /** Delete legacy flat artifacts and prune sibling session directories past
1799
2746
  * the retention window — never this session's own dir. */
1800
2747
  runSessionStartHygiene() {
1801
- if (this.sessionStartHygieneDone) return;
2748
+ if (this.sessionStartHygieneDone || !this.persistArtifacts) return;
1802
2749
  this.sessionStartHygieneDone = true;
1803
2750
  const animusDir = join(this.rootDir, ANIMUS_ARTIFACT_DIR);
1804
2751
  const isCliPublishedSet = this.cliWriterHoldsLock(animusDir) || verifyCommitRecord(animusDir).length === 0;
@@ -1839,13 +2786,29 @@ var ExtractionSession = class {
1839
2786
  /** Write one artifact write-then-rename, so a cross-process reader never
1840
2787
  * sees a torn file; the tmp name carries the pid to keep writers apart. */
1841
2788
  writeSessionArtifact(name, content) {
1842
- const dir = this.sessionDir;
1843
- mkdirSync(dir, { recursive: true });
1844
- const tmpPath = join(dir, `.${name}.${process.pid}.tmp`);
1845
- writeFileSync(tmpPath, content);
1846
- renameSync(tmpPath, join(dir, name));
2789
+ if (this.persistArtifacts) {
2790
+ const dir = this.sessionDir;
2791
+ mkdirSync(dir, { recursive: true });
2792
+ const tmpPath = join(dir, `.${name}.${process.pid}.tmp`);
2793
+ writeFileSync(tmpPath, content);
2794
+ renameSync(tmpPath, join(dir, name));
2795
+ }
1847
2796
  this.onArtifactWrite?.(name, content);
1848
2797
  }
2798
+ /** How an asset's bytes are fingerprinted, of a file or of bytes read:
2799
+ * the engine's fast fingerprint where it has both, the content hash
2800
+ * otherwise, so the two always agree. */
2801
+ assetFingerprints() {
2802
+ const { fingerprintFile, fingerprintBytes } = this.engine();
2803
+ if (fingerprintFile && fingerprintBytes) return {
2804
+ ofFile: (path) => fingerprintFile(path) ?? contentHash(readFileSync(path)),
2805
+ ofBytes: fingerprintBytes
2806
+ };
2807
+ return {
2808
+ ofFile: (path) => contentHash(readFileSync(path)),
2809
+ ofBytes: contentHash
2810
+ };
2811
+ }
1849
2812
  /** Copy each asset() specifier's bytes, from every sheet, into `assets/`
1850
2813
  * under a content-hashed name and substitute a url relative to
1851
2814
  * styles.css. */
@@ -1855,12 +2818,12 @@ var ExtractionSession = class {
1855
2818
  const expected = /* @__PURE__ */ new Set();
1856
2819
  this.assetDependencyPaths.clear();
1857
2820
  this.assetDependencyKeys.clear();
2821
+ if (this.hostSubstitutesAssets) return sheets;
1858
2822
  const urlBySpecifier = /* @__PURE__ */ new Map();
1859
2823
  for (const specifier of specifiers) {
1860
2824
  const cached = this.assetCopyCache.get(specifier);
1861
2825
  if (cached && existsSync(join(assetsDir, cached.fileName))) try {
1862
- const current = statSync(cached.sourcePath);
1863
- if (current.mtimeMs === cached.mtimeMs && current.size === cached.size) {
2826
+ if (this.assetFingerprints().ofFile(cached.sourcePath) === cached.fingerprint) {
1864
2827
  this.trackAssetDependency(cached.sourcePath);
1865
2828
  expected.add(cached.fileName);
1866
2829
  urlBySpecifier.set(specifier, cached.url);
@@ -1869,27 +2832,18 @@ var ExtractionSession = class {
1869
2832
  } catch {}
1870
2833
  const resolvedPath = this.resolveAssetSpecifier(specifier);
1871
2834
  if (!resolvedPath) {
1872
- const message = `unresolvable asset() specifier: ${specifier}`;
1873
- if (this.options.strict) this.strictFailure(`[${this.driverLabel}] ${message}`);
1874
- else this.warn(message);
2835
+ this.reportUnresolvableAsset(specifier);
1875
2836
  urlBySpecifier.set(specifier, specifier);
1876
2837
  continue;
1877
2838
  }
1878
- const bytes = readFileSync(resolvedPath);
1879
- const sourceStat = statSync(resolvedPath);
2839
+ const { fileName, bytes } = writeAssetCopy(resolvedPath, assetsDir);
1880
2840
  this.trackAssetDependency(resolvedPath);
1881
- const ext = extname(resolvedPath);
1882
- const fileName = `${basename(resolvedPath, ext)}.${contentHash(bytes).slice(0, 8)}${ext}`;
1883
2841
  expected.add(fileName);
1884
- if (!existsSync(assetsDir)) mkdirSync(assetsDir, { recursive: true });
1885
- const assetPath = join(assetsDir, fileName);
1886
- if (!existsSync(assetPath)) writeFileSync(assetPath, bytes);
1887
2842
  const url = `./${SESSION_ASSETS_DIR}/${fileName}`;
1888
2843
  urlBySpecifier.set(specifier, url);
1889
2844
  this.assetCopyCache.set(specifier, {
1890
2845
  sourcePath: resolvedPath,
1891
- mtimeMs: sourceStat.mtimeMs,
1892
- size: sourceStat.size,
2846
+ fingerprint: this.assetFingerprints().ofBytes(bytes),
1893
2847
  fileName,
1894
2848
  url
1895
2849
  });
@@ -1897,6 +2851,10 @@ var ExtractionSession = class {
1897
2851
  if (pruneSuperseded) pruneStaleAssets(assetsDir, expected);
1898
2852
  return substituteSheetAssets(sheets, urlBySpecifier);
1899
2853
  }
2854
+ reportUnresolvableAsset(specifier) {
2855
+ const message = `unresolvable asset() specifier: ${specifier}`;
2856
+ this.strictOrWarn(message, `[${this.driverLabel}] ${message}`);
2857
+ }
1900
2858
  trackAssetDependency(path) {
1901
2859
  this.assetDependencyPaths.add(path);
1902
2860
  for (const key of toWatchKeys(path)) this.assetDependencyKeys.add(key);
@@ -1918,30 +2876,17 @@ var ExtractionSession = class {
1918
2876
  return null;
1919
2877
  }
1920
2878
  };
1921
- /** Sync `assets/` to the current build's file set; per-entry failures are
1922
- * tolerated — a concurrent session may have removed the same file. */
1923
- function pruneStaleAssets(assetsDir, expected) {
1924
- if (!existsSync(assetsDir)) return;
1925
- let entries;
1926
- try {
1927
- entries = readdirSync(assetsDir);
1928
- } catch {
1929
- return;
1930
- }
1931
- for (const entry of entries) {
1932
- if (expected.has(entry)) continue;
1933
- try {
1934
- unlinkSync(join(assetsDir, entry));
1935
- } catch {}
1936
- }
1937
- }
1938
2879
  //#endregion
1939
2880
  //#region session/turbopack-orchestrator.ts
1940
2881
  async function runSessionPipeline(session) {
1941
- session.systemPropsModuleId = TURBOPACK_SYSTEM_PROPS_ID;
2882
+ session.delivery = {
2883
+ ...session.delivery,
2884
+ systemPropsModuleId: TURBOPACK_SYSTEM_PROPS_ID
2885
+ };
1942
2886
  session.persistAnalysisInputs = true;
1943
2887
  await session.runFullPipeline();
1944
2888
  }
2889
+ /** Each build's watched root: another build of the root watches it too. */
1945
2890
  const activeWatcherRoots = /* @__PURE__ */ new Set();
1946
2891
  const IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
1947
2892
  ANIMUS_ARTIFACT_DIR,
@@ -1968,17 +2913,18 @@ function isIntrinsicString(value) {
1968
2913
  }
1969
2914
  function startTurbopackWatcher(session, rootDir, options = {}) {
1970
2915
  const { debounceMs = 75, watchFn = watch, holdEvents = false } = options;
1971
- if (activeWatcherRoots.has(rootDir)) return { kind: "already-watched" };
1972
- activeWatcherRoots.add(rootDir);
2916
+ const watched = JSON.stringify([session.sessionId, rootDir]);
2917
+ if (activeWatcherRoots.has(watched)) return { kind: "already-watched" };
2918
+ activeWatcherRoots.add(watched);
1973
2919
  session.debounceCeilingMs = debounceMs;
1974
2920
  const pendingPaths = /* @__PURE__ */ new Set();
1975
2921
  const watchers = /* @__PURE__ */ new Map();
1976
- let timer = null;
1977
2922
  let updateChain = Promise.resolve();
1978
2923
  let closed = false;
1979
2924
  let died = false;
1980
2925
  let holdingEvents = holdEvents;
1981
2926
  const externalWatchers = /* @__PURE__ */ new Map();
2927
+ const dependencyWatchers = /* @__PURE__ */ new Map();
1982
2928
  let pendingOpened = /* @__PURE__ */ new Map();
1983
2929
  let capturedDuringSnapshot = [];
1984
2930
  const externalDiagnosticKey = (root) => `external-watch:${root}`;
@@ -1997,24 +2943,28 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
1997
2943
  session.stickyDiagnostics?.set(externalDiagnosticKey(root), message);
1998
2944
  console.warn(`[animus-extract] ${message}`);
1999
2945
  };
2946
+ let cyclesInFlight = 0;
2000
2947
  const flush = () => {
2001
- timer = null;
2002
2948
  const modifiedFiles = /* @__PURE__ */ new Set();
2003
2949
  const removedFiles = /* @__PURE__ */ new Set();
2004
2950
  for (const path of pendingPaths) (existsSync(path) ? modifiedFiles : removedFiles).add(path);
2005
2951
  pendingPaths.clear();
2006
- updateChain = updateChain.then(() => {
2007
- if (closed) return;
2008
- return session.handleWatchUpdate({
2009
- modifiedFiles,
2010
- removedFiles
2011
- }).catch((err) => {
2012
- console.warn(`[animus-extract] watch update failed: ${String(err)}`);
2013
- }).then(() => {
2014
- if (pendingOpened.size > 0 || capturedDuringSnapshot.length > 0) rollbackPendingExternal();
2015
- });
2952
+ if (closed) return;
2953
+ cyclesInFlight++;
2954
+ const cycle = session.handleWatchUpdate({
2955
+ modifiedFiles,
2956
+ removedFiles
2957
+ }).catch((err) => {
2958
+ console.warn(`[animus-extract] watch update failed: ${String(err)}`);
2959
+ }).finally(() => {
2960
+ cyclesInFlight--;
2961
+ if (cyclesInFlight > 0) return;
2962
+ if (pendingOpened.size > 0 || capturedDuringSnapshot.length > 0) rollbackPendingExternal();
2963
+ syncDependencyWatchers();
2016
2964
  });
2965
+ updateChain = updateChain.then(() => cycle);
2017
2966
  };
2967
+ const coalescing = new CoalescingWindow(flush, debounceMs);
2018
2968
  const rollbackPendingExternal = () => {
2019
2969
  for (const watcher of pendingOpened.values()) watcher.close();
2020
2970
  pendingOpened = /* @__PURE__ */ new Map();
@@ -2023,16 +2973,17 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
2023
2973
  const closeAll = () => {
2024
2974
  if (closed) return;
2025
2975
  closed = true;
2026
- if (timer) clearTimeout(timer);
2027
- timer = null;
2976
+ coalescing.cancel();
2028
2977
  for (const watcher of watchers.values()) watcher.close();
2029
2978
  watchers.clear();
2979
+ for (const { watcher } of dependencyWatchers.values()) watcher.close();
2980
+ dependencyWatchers.clear();
2030
2981
  for (const watcher of externalWatchers.values()) watcher.close();
2031
2982
  externalWatchers.clear();
2032
2983
  rollbackPendingExternal();
2033
2984
  session.onExternalRootResolved = null;
2034
2985
  session.onExternalRootsCommitted = null;
2035
- activeWatcherRoots.delete(rootDir);
2986
+ activeWatcherRoots.delete(watched);
2036
2987
  };
2037
2988
  const onWatcherError = (err) => {
2038
2989
  died = true;
@@ -2048,10 +2999,7 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
2048
2999
  } catch (err) {
2049
3000
  console.warn(`[animus-extract] Turbopack debounce status update failed: ${String(err)}`);
2050
3001
  }
2051
- if (!timer) {
2052
- timer = setTimeout(flush, debounceMs);
2053
- timer.unref?.();
2054
- }
3002
+ coalescing.note();
2055
3003
  };
2056
3004
  const onEvent = (baseDir, filename) => {
2057
3005
  if (closed || !filename) return;
@@ -2095,6 +3043,57 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
2095
3043
  degradeExternalRoot(root, err);
2096
3044
  }
2097
3045
  };
3046
+ /** Whether the root or an external root watcher already reports `path`. */
3047
+ const watchedByRoot = (path) => {
3048
+ if (isPathWithinRoot(rootDir, path) && !relative(rootDir, path).split(/[\\/]/).some((segment) => IGNORED_SEGMENTS.has(segment))) return true;
3049
+ return [...externalWatchers.keys()].some((root) => isPathWithinRoot(root, path));
3050
+ };
3051
+ /** Watches the Session's plan (its files, and the absent paths whose
3052
+ * creation reloads the system) where no root watcher reports them, by
3053
+ * their directories, for those paths only. */
3054
+ const syncDependencyWatchers = () => {
3055
+ if (closed) return;
3056
+ const wanted = /* @__PURE__ */ new Map();
3057
+ const plan = session.watchPlan();
3058
+ for (const path of [...plan.files, ...plan.missing]) {
3059
+ if (watchedByRoot(path)) continue;
3060
+ const files = wanted.get(dirname(path)) ?? /* @__PURE__ */ new Set();
3061
+ files.add(path);
3062
+ wanted.set(dirname(path), files);
3063
+ }
3064
+ for (const [dir, { watcher }] of [...dependencyWatchers]) {
3065
+ if (wanted.has(dir)) continue;
3066
+ watcher.close();
3067
+ dependencyWatchers.delete(dir);
3068
+ }
3069
+ for (const [dir, files] of wanted) {
3070
+ const existing = dependencyWatchers.get(dir);
3071
+ if (existing) {
3072
+ existing.files = files;
3073
+ continue;
3074
+ }
3075
+ try {
3076
+ const watcher = watchFn(dir, { recursive: false }, (_event, name) => {
3077
+ if (closed || !name) return;
3078
+ const abs = join(dir, name.toString());
3079
+ if (dependencyWatchers.get(dir)?.files.has(abs)) enqueuePath(abs);
3080
+ });
3081
+ watcher.on("error", (err) => {
3082
+ watcher.close();
3083
+ dependencyWatchers.delete(dir);
3084
+ console.warn(`[animus-extract] dependency watch on ${dir} failed (${String(err)}); its changes require a dev-server restart`);
3085
+ });
3086
+ watcher.unref?.();
3087
+ dependencyWatchers.set(dir, {
3088
+ watcher,
3089
+ files
3090
+ });
3091
+ } catch (err) {
3092
+ if (errnoCode(err) === "ENOENT") continue;
3093
+ console.warn(`[animus-extract] dependency watch on ${dir} unavailable (${String(err)}); its changes require a dev-server restart`);
3094
+ }
3095
+ }
3096
+ };
2098
3097
  const addWatcher = (dir, recursive) => {
2099
3098
  const watcher = watchFn(dir, { recursive }, (_event, filename) => onEvent(dir, filename));
2100
3099
  watcher.on("error", onWatcherError);
@@ -2113,6 +3112,7 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
2113
3112
  return { kind: "unavailable" };
2114
3113
  }
2115
3114
  for (const root of session.externalWatchRoots ?? []) openExternalWatcher(root, externalWatchers);
3115
+ syncDependencyWatchers();
2116
3116
  session.onExternalRootResolved = (root) => {
2117
3117
  if (closed) return;
2118
3118
  if (externalWatchers.has(root) || pendingOpened.has(root)) return;
@@ -2137,7 +3137,8 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
2137
3137
  onDied: null,
2138
3138
  deliverHeldEvents: () => {
2139
3139
  holdingEvents = false;
2140
- if (pendingPaths.size > 0) flush();
3140
+ syncDependencyWatchers();
3141
+ if (pendingPaths.size > 0) coalescing.flush();
2141
3142
  return updateChain;
2142
3143
  },
2143
3144
  settle: () => updateChain
@@ -2148,4 +3149,4 @@ function startTurbopackWatcher(session, rootDir, options = {}) {
2148
3149
  };
2149
3150
  }
2150
3151
  //#endregion
2151
- export { ANIMUS_ARTIFACT_DIR, ANIMUS_CSS_MODULE_ID, CLI_COMMIT_ARTIFACT, CLI_LOCK_ARTIFACT, CLI_LOCK_HEARTBEAT_INTERVAL_MS, CLI_LOCK_STALE_AFTER_MS, ExtractionSession, MANIFEST_ARTIFACT, SESSION_ASSETS_DIR, STYLES_ARTIFACT, SYSTEM_PROPS_ARTIFACT, TURBOPACK_SYSTEM_PROPS_ID, analysisCommitPath, analysisInputsPath, analysisStatusPath, checkLockLiveness, collectSessionAssets, decodeCommitRecord, engineApi, getAnalysisStartedPromise, getAnalyzedHashes, getManifestJson, getReplacementEpoch, getSessionArtifactDir, getSharedCss, getSharedExternalDirs, getSharedExternalEntries, getSharedExternalSideEffects, getSharedSystemProps, holdDirectoryClaim, isLockHolderAlive, lockRecordBytes, manifestPath, pruneStaleAssets, readCliLockRecord, readJsonEnvelope, replacementEpochPath, resetAnalysisStartedPromise, runSessionPipeline, sessionArtifactDir, setAnalysisStartedPromise, setSharedEngine, startTurbopackWatcher, stylesPath, systemPropsPath, verifyCommitRecord };
3152
+ export { ANIMUS_ARTIFACT_DIR, ANIMUS_CSS_MODULE_ID, CLI_COMMIT_ARTIFACT, CLI_LOCK_ARTIFACT, CLI_LOCK_HEARTBEAT_INTERVAL_MS, CLI_LOCK_STALE_AFTER_MS, COALESCING_WINDOW_MS, CoalescingWindow, EnvironmentBuilds, EnvironmentFailure, ExtractionSession, MANIFEST_ARTIFACT, SESSION_ARTIFACT, SESSION_ASSETS_DIR, STYLES_ARTIFACT, SYSTEM_PROPS_ARTIFACT, SessionConfigurationConflict, TURBOPACK_SYSTEM_PROPS_ID, analysisCommitPath, analysisInputsPath, analysisStatusPath, checkLockLiveness, collectSessionAssets, createBuildId, decodeCommitRecord, engineApi, engineFactory, engineHandle, findSession, getAnalysisStartedPromise, getAnalyzedHashes, getManifestJson, getReplacementEpoch, getSessionArtifactDir, getSharedCss, getSharedExternalDirs, getSharedExternalEntries, getSharedExternalSideEffects, getSharedSystemProps, holdDirectoryClaim, isLockHolderAlive, joinSession, lockRecordBytes, manifestPath, pruneStaleAssets, readCliLockRecord, readJsonEnvelope, releaseEngineHandle, replacementEpochPath, requireEngine, resetAnalysisStartedPromise, runSessionPipeline, sessionArtifactDir, sessionKey, setAnalysisStartedPromise, startTurbopackWatcher, stylesPath, systemPropsPath, verifyCommitRecord };