@animus-ui/vite-plugin 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5,45 +5,119 @@ Object.defineProperties(exports, {
5
5
  let _animus_ui_extract_pipeline = require("@animus-ui/extract/pipeline");
6
6
  let path = require("path");
7
7
  let fs = require("fs");
8
+ let _animus_ui_extract_session = require("@animus-ui/extract/session");
9
+ let child_process = require("child_process");
10
+ let vite = require("vite");
8
11
  let crypto = require("crypto");
12
+ //#region src/asset-copy-dirs.ts
13
+ /**
14
+ * Each process's dev asset copies live in their own directory, named by its
15
+ * pid and start time, so a sweep can tell an abandoned directory from a
16
+ * live server's even when the pid has been reused.
17
+ */
18
+ /** A directory left this long is swept unless its owner proves live. */
19
+ const ABANDONED_AFTER_MS = 864e5;
20
+ /** `ps` reports start times to the second. */
21
+ const START_TIME_TOLERANCE_MS = 2e3;
22
+ const processStartMs = Math.round(Date.now() - process.uptime() * 1e3);
23
+ /** This process's copy directory name: `<pid>-<start ms>`. */
24
+ function processCopyDir() {
25
+ return `${process.pid}-${processStartMs}`;
26
+ }
27
+ /** When `pid` started, as `ps` reports it: null where `ps` ran and could
28
+ * not say, `'unavailable'` where there is no `ps` (Windows). */
29
+ function startTimeOf(pid) {
30
+ if (process.platform === "win32") return "unavailable";
31
+ try {
32
+ const started = Date.parse((0, child_process.execFileSync)("ps", [
33
+ "-o",
34
+ "lstart=",
35
+ "-p",
36
+ String(pid)
37
+ ], {
38
+ encoding: "utf-8",
39
+ stdio: [
40
+ "ignore",
41
+ "pipe",
42
+ "ignore"
43
+ ]
44
+ }).trim());
45
+ return Number.isNaN(started) ? null : started;
46
+ } catch (error) {
47
+ if (error?.code === "ENOENT") return "unavailable";
48
+ return null;
49
+ }
50
+ }
51
+ /** Whether the process that named `entry` is still running. A young
52
+ * directory needs only a live pid; one past ABANDONED_AFTER_MS needs its
53
+ * recorded start time to match the running process too. */
54
+ function ownerLive(copyRoot, entry) {
55
+ const match = /^(\d+)-(\d+)$/.exec(entry);
56
+ if (match === null) return false;
57
+ const pid = Number(match[1]);
58
+ const startMs = Number(match[2]);
59
+ if (!(0, _animus_ui_extract_session.isLockHolderAlive)(pid)) return false;
60
+ let ageMs;
61
+ try {
62
+ ageMs = Date.now() - (0, fs.statSync)((0, path.join)(copyRoot, entry)).mtimeMs;
63
+ } catch {
64
+ return false;
65
+ }
66
+ if (ageMs < ABANDONED_AFTER_MS) return true;
67
+ const started = startTimeOf(pid);
68
+ if (started === "unavailable") return true;
69
+ return started !== null && Math.abs(started - startMs) <= START_TIME_TOLERANCE_MS;
70
+ }
71
+ /** The copy roots this process has swept: once per process. */
72
+ const sweptCopyRoots = /* @__PURE__ */ new Set();
73
+ /** Removes every other process's copy directory whose owner is not live. */
74
+ function sweepAbandonedCopyDirs(copyRoot) {
75
+ if (sweptCopyRoots.has(copyRoot)) return;
76
+ sweptCopyRoots.add(copyRoot);
77
+ let entries;
78
+ try {
79
+ entries = (0, fs.readdirSync)(copyRoot);
80
+ } catch {
81
+ return;
82
+ }
83
+ for (const entry of entries) {
84
+ if (entry === processCopyDir() || ownerLive(copyRoot, entry)) continue;
85
+ (0, fs.rmSync)((0, path.join)(copyRoot, entry), {
86
+ recursive: true,
87
+ force: true
88
+ });
89
+ }
90
+ }
91
+ //#endregion
9
92
  //#region src/build-start.ts
10
93
  /**
11
94
  * `resolveSpecifier` maps a specifier to an absolute id. `emitAsset` is
12
95
  * present in build only, where `asset()` files become Rollup assets.
13
96
  */
14
97
  async function runBuildStart(ctx, resolveSpecifier, emitAsset) {
15
- (0, _animus_ui_extract_pipeline.clearEngineCache)(ctx.engineApi);
16
98
  ctx.assetPassComplete = false;
17
99
  ctx.assetUrlBySpecifier.clear();
18
100
  ctx.assetResolutionFailures.clear();
19
- let t0 = performance.now();
20
- ctx.loadSystem();
101
+ const t0 = performance.now();
21
102
  if (ctx.options.layers) {
22
103
  (0, _animus_ui_extract_pipeline.validateLayerOrder)(ctx.options.layers);
23
104
  ctx.log(`Custom layers: [${ctx.options.layers.join(", ")}]`);
24
105
  }
106
+ const session = ctx.session ?? ctx.createSession();
107
+ ctx.excludeMatcher = session.scanConfig.excludeMatcher;
108
+ ctx.extensionsSet = session.scanConfig.extensionsSet;
109
+ session.reattach();
110
+ session.hostResolve = resolveSpecifier;
111
+ if (ctx.isProd) await session.runFullPipeline();
112
+ else await ctx.runAttempt((started) => started.runFullPipeline());
25
113
  if (ctx.verbose) {
26
114
  const propCount = Object.keys(JSON.parse(ctx.system.propConfigJson)).length;
27
115
  const groupCount = Object.keys(JSON.parse(ctx.system.groupRegistryJson)).length;
28
116
  ctx.log(`System loaded: ${propCount} props, ${groupCount} groups (${Math.round(performance.now() - t0)}ms)`);
29
117
  }
30
- const discovered = await discoverSources(ctx, resolveSpecifier);
31
- if (!ctx.isProd) ctx.rediscoverSources = async () => {
32
- const rediscovered = await discoverSources(ctx, resolveSpecifier);
33
- return {
34
- ...rediscovered,
35
- rawEntries: rediscovered.rawEntries.map((entry) => {
36
- const cached = ctx.fileCache.get(entry.path);
37
- return cached ? {
38
- path: entry.path,
39
- ...cached
40
- } : entry;
41
- })
42
- };
43
- };
44
- t0 = performance.now();
45
- await ctx.analyzeIngested(discovered);
46
118
  const assetSpecifiers = ctx.sheetAssetSpecifiers();
119
+ ctx.assetSourceBySpecifier.clear();
120
+ if (ctx.assetCopyDir !== null) sweepAbandonedCopyDirs((0, path.dirname)(ctx.assetCopyDir));
47
121
  for (const specifier of assetSpecifiers) {
48
122
  const resolvedPath = await resolveSpecifier(specifier);
49
123
  if (!resolvedPath) {
@@ -56,9 +130,12 @@ async function runBuildStart(ctx, resolveSpecifier, emitAsset) {
56
130
  } catch (err) {
57
131
  ctx.assetFallback(specifier, `[animus-extract] failed to emit asset() specifier ${specifier}: ${String(err)}`, err);
58
132
  }
59
- else ctx.assetUrlBySpecifier.set(specifier, ctx.devFsUrl(resolvedPath));
133
+ else try {
134
+ ctx.assetUrlBySpecifier.set(specifier, ctx.devAssetUrl(specifier, resolvedPath));
135
+ } catch (err) {
136
+ ctx.assetFallback(specifier, `[animus-extract] failed to copy asset() specifier ${specifier}: ${String(err)}`, err);
137
+ }
60
138
  }
61
- ctx.substituteSheets();
62
139
  ctx.assetPassComplete = true;
63
140
  if (ctx.storedManifest) {
64
141
  const report = ctx.storedManifest.report;
@@ -69,7 +146,7 @@ async function runBuildStart(ctx, resolveSpecifier, emitAsset) {
69
146
  else if (d.kind === "variant") ctx.trace(`${d.component} variant '${d.name}' pruned: ${d.reason}`);
70
147
  else if (d.kind === "state") ctx.trace(`${d.component} state '${d.name}' pruned: ${d.reason}`);
71
148
  ctx.log(`CSS: ${ctx.resolvedComponentCss.length} bytes (${Object.keys(ctx.storedManifest.components).length} components)`);
72
- if (!ctx.isProd && ctx.storedSheets) {
149
+ if (!ctx.isProd) {
73
150
  const staticSize = (0, _animus_ui_extract_pipeline.assembleStylesheet)({
74
151
  layers: ctx.options.layers,
75
152
  variableCss: ctx.variableCss,
@@ -88,88 +165,6 @@ async function runBuildStart(ctx, resolveSpecifier, emitAsset) {
88
165
  ctx.layerDeclaration = declaration;
89
166
  if (ctx.options.verify) ctx.runSelfVerify();
90
167
  }
91
- /** Local and external discovery, publishing the package state on `ctx`, and
92
- * the analysis input it yields. */
93
- async function discoverSources(ctx, resolveSpecifier) {
94
- const t0 = performance.now();
95
- ctx.excludeMatcher = (0, _animus_ui_extract_pipeline.createExcludeMatcher)(ctx.options.exclude);
96
- const excludePatterns = ctx.excludeMatcher;
97
- ctx.extensionsSet = new Set(ctx.options.extensions ?? _animus_ui_extract_pipeline.DEFAULT_EXTENSIONS);
98
- const filePaths = (0, _animus_ui_extract_pipeline.discoverFiles)(ctx.rootDir, ctx.rootDir, excludePatterns, ctx.extensionsSet);
99
- const rawEntries = [];
100
- const ingestionFailures = [];
101
- for (const filePath of filePaths) {
102
- const relPath = (0, path.relative)(ctx.rootDir, filePath);
103
- let source;
104
- try {
105
- source = (0, fs.readFileSync)(filePath, "utf-8");
106
- } catch (err) {
107
- if (!(0, _animus_ui_extract_pipeline.isDeletedSource)(err)) ingestionFailures.push((0, _animus_ui_extract_pipeline.unreadableSourceDiagnostic)(relPath, err));
108
- continue;
109
- }
110
- const hash = !ctx.isProd ? (0, _animus_ui_extract_pipeline.contentHash)(source) : void 0;
111
- rawEntries.push({
112
- path: relPath,
113
- source,
114
- hash
115
- });
116
- }
117
- const localFileCount = rawEntries.length;
118
- const discoveryDiagnostics = [];
119
- const packageSpecifiers = await (0, _animus_ui_extract_pipeline.extractSystemFilePackages)(ctx.resolvedSystemPath, (0, _animus_ui_extract_pipeline.engineModuleParser)(ctx.engineApi()), (diagnostic) => discoveryDiagnostics.push(diagnostic), resolveSpecifier);
120
- discoveryDiagnostics.push(...(0, _animus_ui_extract_pipeline.importedKitDiagnostics)(rawEntries, ctx.engineApi(), ctx.rootDir, packageSpecifiers));
121
- ctx.externalSourceEntries.clear();
122
- const collected = await (0, _animus_ui_extract_pipeline.collectExternalPackageSources)({
123
- specifiers: packageSpecifiers,
124
- resolveSpecifier,
125
- rootDir: ctx.rootDir,
126
- extensionsSet: ctx.extensionsSet,
127
- hasEntry: (relPath) => rawEntries.some((entry) => entry.path === relPath),
128
- onUnreadable: (relPath, err) => {
129
- if (!(0, _animus_ui_extract_pipeline.isDeletedSource)(err)) ingestionFailures.push((0, _animus_ui_extract_pipeline.unreadableSourceDiagnostic)(relPath, err));
130
- }
131
- });
132
- ctx.ingestionFailureDiagnostics = ingestionFailures;
133
- (0, _animus_ui_extract_pipeline.surfaceManifestDiagnostics)({ diagnostics: [...discoveryDiagnostics, ...collected.diagnostics] }, (message) => ctx.warn(message), {
134
- levels: ctx.options.diagnostics,
135
- strict: ctx.options.strict,
136
- reportErrors: ctx.reportErrors()
137
- });
138
- ctx.packageMap = collected.packageMap;
139
- ctx.externalPackageOutcomes = collected.outcomes;
140
- ctx.kitDescriptors = collected.kitDescriptors;
141
- ctx.externalDirOwners = (0, _animus_ui_extract_pipeline.firstOwners)(collected.dirOwnerSets);
142
- ctx.externalFileOwners = collected.fileOwners;
143
- ctx.enforceIncludeResolution();
144
- for (const [specifier, srcEntry] of collected.sourceEntries) ctx.externalSourceEntries.set(specifier, srcEntry);
145
- ctx.externalSourceSideEffects = collected.sourceEntrySideEffects;
146
- for (const entry of collected.entries) {
147
- const hash = !ctx.isProd ? (0, _animus_ui_extract_pipeline.contentHash)(entry.source) : void 0;
148
- rawEntries.push({
149
- path: entry.path,
150
- source: entry.source,
151
- hash
152
- });
153
- }
154
- ctx.externalPackageDirs = collected.packageDirs;
155
- ctx.externalLinkedDirs = collected.linkedDirs;
156
- ctx.externalOutputDirs = collected.outputDirs;
157
- ctx.registerSystemWatchPaths();
158
- const packageFileCount = rawEntries.length - localFileCount;
159
- ctx.log(`Discovered ${rawEntries.length} files (${packageFileCount} from packages) (${Math.round(performance.now() - t0)}ms)`);
160
- return {
161
- rawEntries,
162
- afterIngestion: (accepted) => {
163
- if (!ctx.isProd) ctx.mutateFileCache((cache) => {
164
- cache.clear();
165
- for (const entry of accepted.originalEntries) cache.set(entry.path, {
166
- hash: entry.hash,
167
- source: entry.source
168
- });
169
- });
170
- }
171
- };
172
- }
173
168
  //#endregion
174
169
  //#region src/config.ts
175
170
  function applyResolvedConfig(ctx, config) {
@@ -177,7 +172,19 @@ function applyResolvedConfig(ctx, config) {
177
172
  ctx.development = !ctx.isProd || Boolean(config.build.watch);
178
173
  ctx.emissionProd = (0, _animus_ui_extract_pipeline.resolveMode)(ctx.options.mode, () => config.command === "build" ? "production" : "development").mode === "production";
179
174
  ctx.rootDir = config.root;
175
+ if (!ctx.isProd) {
176
+ const copyDir = (0, path.resolve)(config.cacheDir, "animus-assets", processCopyDir());
177
+ const served = (0, vite.isFileLoadingAllowed)(config, (0, path.join)(copyDir, "asset"));
178
+ ctx.assetCopyDir = served ? copyDir : null;
179
+ ctx.servesFile = (path$8) => (0, vite.isFileLoadingAllowed)(config, path$8);
180
+ if (!served) ctx.warn(`[animus-extract] asset() copies in ${copyDir} are not served under server.fs; serving the live files instead, so an edited asset keeps its URL`);
181
+ }
182
+ ctx.outputExcludes = (0, _animus_ui_extract_pipeline.outputExclusions)(config.root, (0, _animus_ui_extract_pipeline.viteOutputTargets)(config));
180
183
  ctx.resolveConditions = config.resolve.conditions.map((condition) => condition === "development|production" ? config.isProduction ? "production" : "development" : condition);
184
+ ctx.extensionPolicy = (0, _animus_ui_extract_pipeline.viteExtensionPolicy)(config.resolve.extensions);
185
+ ctx.assetClassification = (0, _animus_ui_extract_pipeline.viteAssetClassification)(config.assetsInclude);
186
+ ctx.loadExpander = (0, _animus_ui_extract_pipeline.viteHostExpander)(config);
187
+ ctx.log(`extension order: ${ctx.extensionPolicy.resolverVersion}`);
181
188
  ctx.logger = config.logger;
182
189
  ctx.base = config.base ?? "/";
183
190
  ctx.lcssTargets = (0, _animus_ui_extract_pipeline.resolveLightningTargets)(ctx.options.targets, ctx.rootDir);
@@ -196,6 +203,7 @@ function applyResolvedConfig(ctx, config) {
196
203
  ctx.pathAliasesJson = built.json;
197
204
  ctx.log(`Path aliases forwarded: ${built.count} entries`);
198
205
  }
206
+ ctx.createSession();
199
207
  }
200
208
  //#endregion
201
209
  //#region src/constants.ts
@@ -277,80 +285,6 @@ function eventKey(file, timestamp) {
277
285
  return `${file}\u0000${timestamp}`;
278
286
  }
279
287
  //#endregion
280
- //#region src/module-invalidation.ts
281
- /**
282
- * Completeness is load-bearing: the file-plan diff and the sources re-delivered
283
- * when a held publication ends are the only invalidation candidate sources, so
284
- * a node shape missed here stays stale for the session.
285
- */
286
- function invalidateFileModules(ctx, relPaths) {
287
- const server = ctx.devServer;
288
- if (!server || relPaths.length === 0) return 0;
289
- const graphs = server.environments ? Object.values(server.environments).map((env) => env?.moduleGraph).filter(Boolean) : [server.moduleGraph].filter(Boolean);
290
- let total = 0;
291
- for (const relPath of relPaths) {
292
- const absPath = (0, path.resolve)(ctx.rootDir, relPath);
293
- let fileCount = 0;
294
- for (const graph of graphs) {
295
- const nodes = /* @__PURE__ */ new Set();
296
- const byFile = graph.getModulesByFile?.(absPath);
297
- if (byFile) for (const mod of byFile) nodes.add(mod);
298
- const byId = graph.getModuleById?.(absPath);
299
- if (byId) nodes.add(byId);
300
- for (const mod of nodes) {
301
- graph.invalidateModule(mod);
302
- fileCount++;
303
- }
304
- }
305
- if (fileCount > 0) ctx.trace(`HMR invalidate: ${relPath} (plan changed, ${fileCount} nodes)`);
306
- total += fileCount;
307
- }
308
- return total;
309
- }
310
- //#endregion
311
- //#region src/reset-coalescer.ts
312
- var ResetCoalescer = class {
313
- run;
314
- onError;
315
- quietMs;
316
- schedule;
317
- cancel;
318
- timer = null;
319
- running = false;
320
- dirty = false;
321
- constructor(run, onError, quietMs = 60, schedule = (fn, ms) => setTimeout(fn, ms), cancel = (timer) => clearTimeout(timer)) {
322
- this.run = run;
323
- this.onError = onError;
324
- this.quietMs = quietMs;
325
- this.schedule = schedule;
326
- this.cancel = cancel;
327
- }
328
- /** Ask for a reset; bursts within the quiescence window collapse. */
329
- request() {
330
- if (this.running) {
331
- this.dirty = true;
332
- return;
333
- }
334
- if (this.timer !== null) this.cancel(this.timer);
335
- this.timer = this.schedule(async () => {
336
- this.timer = null;
337
- this.running = true;
338
- try {
339
- const result = this.run();
340
- if (result instanceof Promise) await result;
341
- } catch (err) {
342
- this.onError(err);
343
- } finally {
344
- this.running = false;
345
- if (this.dirty) {
346
- this.dirty = false;
347
- this.request();
348
- }
349
- }
350
- }, this.quietMs);
351
- }
352
- };
353
- //#endregion
354
288
  //#region src/context.ts
355
289
  function emptySystemConfig() {
356
290
  return {
@@ -365,55 +299,14 @@ function emptySystemConfig() {
365
299
  keyframesJson: null
366
300
  };
367
301
  }
368
- const analysisChains = /* @__PURE__ */ new WeakMap();
369
- /** Runs `task` after every analysis transaction already scheduled for this
370
- * context. Entry points only — a nested call waits on its own chain. */
371
- function runExclusiveAnalysis(ctx, task) {
372
- const result = (analysisChains.get(ctx) ?? Promise.resolve()).then(task);
373
- analysisChains.set(ctx, result.then(() => void 0, () => void 0));
374
- return result;
375
- }
376
- function generateSystemPropsModule(ctx) {
377
- return (0, _animus_ui_extract_pipeline.buildSystemPropsModule)({
378
- systemPropMapJson: ctx.storedSystemPropMapJson,
379
- groupRegistryJson: ctx.system.groupRegistryJson,
380
- dynamicProps: JSON.parse(ctx.storedDynamicPropsJson),
381
- admittedTransforms: JSON.parse(ctx.storedAdmittedTransformsJson),
382
- typedSystemProps: JSON.parse(ctx.storedTypedSystemPropsJson)
383
- });
384
- }
385
- /** Memoized on the inputs themselves, so no writer owes the memo a refresh
386
- * and a stale memo cannot serve a module from a generation that is gone. */
387
- const systemPropsModules = /* @__PURE__ */ new WeakMap();
388
- /** NUL cannot appear in JSON text, so concatenation is injective. */
389
- function systemPropsModuleKey(ctx) {
390
- return [
391
- ctx.storedSystemPropMapJson,
392
- ctx.system.groupRegistryJson,
393
- ctx.storedDynamicPropsJson,
394
- ctx.storedAdmittedTransformsJson,
395
- ctx.storedTypedSystemPropsJson
396
- ].join("\0");
397
- }
398
- /** The exact source `virtual:animus/system-props` serves now — one definition,
399
- * so the served bytes and the HMR change decision cannot diverge. */
400
- function systemPropsModuleSource(ctx) {
401
- const key = systemPropsModuleKey(ctx);
402
- const memo = systemPropsModules.get(ctx);
403
- if (memo && memo.key === key) return memo.source;
404
- const source = generateSystemPropsModule(ctx);
405
- systemPropsModules.set(ctx, {
406
- key,
407
- source
408
- });
409
- return source;
410
- }
411
- /** Drop a deleted or renamed-away raw original from the dev file cache.
412
- * Generated MDX/Svelte children live only in the published corpus. */
413
- function pruneFileCache(cache, rootDir, absPath) {
414
- const rel = (0, path.relative)(rootDir, (0, path.resolve)(absPath));
415
- return cache.delete(rel);
416
- }
302
+ /** The system-props module before a first publication: no props. */
303
+ const EMPTY_SYSTEM_PROPS_MODULE = (0, _animus_ui_extract_pipeline.buildSystemPropsModule)({
304
+ systemPropMapJson: "{}",
305
+ groupRegistryJson: emptySystemConfig().groupRegistryJson,
306
+ dynamicProps: {},
307
+ admittedTransforms: {},
308
+ typedSystemProps: []
309
+ });
417
310
  const NO_SHEETS = {
418
311
  variableCss: "",
419
312
  globalCss: "",
@@ -424,7 +317,6 @@ var PluginContext = class {
424
317
  verbose;
425
318
  /** The higher tier: `verbose: 'trace'` or `ANIMUS_DEBUG=trace`. */
426
319
  tracing;
427
- staticCssJson;
428
320
  isProd = false;
429
321
  /** Serving or watching: an error-level diagnostic reports, and only a
430
322
  * one-shot build fails on one. */
@@ -435,145 +327,298 @@ var PluginContext = class {
435
327
  rootDir = "";
436
328
  logger = null;
437
329
  system = emptySystemConfig();
438
- /** Whether a system has loaded, so a failed reload has one to keep. */
439
- systemLoaded = false;
440
330
  lcssTargets = {};
441
331
  pathAliasesJson = null;
442
332
  /** Vite's export conditions, so the loader reads the files Vite serves. */
443
333
  resolveConditions = [];
444
- /** Development's repeat of buildStart's discovery, with its resolver. */
445
- rediscoverSources = null;
334
+ /** Vite's resolved extension order; the built-in policy until
335
+ * `configResolved`. */
336
+ extensionPolicy = _animus_ui_extract_pipeline.BUILT_IN_EXTENSION_POLICY;
337
+ /** Vite's asset classification, captured at `configResolved`. */
338
+ assetClassification = (0, _animus_ui_extract_pipeline.builtInAssetClassification)("noHost");
339
+ /** How Vite's build expands computed loads, captured at
340
+ * `configResolved`; none where it cannot be reproduced exactly. */
341
+ loadExpander;
446
342
  extensionsSet;
343
+ /** Built once per buildStart, before any discovery, from the user's
344
+ * exclude list and the output directory. */
447
345
  excludeMatcher;
346
+ /** Where Vite writes, every environment's output directories and files,
347
+ * as structural exclusions; none outside the root. Set in
348
+ * `configResolved`. */
349
+ outputExcludes = [];
448
350
  storedManifest = null;
449
351
  storedManifestJson = "";
450
- globalCss = "";
451
- resolvedComponentCss = "";
452
- storedSheets = null;
352
+ /** The analysis sheets with their asset() placeholders. They are read
353
+ * through `globalCss` and `resolvedComponentCss`, which substitute the
354
+ * current asset URLs, so an edited asset changes its URL without
355
+ * re-analysis. */
356
+ analysisGlobalCss = "";
357
+ analysisComponentCss = "";
358
+ get globalCss() {
359
+ return this.substitutedSheets().globalCss;
360
+ }
361
+ set globalCss(css) {
362
+ this.analysisGlobalCss = css;
363
+ }
364
+ get resolvedComponentCss() {
365
+ return this.substitutedSheets().componentCss;
366
+ }
367
+ set resolvedComponentCss(css) {
368
+ this.analysisComponentCss = css;
369
+ }
370
+ substitutedSheets() {
371
+ return (0, _animus_ui_extract_pipeline.substituteSheetAssets)({
372
+ ...NO_SHEETS,
373
+ globalCss: this.analysisGlobalCss,
374
+ componentCss: this.analysisComponentCss
375
+ }, this.assetUrlBySpecifier);
376
+ }
453
377
  layerDeclaration = "";
454
378
  transformOutputHashes = /* @__PURE__ */ new Map();
455
379
  recordTransformOutput(relativePath, code) {
456
380
  this.transformOutputHashes.set(relativePath, (0, _animus_ui_extract_pipeline.contentHash)(code));
457
381
  }
458
382
  reverseProvenance = {};
459
- storedSystemPropMapJson = "{}";
460
- storedDynamicPropsJson = "{}";
461
- storedAdmittedTransformsJson = "{}";
462
- storedTypedSystemPropsJson = "[]";
463
- fileCache = /* @__PURE__ */ new Map();
464
- /** Mutation attempts against `fileCache`, for memos that must not key on
465
- * `size` — a delete plus an unrelated create leaves it unchanged. */
466
- fileCacheGeneration = 0;
467
- /** The one mutator of the raw-source cache: every add, replacement and
468
- * deletion goes through here so no write escapes the counter above. */
469
- mutateFileCache(mutate) {
470
- const cache = this.fileCache;
471
- const result = mutate(cache);
472
- this.fileCacheGeneration += 1;
473
- return result;
474
- }
475
- /** Generated MDX/Svelte paths never enter `fileCache` — they live only in
476
- * this corpus's published projection. */
477
- corpus = (0, _animus_ui_extract_pipeline.createSourceCorpus)({
478
- engineApi: () => this.engineApi(),
479
- prefix: "[animus-extract]",
480
- strict: () => !!this.options.strict,
481
- warn: (message) => this.warn(message),
482
- reportErrors: () => this.reportErrors()
483
- });
484
- rawExtensionFallbacks = /* @__PURE__ */ new Set();
383
+ /** The exact source `virtual:animus/system-props` serves: the last
384
+ * publication's, so the served bytes and the HMR change decision cannot
385
+ * diverge. */
386
+ systemPropsModule = EMPTY_SYSTEM_PROPS_MODULE;
485
387
  /** Original path → hash of bytes whose parse aborted in a rejected attempt;
486
388
  * non-empty while that rejection holds publication. Those bytes are served
487
389
  * raw so the host reports its own syntax error instead of receiving the
488
390
  * previous module from the retained engine. */
489
391
  abortedParseHashes = /* @__PURE__ */ new Map();
490
- /** A system reload is pending: the next analysis attempt loads the latest
491
- * system. A parse hold keeps it pending and the published system served;
492
- * new dependency watch keys stay registered meanwhile. */
493
- systemReloadOwed = false;
494
- /** The one mutator of a file's fallback state — every raw-serve exit of the
495
- * transform hook reports here, except aborted bytes, which never execute.
496
- * Production keeps the set empty. The
497
- * barrier's one-shot withhold release must not route through here: it
498
- * retires other files' records, no claim about their current serve. */
499
- recordFallbackState(relativePath, isFallback) {
500
- if (this.isProd) return;
501
- if (isFallback) this.rawExtensionFallbacks.add(relativePath);
502
- else this.rawExtensionFallbacks.delete(relativePath);
503
- }
504
392
  hotUpdateEvents = new HotUpdateEvents();
505
- pendingReloadTimer = null;
506
- packageMap = {};
393
+ /** Coalesces a burst of invalidations into one full reload. */
394
+ reloadWindow = new _animus_ui_extract_session.CoalescingWindow(() => {
395
+ this.devServer?.hot?.send({ type: "full-reload" });
396
+ });
507
397
  base = "/";
508
398
  assetUrlBySpecifier = /* @__PURE__ */ new Map();
399
+ /** Where dev asset() copies live, immutable and content-named; null in a
400
+ * build, or when the dev server does not serve the directory, which
401
+ * serves the live files instead. Set in `configResolved`. */
402
+ assetCopyDir = null;
403
+ /** specifier → the source file its dev copy was made from. */
404
+ assetSourceBySpecifier = /* @__PURE__ */ new Map();
405
+ /** source → its copies, oldest first: the current and the previous. */
406
+ assetCopyHistory = /* @__PURE__ */ new Map();
407
+ /** Specifiers whose copy the dev server does not serve, warned once. */
408
+ unservedAssetCopies = /* @__PURE__ */ new Set();
409
+ /** Whether the dev server serves `path`, by Vite's `server.fs` rule;
410
+ * set in `configResolved`. */
411
+ servesFile = () => true;
509
412
  assetResolutionFailures = /* @__PURE__ */ new Set();
510
413
  assetPassComplete = false;
511
414
  externalPackageDirs = [];
512
- /** Those whose package is linked rather than installed. */
513
- externalLinkedDirs = [];
514
- /** Those read from their package's compiled output. */
515
- externalOutputDirs = [];
516
- externalDirOwners = {};
517
415
  /** specifier → whether its redirected source entry has side effects, from
518
416
  * the owning package's `sideEffects`; absent leaves Vite's default. */
519
417
  externalSourceSideEffects = /* @__PURE__ */ new Map();
520
- externalFileOwners = {};
521
418
  externalSourceEntries = /* @__PURE__ */ new Map();
522
419
  externalPackageOutcomes = [];
523
- /** The analysed kits' descriptors, from the last discovery. */
524
- kitDescriptors = [];
525
- /** Configured files buildStart or a late rediscovery could not read.
526
- * Later analyses read no files themselves, so they replay these until the
527
- * file is read into the cache or deleted. */
528
- ingestionFailureDiagnostics = [];
529
420
  devServer;
530
- resolvedSystemPath = null;
531
- systemDependencyKeys = /* @__PURE__ */ new Set();
532
- systemDependencyPaths = [];
533
- v2Engine = null;
534
- v2SentSources = null;
535
- v2DriftWarned = false;
421
+ /** The Session's path lists already registered with Vite's watcher; the
422
+ * Session replaces a list when it changes. */
423
+ watchedPathLists = /* @__PURE__ */ new WeakSet();
536
424
  /** Every native extraction call goes through this one accessor. */
537
425
  engineApi;
426
+ /** The native module's stateless lookups, before the root resolves: the
427
+ * shared factory, which registers no handle. */
428
+ kitEngine = _animus_ui_extract_session.engineFactory;
429
+ engineKey = null;
430
+ /** This context's build: one Vite config instance, one build. */
431
+ build = (0, _animus_ui_extract_session.createBuildId)();
432
+ /** The build's Session, created once the Vite root resolves. */
433
+ session = null;
434
+ /** The outcomes each in-flight `submitChanges` collects. */
435
+ outcomeCollectors = /* @__PURE__ */ new Set();
538
436
  constructor(options, engineApiOverride) {
539
437
  this.options = options;
540
- this.staticCssJson = (0, _animus_ui_extract_pipeline.serializeStaticCss)(options.staticCss);
541
438
  const verbosity = (0, _animus_ui_extract_pipeline.resolveVerbosity)(options.verbose, process.env.ANIMUS_DEBUG);
542
439
  this.verbose = verbosity !== "quiet";
543
440
  this.tracing = verbosity === "trace";
544
441
  this.extensionsSet = new Set(options.extensions ?? _animus_ui_extract_pipeline.DEFAULT_EXTENSIONS);
545
- this.excludeMatcher = (0, _animus_ui_extract_pipeline.createExcludeMatcher)(options.exclude);
546
442
  if (engineApiOverride) {
547
443
  this.engineApi = engineApiOverride;
444
+ this.kitEngine = engineApiOverride;
548
445
  return;
549
446
  }
550
- const engineModuleId = "@animus-ui/extract";
551
- this.engineApi = (0, _animus_ui_extract_pipeline.createV2EngineApi)({
552
- label: "animus-extract",
553
- isV2: () => true,
554
- loadNativeEngine: () => require(engineModuleId),
555
- rehydrateFilesJson: (filesJsonRaw) => {
556
- if (!filesJsonRaw.includes("\"source\":\"\"")) return filesJsonRaw;
557
- const entries = (0, _animus_ui_extract_pipeline.parseFilesJson)(filesJsonRaw, "animus-extract");
558
- const analysisEntries = this.corpus.published.analysisEntries;
559
- for (const entry of entries) if (entry.source === "") entry.source = analysisEntries.get(entry.path)?.source ?? "";
560
- return JSON.stringify(entries);
561
- },
562
- store: {
563
- getEngine: () => this.v2Engine,
564
- setEngine: (engine) => {
565
- this.v2Engine = engine;
566
- },
567
- getSentSources: () => this.v2SentSources,
568
- setSentSources: (sources) => {
569
- this.v2SentSources = sources;
570
- },
571
- getDriftWarned: () => this.v2DriftWarned,
572
- setDriftWarned: (value) => {
573
- this.v2DriftWarned = value;
574
- }
447
+ const build = this.build;
448
+ const rehydrateFilesJson = (filesJsonRaw) => {
449
+ if (!filesJsonRaw.includes("\"source\":\"\"")) return filesJsonRaw;
450
+ const entries = (0, _animus_ui_extract_pipeline.parseFilesJson)(filesJsonRaw, "animus-extract");
451
+ const analysisEntries = this.session?.corpus.published.analysisEntries ?? /* @__PURE__ */ new Map();
452
+ for (const entry of entries) if (entry.source === "") entry.source = analysisEntries.get(entry.path)?.source ?? "";
453
+ return JSON.stringify(entries);
454
+ };
455
+ this.engineApi = () => {
456
+ if (!this.rootDir) throw new Error("[animus-extract] the engine was used before the Vite root resolved");
457
+ this.engineKey ??= (0, _animus_ui_extract_session.sessionKey)(build, this.rootDir);
458
+ return (0, _animus_ui_extract_session.engineHandle)(this.engineKey, {
459
+ label: "animus-extract",
460
+ rehydrateFilesJson
461
+ })();
462
+ };
463
+ }
464
+ /** Creates the build's Session from the resolved config: Vite's engine,
465
+ * logger and core options, kept in memory. */
466
+ createSession() {
467
+ this.session?.close();
468
+ const options = this.options;
469
+ const session = new _animus_ui_extract_session.ExtractionSession({
470
+ system: options.system,
471
+ exclude: options.exclude,
472
+ extensions: options.extensions,
473
+ strict: options.strict,
474
+ diagnostics: options.diagnostics,
475
+ verbose: options.verbose,
476
+ prefix: options.prefix,
477
+ prefixContextualVars: options.prefixContextualVars,
478
+ targets: options.targets,
479
+ minify: options.minify,
480
+ staticCss: options.staticCss,
481
+ layers: options.layers,
482
+ engine: options.engine,
483
+ mode: this.emissionProd ? "production" : "development"
484
+ }, {
485
+ build: this.build,
486
+ unbundledComputedImports: true,
487
+ engine: () => this.engineApi(),
488
+ logger: {
489
+ info: (line) => (this.logger ?? console).info(line),
490
+ warn: (line) => (this.logger ?? console).warn(line),
491
+ error: (line) => (this.logger ?? console).error(line)
575
492
  }
576
493
  });
494
+ session.persistArtifacts = false;
495
+ session.hostSubstitutesAssets = true;
496
+ session.driverLabel = "animus-extract";
497
+ session.onOutcome = (outcome) => this.observeOutcome(session, outcome);
498
+ session.development = this.development;
499
+ session.rootDir = this.rootDir;
500
+ session.conditions = this.resolveConditions;
501
+ session.extensionPolicy = this.extensionPolicy;
502
+ session.assetClassification = this.assetClassification;
503
+ session.loadExpander = this.loadExpander;
504
+ session.pathAliasesJson = this.pathAliasesJson;
505
+ session.runtimeImport = options.runtimeImport ?? _animus_ui_extract_pipeline.DEFAULT_RUNTIME_IMPORT;
506
+ session.cssModuleId = VIRTUAL_CSS_ID;
507
+ session.systemPropsModuleId = VIRTUAL_SYSTEM_PROPS_ID;
508
+ session.structuralExclude = this.outputExcludes;
509
+ this.session = session;
510
+ return session;
511
+ }
512
+ /** Serves each publication as the Session makes it, so every reader in
513
+ * between sees one generation. */
514
+ observeOutcome(session, outcome) {
515
+ let systemPropsChanged = false;
516
+ if (outcome.kind === "held") for (const [path$6, hash] of outcome.abortedOriginals) this.abortedParseHashes.set(path$6, hash);
517
+ else {
518
+ this.abortedParseHashes.clear();
519
+ const before = this.systemPropsModule;
520
+ this.adoptPublication(session);
521
+ systemPropsChanged = this.systemPropsModule !== before;
522
+ }
523
+ for (const collected of this.outcomeCollectors) collected.push({
524
+ outcome,
525
+ systemPropsChanged
526
+ });
527
+ }
528
+ /** Submits a dev change set to the Session. Until a first publication
529
+ * there is no generation to update: the full read covers the change. */
530
+ submitChanges(changes) {
531
+ return this.runAttempt((session) => session.publication === null ? session.runFullPipeline([], changes.sources) : session.handleWatchUpdate(changes));
532
+ }
533
+ /** Runs one Session attempt and returns the outcomes that settle it. A
534
+ * hold warns and resolves with its outcome (Vite reports the syntax
535
+ * error itself); any other failure rejects. Either way the watcher
536
+ * learns the paths the attempt's system load named. */
537
+ async runAttempt(attempt) {
538
+ const session = this.session ?? this.createSession();
539
+ const collected = [];
540
+ this.outcomeCollectors.add(collected);
541
+ try {
542
+ await attempt(session);
543
+ } catch (error) {
544
+ if (!collected.some(({ outcome }) => outcome.kind === "held")) throw error;
545
+ this.warn(error instanceof Error ? error.message : String(error));
546
+ } finally {
547
+ this.outcomeCollectors.delete(collected);
548
+ this.registerSystemWatchPaths();
549
+ }
550
+ return collected;
551
+ }
552
+ /** Serves the Session's last publication: the manifest, the analysis
553
+ * sheets (asset placeholders kept; Vite substitutes them), the system,
554
+ * the package map and the external-package state. */
555
+ adoptPublication(session) {
556
+ const publication = session.publication;
557
+ if (publication === null) return;
558
+ const manifest = publication.manifest;
559
+ this.system = publication.system;
560
+ this.storedManifest = manifest;
561
+ this.storedManifestJson = publication.manifestJson;
562
+ this.globalCss = publication.analysisGlobalCss;
563
+ this.resolvedComponentCss = publication.analysisComponentCss;
564
+ this.reverseProvenance = manifest.reverse_provenance;
565
+ this.systemPropsModule = publication.systemPropsModule;
566
+ this.externalPackageDirs = session.externalPackageDirs;
567
+ this.externalSourceEntries = session.externalSourceEntries;
568
+ this.externalSourceSideEffects = session.externalSourceSideEffects;
569
+ this.externalPackageOutcomes = session.lastExternalOutcomes;
570
+ this.applyAssetSubstitutions();
571
+ }
572
+ /** The environment a host without environments builds as. */
573
+ singleEnvironment = { name: "build" };
574
+ /** Each environment build that started and has not ended, by name. A
575
+ * custom `buildApp` builds a shared plugin's environments in parallel, so
576
+ * the context's build retires with the last of them, not the first. */
577
+ openBuilds = /* @__PURE__ */ new Map();
578
+ /** Opens `environment`'s build, after any open build under its name has
579
+ * ended: a builder reused while an earlier build still runs, or a second
580
+ * environment under one name, builds after it rather than beside it.
581
+ * Each build then ends its own generation once, in `buildEnd`. */
582
+ async openBuild(environment) {
583
+ for (;;) {
584
+ const open = this.openBuilds.get(environment.name);
585
+ if (open === void 0) break;
586
+ await open.ended;
587
+ }
588
+ let end = () => {};
589
+ const ended = new Promise((settle) => {
590
+ end = settle;
591
+ });
592
+ this.openBuilds.set(environment.name, {
593
+ environment,
594
+ ended,
595
+ end
596
+ });
597
+ }
598
+ /** Ends `environment`'s open build; whether that left none open. Vite
599
+ * ends each build once, in `buildEnd`, and a dev server ends only the
600
+ * environments that started, so only the build that opened an entry
601
+ * ends it. */
602
+ endBuild(environment) {
603
+ const open = this.openBuilds.get(environment.name);
604
+ if (open === void 0 || open.environment !== environment) return false;
605
+ this.openBuilds.delete(environment.name);
606
+ open.end();
607
+ return this.openBuilds.size === 0;
608
+ }
609
+ /** Retires this context's build when Vite closes it: the Session leaves
610
+ * the registry (with its manifest and watch state) and the engine handle
611
+ * is released, injected engine or not. The next build start rejoins. */
612
+ close() {
613
+ this.session?.close();
614
+ this.releaseEngine();
615
+ }
616
+ /** Releases this context's engine handle: a standalone one, which no
617
+ * joined build retires. The next use makes a fresh one. */
618
+ releaseEngine() {
619
+ if (this.engineKey === null) return;
620
+ (0, _animus_ui_extract_session.releaseEngineHandle)(this.engineKey);
621
+ this.engineKey = null;
577
622
  }
578
623
  log(msg) {
579
624
  if (this.verbose) (this.logger ?? console).info(`[animus] ${msg}`);
@@ -604,214 +649,57 @@ var PluginContext = class {
604
649
  if (!report) throw new Error(message, cause === void 0 ? void 0 : { cause });
605
650
  report(message);
606
651
  }
607
- /** Load the system into `this.system`. On failure the previous config is
608
- * kept; strict mode throws instead, and strict development reports the
609
- * failure once a system has loaded, since there is one to keep. */
610
- loadSystem() {
611
- this.resolvedSystemPath = (0, path.resolve)(this.rootDir, this.options.system);
612
- for (const key of (0, _animus_ui_extract_pipeline.toWatchKeys)(this.resolvedSystemPath)) this.systemDependencyKeys.add(key);
613
- try {
614
- this.system = (0, _animus_ui_extract_pipeline.loadSystemConfig)(this.engineApi, {
615
- systemPath: this.resolvedSystemPath,
616
- rootDir: this.rootDir,
617
- prefix: this.options.prefix,
618
- prefixContextualVars: this.options.prefixContextualVars,
619
- conditions: this.resolveConditions
620
- });
621
- const deps = this.system.dependencies ?? [];
622
- const keys = /* @__PURE__ */ new Set();
623
- for (const key of (0, _animus_ui_extract_pipeline.toWatchKeys)(this.resolvedSystemPath)) keys.add(key);
624
- for (const dep of deps) for (const key of (0, _animus_ui_extract_pipeline.toWatchKeys)(dep)) keys.add(key);
625
- this.systemDependencyKeys = keys;
626
- this.systemDependencyPaths = deps;
627
- this.registerSystemWatchPaths();
628
- this.systemLoaded = true;
629
- } catch (e) {
630
- if (this.options.strict) {
631
- const message = `[animus-extract] Failed to load system from ${this.resolvedSystemPath}: ${e}`;
632
- if (!this.systemLoaded) throw new Error(message, { cause: e });
633
- this.strictFailure(message, e);
634
- return;
652
+ devFsUrl(absPath) {
653
+ return `${this.base.endsWith("/") ? this.base.slice(0, -1) : this.base}/@fs${absPath}`;
654
+ }
655
+ /** A dev asset() URL: an immutable content-named copy of `absPath`, so an
656
+ * edit gets a new URL, or the live file where copies are not served. */
657
+ devAssetUrl(specifier, absPath) {
658
+ if (this.assetCopyDir === null) return this.devFsUrl(absPath);
659
+ const { fileName } = (0, _animus_ui_extract_pipeline.writeAssetCopy)(absPath, this.assetCopyDir);
660
+ const copyPath = (0, path.join)(this.assetCopyDir, fileName);
661
+ if (!this.servesFile(copyPath)) {
662
+ if (!this.unservedAssetCopies.has(specifier)) {
663
+ this.unservedAssetCopies.add(specifier);
664
+ this.warn(`[animus-extract] the copy of asset() ${specifier} is not served under server.fs; serving the live file instead, so an edit keeps its URL`);
635
665
  }
636
- console.warn(`[animus-extract] Failed to load system from ${this.resolvedSystemPath}:`, e);
666
+ return this.devFsUrl(absPath);
667
+ }
668
+ if (this.assetSourceBySpecifier.get(specifier) !== absPath) this.devServer?.watcher?.add(absPath);
669
+ this.assetSourceBySpecifier.set(specifier, absPath);
670
+ this.retainAssetCopy(absPath, fileName);
671
+ return this.devFsUrl(copyPath);
672
+ }
673
+ /** Keeps each source's current copy and the one before it; an asset
674
+ * edit reloads the page, so an older URL is never fetched again. A copy
675
+ * another source still keeps is never removed. */
676
+ retainAssetCopy(source, fileName) {
677
+ const history = this.assetCopyHistory.get(source) ?? [];
678
+ if (history[history.length - 1] !== fileName) history.push(fileName);
679
+ this.assetCopyHistory.set(source, history);
680
+ while (history.length > 2) {
681
+ const oldest = history.shift();
682
+ if (![...this.assetCopyHistory.values()].some((copies) => copies.includes(oldest)) && this.assetCopyDir !== null) (0, fs.rmSync)((0, path.join)(this.assetCopyDir, oldest), { force: true });
637
683
  }
638
684
  }
639
- /** The read failures still standing: a file stops counting once the cache
640
- * holds a read of it or it is gone, as in the shared session, so it stops
641
- * warning and stops holding pruning off. */
642
- unsettledIngestionFailures() {
643
- return this.ingestionFailureDiagnostics.filter(({ file }) => {
644
- if (this.fileCache.has(file)) return false;
685
+ /** Whether `absFile` is the source of a dev asset copy. */
686
+ isAssetSource(absFile) {
687
+ const keys = new Set((0, _animus_ui_extract_pipeline.toWatchKeys)(absFile));
688
+ return [...this.assetSourceBySpecifier.values()].some((source) => (0, _animus_ui_extract_pipeline.toWatchKeys)(source).some((key) => keys.has(key)));
689
+ }
690
+ /** Copies an edited asset under its new content name and redelivers the
691
+ * stylesheet that names it. */
692
+ refreshAssetCopies(absFile) {
693
+ const keys = new Set((0, _animus_ui_extract_pipeline.toWatchKeys)(absFile));
694
+ for (const [specifier, source] of this.assetSourceBySpecifier) {
695
+ if (!(0, _animus_ui_extract_pipeline.toWatchKeys)(source).some((key) => keys.has(key))) continue;
645
696
  try {
646
- (0, fs.statSync)((0, path.resolve)(this.rootDir, file));
647
- return true;
697
+ this.assetUrlBySpecifier.set(specifier, this.devAssetUrl(specifier, source));
648
698
  } catch (err) {
649
- return !(0, _animus_ui_extract_pipeline.isDeletedSource)(err);
650
- }
651
- });
652
- }
653
- /** Runs project analysis and updates every manifest-derived state. Returns
654
- * false when nothing published, and the caller must roll its cache back.
655
- * While a source is skipped or unreadable, its renders are unseen, so
656
- * nothing is pruned. */
657
- runAnalysis(fileEntries, skippedOriginals = []) {
658
- const ingestionFailures = this.unsettledIngestionFailures();
659
- let result;
660
- try {
661
- result = (0, _animus_ui_extract_pipeline.runProjectAnalysis)(this.engineApi, {
662
- fileEntries,
663
- packageMap: this.packageMap,
664
- system: this.system,
665
- emitter: {
666
- runtimeImport: this.options.runtimeImport ?? "@animus-ui/system",
667
- cssModuleId: VIRTUAL_CSS_ID
668
- },
669
- pathAliasesJson: this.pathAliasesJson,
670
- staticCssJson: this.staticCssJson,
671
- externalDirs: this.externalPackageDirs.map((dir) => (0, path.relative)(this.rootDir, dir)),
672
- devMode: !this.emissionProd,
673
- analysisContext: {
674
- skippedSources: [...skippedOriginals, ...ingestionFailures.map((d) => d.file)].map((file) => (0, path.isAbsolute)(file) ? (0, path.relative)(this.rootDir, file) : file),
675
- unbundledComputedImports: true,
676
- packageDirs: this.externalPackageDirs.map((dir) => (0, path.relative)(this.rootDir, dir)),
677
- linkedDirs: this.externalLinkedDirs.map((dir) => (0, path.relative)(this.rootDir, dir)),
678
- outputDirs: this.externalOutputDirs.map((dir) => (0, path.relative)(this.rootDir, dir))
679
- },
680
- warn: (m) => this.warn(m),
681
- info: (m) => this.log(m),
682
- strict: this.options.strict,
683
- diagnostics: this.options.diagnostics,
684
- reportErrors: this.reportErrors(),
685
- extraDiagnostics: ingestionFailures,
686
- kitDescriptors: this.kitDescriptors
687
- });
688
- } catch (e) {
689
- const message = `[animus-extract] analyzeProject failed: ${e}`;
690
- if (e instanceof _animus_ui_extract_pipeline.DiagnosticFailure) throw new Error(message, { cause: e });
691
- if (this.options.strict) {
692
- if (this.storedManifest === null) throw new Error(message, { cause: e });
693
- this.strictFailure(message, e);
694
- return false;
695
- }
696
- console.warn("[animus-extract] analyzeProject failed:", e);
697
- return false;
698
- }
699
- this.assertRuntimeImportSuppliesTerminals(result.manifest);
700
- (0, _animus_ui_extract_pipeline.reportSurvivingAssetPlaceholders)((0, _animus_ui_extract_pipeline.generatedModuleCode)(result.manifest), {
701
- strict: this.options.strict,
702
- warn: (message) => this.warn(message),
703
- reportErrors: this.reportErrors(),
704
- prefix: "[animus-extract]",
705
- surface: "generated runtime modules"
706
- });
707
- this.storedManifest = result.manifest;
708
- this.storedManifestJson = result.manifestJson;
709
- this.ingestionFailureDiagnostics = ingestionFailures;
710
- this.storedSystemPropMapJson = JSON.stringify(result.manifest.system_prop_map);
711
- this.storedDynamicPropsJson = JSON.stringify(result.manifest.dynamic_props);
712
- this.storedAdmittedTransformsJson = JSON.stringify(result.manifest.admitted_transforms);
713
- this.storedTypedSystemPropsJson = JSON.stringify(result.manifest.typed_system_props);
714
- this.reverseProvenance = result.manifest.reverse_provenance;
715
- this.storedSheets = result.manifest.sheets;
716
- this.globalCss = result.globalCss;
717
- this.resolvedComponentCss = result.componentCss;
718
- this.applyAssetSubstitutions();
719
- return true;
720
- }
721
- assertRuntimeImportSuppliesTerminals(manifest) {
722
- const override = this.options.runtimeImport;
723
- if (!override || override === "@animus-ui/system") return;
724
- const offenders = [];
725
- for (const [id, descriptor] of Object.entries(manifest.components)) if (/\bcreateComponent\(|\bcreateComposedFamily\(/.test(descriptor.replacement)) offenders.push(id);
726
- if (offenders.length === 0) return;
727
- const shown = offenders.slice(0, 5).join(", ");
728
- throw new Error(`[animus-extract] runtimeImport '${override}' is valid only when every extracted terminal is .asClass(), but ${offenders.length} component(s) need createComponent/createComposedFamily from the default '@animus-ui/system' runtime: ${shown}` + (offenders.length > 5 ? ", …" : "") + `. Remove the override or convert these terminals to .asClass().`);
729
- }
730
- /** The one ingest → quarantine → analyze → publish transaction. The corpus
731
- * publishes only on success. `afterIngestion` runs after quarantine even
732
- * when an aborted parse rejects the attempt; `beforeAnalysis` runs only
733
- * once the attempt will analyze. A rejection names its aborted originals
734
- * and holds publication: development callers keep their cache's latest
735
- * sources, valid edits included, for the repair to publish together. In
736
- * every strictness mode it fails a production build, which has no
737
- * generation to keep, and only warns in development: the host reports the
738
- * syntax error itself, and a throw would leave its client on the failed
739
- * module. The publication that ends a hold invalidates the modules of
740
- * every original whose admitted bytes changed meanwhile and returns them
741
- * as `redeliver`, for a hot update to push to the client. An owed system
742
- * reload loads the latest system for the attempt: a hold restores the
743
- * published one and keeps the reload owed, any other outcome completes
744
- * the reload's invalidation. */
745
- async analyzeIngested(options) {
746
- const holding = this.abortedParseHashes.size > 0;
747
- const previous = this.corpus.published.ownership;
748
- const reloadingSystem = this.systemReloadOwed;
749
- const prevPlans = reloadingSystem ? (0, _animus_ui_extract_pipeline.snapshotFilePlans)(this.storedManifest) : null;
750
- const publishedSystem = this.system;
751
- let published = false;
752
- try {
753
- if (reloadingSystem) this.loadSystem();
754
- const accepted = await this.corpus.prepare(options?.rawEntries ?? {
755
- fileCache: this.fileCache,
756
- externalFileOwners: this.externalFileOwners
757
- });
758
- options?.afterIngestion?.(accepted);
759
- const rejection = this.corpus.rejection(accepted);
760
- if (rejection) {
761
- for (const [path$3, hash] of rejection.originals) this.abortedParseHashes.set(path$3, hash);
762
- if (this.isProd) throw new Error(rejection.message);
763
- this.warn(rejection.message);
764
- return {
765
- ok: false,
766
- accepted,
767
- abortedOriginals: rejection.originals,
768
- endedHold: false,
769
- redeliver: []
770
- };
771
- }
772
- if (reloadingSystem) (0, _animus_ui_extract_pipeline.clearEngineCache)(this.engineApi);
773
- options?.beforeAnalysis?.(accepted);
774
- const ok = this.runAnalysis(accepted.analysisEntries, accepted.skippedOriginals) !== false;
775
- if (!ok) return {
776
- ok,
777
- accepted,
778
- endedHold: false,
779
- redeliver: []
780
- };
781
- this.publishSourceIngestion(accepted);
782
- published = true;
783
- this.abortedParseHashes.clear();
784
- if (!holding) return {
785
- ok,
786
- accepted,
787
- endedHold: false,
788
- redeliver: []
789
- };
790
- const redeliver = Object.values(accepted.ownership).filter((owner) => previous[owner.originalPath]?.originalHash !== owner.originalHash).map((owner) => owner.originalPath);
791
- invalidateFileModules(this, redeliver);
792
- return {
793
- ok,
794
- accepted,
795
- endedHold: true,
796
- redeliver
797
- };
798
- } finally {
799
- const systemHeld = reloadingSystem && this.abortedParseHashes.size > 0;
800
- if (systemHeld) this.system = publishedSystem;
801
- else if (reloadingSystem) {
802
- this.systemReloadOwed = false;
803
- if (published && prevPlans) invalidateFileModules(this, (0, _animus_ui_extract_pipeline.diffFilePlans)(prevPlans, (0, _animus_ui_extract_pipeline.snapshotFilePlans)(this.storedManifest)));
699
+ this.warn(`[animus-extract] could not copy edited asset ${source}: ${String(err)}`);
804
700
  }
805
- if (reloadingSystem) this.invalidateSystemReloadModules(!systemHeld);
806
701
  }
807
- }
808
- publishSourceIngestion(result) {
809
- this.corpus.publish(result);
810
- this.externalFileOwners = (0, _animus_ui_extract_pipeline.projectExternalFileOwners)(result, this.externalFileOwners);
811
- this.enforceExternalTokenContracts();
812
- }
813
- devFsUrl(absPath) {
814
- return `${this.base.endsWith("/") ? this.base.slice(0, -1) : this.base}/@fs${absPath}`;
702
+ this.invalidateSystemReloadModules(true);
815
703
  }
816
704
  assetFallback(specifier, message, cause) {
817
705
  if (this.options.strict) this.strictFailure(message, cause);
@@ -823,8 +711,8 @@ var PluginContext = class {
823
711
  sheetAssetSpecifiers() {
824
712
  return (0, _animus_ui_extract_pipeline.findSheetAssetSpecifiers)({
825
713
  variableCss: this.system.variableCss,
826
- globalCss: this.globalCss,
827
- componentCss: this.resolvedComponentCss
714
+ globalCss: this.analysisGlobalCss,
715
+ componentCss: this.analysisComponentCss
828
716
  });
829
717
  }
830
718
  /** `system.variableCss` with its asset() placeholders substituted from
@@ -837,18 +725,6 @@ var PluginContext = class {
837
725
  variableCss: this.system.variableCss
838
726
  }, this.assetUrlBySpecifier).variableCss;
839
727
  }
840
- /** Substitutes the analysis sheets in place, through the same step as the
841
- * session; the theme's variable CSS is substituted on read
842
- * (`variableCss`). */
843
- substituteSheets() {
844
- const substituted = (0, _animus_ui_extract_pipeline.substituteSheetAssets)({
845
- ...NO_SHEETS,
846
- globalCss: this.globalCss,
847
- componentCss: this.resolvedComponentCss
848
- }, this.assetUrlBySpecifier);
849
- this.globalCss = substituted.globalCss;
850
- this.resolvedComponentCss = substituted.componentCss;
851
- }
852
728
  /** Resolves asset specifiers a system reload introduced. Node-side only —
853
729
  * the plugin hook context is unavailable here, so results can differ. */
854
730
  applyAssetSubstitutions() {
@@ -856,44 +732,9 @@ var PluginContext = class {
856
732
  if (this.assetResolutionFailures.delete(specifier)) this.assetUrlBySpecifier.delete(specifier);
857
733
  if (this.assetUrlBySpecifier.has(specifier)) continue;
858
734
  const resolved = (0, _animus_ui_extract_pipeline.resolveAssetFile)(specifier, this.rootDir, this.pathAliasesJson);
859
- if (resolved) this.assetUrlBySpecifier.set(specifier, this.devFsUrl(resolved));
735
+ if (resolved) this.assetUrlBySpecifier.set(specifier, this.devAssetUrl(specifier, resolved));
860
736
  else this.assetFallback(specifier, `[animus-extract] unresolvable asset() specifier: ${specifier}`);
861
737
  }
862
- this.substituteSheets();
863
- }
864
- resetCoalescer = null;
865
- requestSystemReload(trigger) {
866
- this.log(`HMR system reload scheduled: ${trigger}`);
867
- this.resetCoalescer ??= new ResetCoalescer(
868
- async () => this.performSystemReload(),
869
- /** The coalescer fires from a bare timer, outside Vite's error
870
- * handling, so an unhandled strict throw would exit the process. */
871
- (err) => {
872
- const display = String(err);
873
- this.warn(`[animus-extract] system reload failed: ${display}`);
874
- const message = err instanceof Error ? err.message : String(err);
875
- const stack = err instanceof Error ? err.stack ?? "" : "";
876
- this.devServer?.hot?.send({
877
- type: "error",
878
- err: {
879
- message,
880
- stack,
881
- plugin: "animus-extract"
882
- }
883
- });
884
- }
885
- );
886
- this.resetCoalescer.request();
887
- }
888
- async performSystemReload() {
889
- return runExclusiveAnalysis(this, () => this.performSystemReloadExclusive());
890
- }
891
- async performSystemReloadExclusive() {
892
- const resetStart = performance.now();
893
- this.systemReloadOwed = true;
894
- const { ok } = await this.analyzeIngested(await this.rediscoverSources?.());
895
- if (!ok) return;
896
- this.log(`HMR system reload complete: ${Math.round(performance.now() - resetStart)}ms`);
897
738
  }
898
739
  invalidateSystemReloadModules(reloadClient) {
899
740
  const server = this.devServer;
@@ -909,15 +750,35 @@ var PluginContext = class {
909
750
  if (reloadClient) server.hot?.send({ type: "full-reload" });
910
751
  }
911
752
  isSystemDependency(absFile) {
912
- return (0, _animus_ui_extract_pipeline.toWatchKeys)(absFile).some((key) => this.systemDependencyKeys.has(key));
753
+ return this.session?.isSystemDependency(absFile) ?? false;
913
754
  }
914
- /** Watch every loader-reported dependency path. Vite hard-ignores
915
- * node_modules, so dependencies installed there produce no events. */
755
+ /** Watch every loader-reported dependency path, the external package
756
+ * roots, and the paths the load found absent that no watched directory
757
+ * covers (creating one reloads the system). Vite hard-ignores
758
+ * node_modules, so dependencies installed there produce no events. A list
759
+ * already registered is not added again. */
916
760
  registerSystemWatchPaths() {
917
761
  const watcher = this.devServer?.watcher;
918
- if (!watcher) return;
919
- if (this.systemDependencyPaths.length > 0) watcher.add(this.systemDependencyPaths);
920
- if (this.externalPackageDirs.length > 0) watcher.add(this.externalPackageDirs);
762
+ const session = this.session;
763
+ if (!watcher || !session) return;
764
+ for (const paths of [
765
+ session.systemDependencyPaths,
766
+ session.systemMissingPaths,
767
+ session.externalPackageDirs
768
+ ]) {
769
+ if (paths.length === 0 || this.watchedPathLists.has(paths)) continue;
770
+ this.watchedPathLists.add(paths);
771
+ const added = paths === session.systemMissingPaths ? this.uncoveredMissingPaths(paths, session.externalPackageDirs) : paths;
772
+ if (added.length > 0) watcher.add(added);
773
+ }
774
+ }
775
+ /** The absent paths no watched directory reports a creation for. Vite's
776
+ * root watch and a package root's watch already do, and a watch on an
777
+ * absent file inside a watched directory stops that directory's events
778
+ * under inotify (chokidar's non-fsevents handler). */
779
+ uncoveredMissingPaths(paths, packageDirs) {
780
+ const watchedDirs = this.rootDir ? [this.rootDir, ...packageDirs] : packageDirs;
781
+ return paths.filter((path$7) => !watchedDirs.some((dir) => (0, _animus_ui_extract_pipeline.isPathWithinRoot)(dir, path$7)));
921
782
  }
922
783
  /** Invalidate the component CSS and system-props modules, then reload. The
923
784
  * reload alone rescues nothing: Vite keeps serving its cached transform.
@@ -930,33 +791,7 @@ var PluginContext = class {
930
791
  const mod = server.moduleGraph.getModuleById(moduleId);
931
792
  if (mod) server.moduleGraph.invalidateModule(mod);
932
793
  }
933
- if (this.pendingReloadTimer) return;
934
- this.pendingReloadTimer = setTimeout(() => {
935
- this.pendingReloadTimer = null;
936
- this.devServer?.hot?.send({ type: "full-reload" });
937
- }, 100);
938
- }
939
- /** An unresolvable `.includes()` specifier or a stale dist entry under an
940
- * extended package warns, and fails the build under `strict` (strict
941
- * development reports it). */
942
- enforceIncludeResolution() {
943
- for (const message of [(0, _animus_ui_extract_pipeline.unresolvableIncludesMessage)(this.externalPackageOutcomes), (0, _animus_ui_extract_pipeline.staleDistIncludesMessage)(this.externalPackageOutcomes)]) {
944
- if (message === null) continue;
945
- if (this.options.strict) this.strictFailure(message);
946
- else this.warn(message);
947
- }
948
- }
949
- enforceExternalTokenContracts() {
950
- (0, _animus_ui_extract_pipeline.enforceExternalTokenContracts)({
951
- diagnostics: this.storedManifest?.diagnostics,
952
- fileOwners: this.externalFileOwners,
953
- dirOwners: this.externalDirOwners,
954
- sourceThemeManifestsJson: this.system.sourceThemeManifestsJson,
955
- strict: this.options.strict,
956
- prefix: "[animus-extract]",
957
- warn: (message) => this.warn(message),
958
- reportErrors: this.reportErrors()
959
- });
794
+ this.reloadWindow.note();
960
795
  }
961
796
  runSelfVerify() {
962
797
  const failures = (0, _animus_ui_extract_pipeline.runStructuralSelfCheck)({
@@ -981,131 +816,113 @@ var PluginContext = class {
981
816
  }
982
817
  };
983
818
  //#endregion
984
- //#region src/rediscovery.ts
985
- /** The current manifest's unresolved-parent drops, parsed from diagnostics. */
986
- function unresolvedParentDrops(ctx) {
987
- const drops = [];
988
- const manifest = ctx.storedManifest;
989
- if (!manifest) return drops;
990
- for (const d of manifest.diagnostics) {
991
- const parent = (0, _animus_ui_extract_pipeline.unresolvedParentName)(d);
992
- if (parent !== null) drops.push({
993
- file: d.file,
994
- component: d.component,
995
- parent
996
- });
997
- }
998
- return drops;
999
- }
1000
- const EMPTY_DROP_FILES = /* @__PURE__ */ new Set();
1001
- const dropFilesByManifest = /* @__PURE__ */ new WeakMap();
1002
- /** Files carrying an unresolved-parent drop in the current manifest. */
1003
- function unresolvedDropFiles(ctx) {
1004
- const manifest = ctx.storedManifest;
1005
- if (!manifest) return EMPTY_DROP_FILES;
1006
- let files = dropFilesByManifest.get(manifest);
1007
- if (!files) {
1008
- const derived = /* @__PURE__ */ new Set();
1009
- for (const d of manifest.diagnostics) if ((0, _animus_ui_extract_pipeline.isUnresolvedParentDrop)(d)) derived.add(d.file);
1010
- files = derived;
1011
- dropFilesByManifest.set(manifest, files);
1012
- }
1013
- return files;
1014
- }
819
+ //#region src/module-invalidation.ts
1015
820
  /**
1016
- * Folds undiscovered on-disk files when the manifest reports an unresolved
1017
- * parent, then re-analyzes; the iteration cap is containment, not semantics.
821
+ * Completeness is load-bearing: the file-plan diff and the sources re-delivered
822
+ * when a held publication ends are the only invalidation candidate sources, so
823
+ * a node shape missed here stays stale for the session.
1018
824
  */
1019
- async function reconcileSourceCorpus(ctx) {
1020
- if (ctx.isProd) return false;
1021
- let reanalyzed = false;
1022
- for (let iteration = 0; iteration < 3; iteration++) {
1023
- if (unresolvedDropFiles(ctx).size === 0) {
1024
- warnedVerdicts.get(ctx)?.clear();
1025
- return reanalyzed;
1026
- }
1027
- const drops = unresolvedParentDrops(ctx);
1028
- const dropKey = drops.map((d) => `${d.file}\0${d.component}\0${d.parent}`).sort().join("\n");
1029
- const memo = barrenWalkMemos.get(ctx);
1030
- if (memo && memo.dropKey === dropKey && memo.cacheGeneration === ctx.fileCacheGeneration) return reanalyzed;
1031
- const recordedFailures = ctx.ingestionFailureDiagnostics.length;
1032
- const folded = foldUndiscoveredFiles(ctx);
1033
- const newlyUnreadable = ctx.ingestionFailureDiagnostics.length > recordedFailures;
1034
- if (folded.length === 0 && !newlyUnreadable) {
1035
- barrenWalkMemos.set(ctx, {
1036
- dropKey,
1037
- cacheGeneration: ctx.fileCacheGeneration
1038
- });
1039
- warnInadmissibleParents(ctx, drops);
1040
- return reanalyzed;
1041
- }
1042
- barrenWalkMemos.delete(ctx);
1043
- if (folded.length > 0) ctx.log(`rediscovery: folded ${folded.length} on-disk file(s) after unresolved-parent drop`);
1044
- reanalyzed = true;
1045
- let published = false;
1046
- let held = false;
1047
- try {
1048
- const analysis = await ctx.analyzeIngested();
1049
- published = analysis.ok;
1050
- held = analysis.abortedOriginals !== void 0;
1051
- } finally {
1052
- if (!published && !held) ctx.mutateFileCache((cache) => {
1053
- for (const key of folded) cache.delete(key);
1054
- });
825
+ function invalidateFileModules(ctx, relPaths) {
826
+ const server = ctx.devServer;
827
+ if (!server || relPaths.length === 0) return 0;
828
+ const graphs = server.environments ? Object.values(server.environments).map((env) => env?.moduleGraph).filter(Boolean) : [server.moduleGraph].filter(Boolean);
829
+ let total = 0;
830
+ for (const relPath of relPaths) {
831
+ const absPath = (0, path.resolve)(ctx.rootDir, relPath);
832
+ let fileCount = 0;
833
+ for (const graph of graphs) {
834
+ const nodes = /* @__PURE__ */ new Set();
835
+ const byFile = graph.getModulesByFile?.(absPath);
836
+ if (byFile) for (const mod of byFile) nodes.add(mod);
837
+ const byId = graph.getModuleById?.(absPath);
838
+ if (byId) nodes.add(byId);
839
+ for (const mod of nodes) {
840
+ graph.invalidateModule(mod);
841
+ fileCount++;
842
+ }
1055
843
  }
1056
- if (!published) return reanalyzed;
844
+ if (fileCount > 0) ctx.trace(`HMR invalidate: ${relPath} (plan changed, ${fileCount} nodes)`);
845
+ total += fileCount;
1057
846
  }
1058
- if (unresolvedDropFiles(ctx).size > 0) ctx.warn("source-corpus rediscovery did not stabilize after bounded retries");
1059
- return reanalyzed;
847
+ return total;
1060
848
  }
1061
- /** Returns the cache keys this fold added: a failed analysis must restore
1062
- * the cache, or the content-hash gate suppresses the retry forever. A file
1063
- * it cannot read joins the context's ingestion failures instead. */
1064
- function foldUndiscoveredFiles(ctx) {
1065
- const excludeMatcher = ctx.excludeMatcher;
1066
- const filePaths = (0, _animus_ui_extract_pipeline.discoverFiles)(ctx.rootDir, ctx.rootDir, excludeMatcher, ctx.extensionsSet);
1067
- const folded = [];
1068
- const pending = [];
1069
- for (const filePath of filePaths) {
1070
- if ((0, path.extname)(filePath) === ".mdx") continue;
1071
- const relPath = (0, path.relative)(ctx.rootDir, filePath);
1072
- if (ctx.fileCache.has(relPath)) continue;
1073
- let source;
1074
- try {
1075
- source = (0, fs.readFileSync)(filePath, "utf-8");
1076
- } catch (err) {
1077
- if (!(0, _animus_ui_extract_pipeline.isDeletedSource)(err) && !ctx.ingestionFailureDiagnostics.some((d) => d.file === relPath)) ctx.ingestionFailureDiagnostics.push((0, _animus_ui_extract_pipeline.unreadableSourceDiagnostic)(relPath, err));
1078
- continue;
1079
- }
1080
- pending.push([relPath, {
1081
- hash: (0, _animus_ui_extract_pipeline.contentHash)(source),
1082
- source
1083
- }]);
1084
- folded.push(relPath);
1085
- }
1086
- if (pending.length > 0) ctx.mutateFileCache((cache) => {
1087
- for (const [relPath, entry] of pending) cache.set(relPath, entry);
1088
- });
1089
- return folded;
849
+ //#endregion
850
+ //#region src/rediscovery.ts
851
+ const NO_DROPS = /* @__PURE__ */ new Map();
852
+ const dropsByManifest = /* @__PURE__ */ new WeakMap();
853
+ /** The current manifest's extensions dropped over their parent, by file:
854
+ * the engine's typed missing-input evidence (spec §12.2 row 11). */
855
+ function droppedExtensions(ctx) {
856
+ const manifest = ctx.storedManifest;
857
+ if (!manifest) return NO_DROPS;
858
+ let drops = dropsByManifest.get(manifest);
859
+ if (!drops) {
860
+ drops = (0, _animus_ui_extract_pipeline.servedFiles)(manifest).dropped;
861
+ dropsByManifest.set(manifest, drops);
862
+ }
863
+ return drops;
1090
864
  }
1091
- /** Per context: the drop tuple-set and the cache mutation generation at the
1092
- * last walk that folded nothing. */
1093
- const barrenWalkMemos = /* @__PURE__ */ new WeakMap();
1094
865
  /** Per-context (file, parent, condition) verdicts already warned — each
1095
866
  * condition warns once; cleared when the drops disappear. */
1096
867
  const warnedVerdicts = /* @__PURE__ */ new WeakMap();
1097
- const PARENT_PROBE_EXTENSIONS = /* @__PURE__ */ new Set([
1098
- ".tsx",
1099
- ".ts",
1100
- ".jsx",
1101
- ".js"
1102
- ]);
868
+ /** Where a parent the analysis does not hold is on disk, probed from its
869
+ * base under the host's extension order: one no analysed module answers
870
+ * (`absent`), or one the host opens but the analysis never read
871
+ * (`incomplete`), such as a file the watcher has not reported yet. */
872
+ function parentOnDisk(ctx, input) {
873
+ if (input.outcome !== "absent" && input.outcome !== "incomplete" || input.base === null) return { kind: "none" };
874
+ const probed = (0, _animus_ui_extract_pipeline.probeAbsolutePathSpecifier)((0, path.resolve)(ctx.rootDir, input.base), void 0, ctx.rootDir, ctx.extensionPolicy);
875
+ if (probed.kind === "absent") return { kind: "none" };
876
+ if (probed.kind === "case-mismatch") return {
877
+ kind: "inadmissible",
878
+ reason: `is unresolved: ${(0, _animus_ui_extract_pipeline.caseMismatchReason)(`the import '${input.specifier}'`, {
879
+ candidate: (0, path.relative)(ctx.rootDir, probed.candidate),
880
+ actual: (0, path.relative)(ctx.rootDir, probed.actual)
881
+ }, "spell the import as the file is spelled.")}`
882
+ };
883
+ const path$5 = probed.path;
884
+ const relPath = (0, path.relative)(ctx.rootDir, path$5);
885
+ const excludedBy = ctx.excludeMatcher.explain(path$5, relPath);
886
+ const reason = excludedBy ? `which is excluded by pattern '${excludedBy}'. Include that file in the extraction sources or remove this extension.` : !(0, _animus_ui_extract_pipeline.isPathWithinRoot)(ctx.rootDir, path$5) ? "outside the extraction root. Declare its package in the system includes or move it under the project root." : !ctx.extensionsSet.has((0, path.extname)(path$5)) ? `whose extension is not in the configured source extensions.` : (0, path.extname)(path$5) === ".mdx" || ctx.session?.heldSources.has(relPath) ? null : void 0;
887
+ if (reason === void 0) return {
888
+ kind: "fold",
889
+ path: path$5
890
+ };
891
+ if (reason === null) return { kind: "none" };
892
+ return {
893
+ kind: "inadmissible",
894
+ reason: `resolves to ${relPath}, ${reason}`
895
+ };
896
+ }
1103
897
  /**
1104
- * Warns for parents that resolve on disk but cannot be admitted. Resolution
1105
- * is a best-effort scan of the consumer's own relative import specifiers.
898
+ * Folds the on-disk files the manifest's dropped extensions name as missing
899
+ * parents into the Session, in one bounded pass, and returns the outcomes
900
+ * of that attempt. A parent no file answers, or one that cannot be
901
+ * admitted, leaves its drop standing: the transform then fails rather than
902
+ * serving builder code (SA1).
1106
903
  */
1107
- function warnInadmissibleParents(ctx, drops) {
1108
- const excludeMatcher = ctx.excludeMatcher;
904
+ async function reconcileSourceCorpus(ctx) {
905
+ if (ctx.isProd || ctx.session === null) return [];
906
+ const drops = [...droppedExtensions(ctx).values()];
907
+ if (drops.length === 0) {
908
+ warnedVerdicts.get(ctx)?.clear();
909
+ return [];
910
+ }
911
+ const folded = /* @__PURE__ */ new Set();
912
+ const inadmissible = [];
913
+ for (const drop of drops) {
914
+ const onDisk = parentOnDisk(ctx, drop);
915
+ if (onDisk.kind === "fold") folded.add(onDisk.path);
916
+ else if (onDisk.kind === "inadmissible") inadmissible.push([drop, onDisk.reason]);
917
+ }
918
+ warnInadmissibleParents(ctx, drops, inadmissible);
919
+ if (folded.size === 0) return [];
920
+ ctx.log(`rediscovery: folded ${folded.size} on-disk file(s) a dropped extension names as its parent`);
921
+ return ctx.submitChanges({ modifiedFiles: folded });
922
+ }
923
+ /** Warns, once per condition, for parents that resolve on disk but cannot
924
+ * be admitted. */
925
+ function warnInadmissibleParents(ctx, drops, inadmissible) {
1109
926
  let warned = warnedVerdicts.get(ctx);
1110
927
  if (!warned) {
1111
928
  warned = /* @__PURE__ */ new Set();
@@ -1113,31 +930,51 @@ function warnInadmissibleParents(ctx, drops) {
1113
930
  }
1114
931
  const active = new Set(drops.map((d) => `${d.file}\0${d.parent}`));
1115
932
  for (const key of [...warned]) if (!active.has(key.slice(0, key.lastIndexOf("\0")))) warned.delete(key);
1116
- for (const drop of drops) {
1117
- const consumer = ctx.fileCache.get(drop.file);
1118
- if (!consumer) continue;
1119
- const specifier = importSpecifierFor(consumer.source, drop.parent);
1120
- if (!specifier || !specifier.startsWith(".")) continue;
1121
- const resolved = (0, _animus_ui_extract_pipeline.resolveAbsolutePathSpecifier)((0, path.resolve)(ctx.rootDir, (0, path.dirname)(drop.file), specifier), PARENT_PROBE_EXTENSIONS);
1122
- if (!resolved) continue;
1123
- const relPath = (0, path.relative)(ctx.rootDir, resolved);
1124
- const excludedBy = excludeMatcher.explain(resolved, relPath);
1125
- const reason = excludedBy ? `which is excluded by pattern '${excludedBy}'. Include that file in the extraction sources or remove this extension.` : !(0, _animus_ui_extract_pipeline.isPathWithinRoot)(ctx.rootDir, resolved) ? "outside the extraction root. Declare its package in the system includes or move it under the project root." : !ctx.extensionsSet.has((0, path.extname)(resolved)) ? `whose extension is not in the configured source extensions.` : null;
1126
- if (reason === null) continue;
933
+ for (const [drop, reason] of inadmissible) {
1127
934
  const verdictKey = `${drop.file}\0${drop.parent}\0${reason}`;
1128
935
  if (warned.has(verdictKey)) continue;
1129
936
  warned.add(verdictKey);
1130
- ctx.warn(`cannot extract '${drop.component}': parent '${drop.parent}' resolves to ${relPath}, ${reason}`);
937
+ ctx.warn(`cannot extract '${drop.component}': parent '${drop.parent}' ${reason}`);
1131
938
  }
1132
939
  }
1133
- function importSpecifierFor(source, binding) {
1134
- const importRe = /import\s+([^;]+?)\s+from\s+['"]([^'"]+)['"]/g;
1135
- let match;
1136
- while ((match = importRe.exec(source)) !== null) {
1137
- const clause = match[1];
1138
- if (new RegExp(`[{,\\s]${binding}[,\\s}]|^${binding}$`).test(clause)) return match[2];
940
+ //#endregion
941
+ //#region src/session-outcomes.ts
942
+ /** The file event as a Session change set. */
943
+ function changesOf(type, absFile) {
944
+ const files = /* @__PURE__ */ new Set([absFile]);
945
+ return type === "delete" ? { removedFiles: files } : { modifiedFiles: files };
946
+ }
947
+ /** What the published outcomes of one submission change, merged. */
948
+ function mergePublished(outcomes) {
949
+ const merged = {
950
+ published: false,
951
+ full: false,
952
+ endedHold: false,
953
+ systemPropsChanged: false,
954
+ planChanged: /* @__PURE__ */ new Set(),
955
+ redeliver: /* @__PURE__ */ new Set()
956
+ };
957
+ for (const { outcome, systemPropsChanged } of outcomes) {
958
+ if (outcome.kind !== "published") continue;
959
+ merged.published = true;
960
+ merged.full ||= outcome.pass === "full";
961
+ merged.endedHold ||= outcome.endedHold;
962
+ merged.systemPropsChanged ||= systemPropsChanged;
963
+ for (const path of outcome.planChanged) merged.planChanged.add(path);
964
+ for (const path of outcome.redeliver) merged.redeliver.add(path);
965
+ }
966
+ return merged;
967
+ }
968
+ /** Acts on what every submission's publications owe the module graph: a
969
+ * publication that ends a hold re-delivers the originals whose bytes
970
+ * changed meanwhile, and a full pass (a system reload) reloads the
971
+ * client. */
972
+ function applyPublished(ctx, merged) {
973
+ if (merged.redeliver.size > 0) invalidateFileModules(ctx, [...merged.redeliver]);
974
+ if (merged.full) {
975
+ invalidateFileModules(ctx, [...merged.planChanged]);
976
+ ctx.invalidateSystemReloadModules(true);
1139
977
  }
1140
- return null;
1141
978
  }
1142
979
  //#endregion
1143
980
  //#region src/transform.ts
@@ -1154,7 +991,7 @@ function applyDevBridgeImport(code) {
1154
991
  function extendingFiles(ctx, analysisPaths) {
1155
992
  const manifest = ctx.storedManifest;
1156
993
  const files = /* @__PURE__ */ new Set();
1157
- const queue = analysisPaths.flatMap((path$2) => manifest?.files[path$2] ?? []);
994
+ const queue = analysisPaths.flatMap((path$3) => manifest?.files[path$3] ?? []);
1158
995
  const seen = new Set(queue);
1159
996
  while (queue.length > 0) {
1160
997
  const id = queue.shift();
@@ -1168,12 +1005,11 @@ function extendingFiles(ctx, analysisPaths) {
1168
1005
  }
1169
1006
  return files;
1170
1007
  }
1171
- function rawFallbackDescendants(ctx, relPath) {
1172
- return [...extendingFiles(ctx, [relPath])].filter((file) => file !== relPath && ctx.rawExtensionFallbacks.has(file)).sort();
1173
- }
1174
- async function transformSource(ctx, code, id) {
1175
- if (!ctx.storedManifest) return null;
1176
- if (id.startsWith("\0") || id.startsWith("virtual:animus/")) return null;
1008
+ async function transformSource(ctx, code, requestId) {
1009
+ if (requestId.startsWith("\0") || requestId.startsWith("virtual:animus/")) return null;
1010
+ const query = requestId.indexOf("?");
1011
+ if (query !== -1 && !/^v=[\w-]+$/.test(requestId.slice(query + 1))) return null;
1012
+ const id = query === -1 ? requestId : requestId.slice(0, query);
1177
1013
  const isExternalPkg = ctx.externalPackageDirs.some((dir) => (0, _animus_ui_extract_pipeline.isPathWithinRoot)(dir, id));
1178
1014
  const relativePath = (0, path.relative)(ctx.rootDir, id);
1179
1015
  if (!isExternalPkg) {
@@ -1183,57 +1019,36 @@ async function transformSource(ctx, code, id) {
1183
1019
  }
1184
1020
  const abortedHash = ctx.abortedParseHashes.get(relativePath);
1185
1021
  if (abortedHash !== void 0 && abortedHash === (0, _animus_ui_extract_pipeline.contentHash)(code)) return null;
1022
+ if (!ctx.storedManifest) throw new Error(`[animus-extract] no analysis is available to transform ${relativePath}: the project analysis failed; see the error above.`);
1023
+ let detectionFailed = false;
1186
1024
  if (!ctx.storedManifest.files[relativePath]?.length) {
1187
- if (!ctx.isProd && !ctx.fileCache.has(relativePath)) await runExclusiveAnalysis(ctx, async () => {
1188
- if (ctx.fileCache.has(relativePath)) return;
1189
- if (isExternalPkg) {
1190
- const owner = Object.entries(ctx.externalDirOwners).find(([dir]) => (0, _animus_ui_extract_pipeline.isPathWithinRoot)(dir, id));
1191
- if (owner) ctx.externalFileOwners[relativePath] = owner[1];
1192
- }
1193
- const hash = (0, _animus_ui_extract_pipeline.contentHash)(code);
1194
- ctx.mutateFileCache((cache) => cache.set(relativePath, {
1195
- hash,
1196
- source: code
1197
- }));
1198
- const prevPlans = (0, _animus_ui_extract_pipeline.snapshotFilePlans)(ctx.storedManifest);
1199
- let analysisOk = false;
1200
- let held = false;
1025
+ const session = ctx.session;
1026
+ if (!ctx.isProd && session !== null && !session.heldSources.has(relativePath)) {
1027
+ let outcomes = [];
1201
1028
  try {
1202
- const analysis = await ctx.analyzeIngested();
1203
- analysisOk = analysis.ok;
1204
- held = analysis.abortedOriginals !== void 0;
1205
- } finally {
1206
- if (!analysisOk && !held) ctx.mutateFileCache((cache) => cache.delete(relativePath));
1029
+ outcomes = await ctx.submitChanges(changesOf("create", id));
1030
+ } catch (error) {
1031
+ ctx.warn(error instanceof Error ? error.message : String(error));
1032
+ detectionFailed = true;
1207
1033
  }
1208
- if (analysisOk) {
1209
- await reconcileSourceCorpus(ctx);
1210
- invalidateFileModules(ctx, (0, _animus_ui_extract_pipeline.diffFilePlans)(prevPlans, (0, _animus_ui_extract_pipeline.snapshotFilePlans)(ctx.storedManifest), { exclude: relativePath }));
1034
+ if (mergePublished(outcomes).published) {
1035
+ const merged = mergePublished([...outcomes, ...await reconcileSourceCorpus(ctx)]);
1036
+ applyPublished(ctx, merged);
1037
+ if (!merged.full) invalidateFileModules(ctx, [...merged.planChanged].filter((path$4) => path$4 !== relativePath));
1211
1038
  const compCount = ctx.storedManifest?.files[relativePath]?.length ?? 0;
1212
1039
  ctx.info(`New file detected: ${relativePath} — ${compCount ? `${compCount} components extracted` : "no components"}`);
1213
1040
  ctx.invalidateExtractedModules();
1214
1041
  }
1215
- });
1216
- if (!ctx.storedManifest.files[relativePath]?.length) {
1217
- ctx.recordFallbackState(relativePath, unresolvedDropFiles(ctx).has(relativePath));
1218
- return null;
1219
- }
1220
- }
1221
- if (!ctx.isProd && ctx.rawExtensionFallbacks.size > 0) {
1222
- const conflicted = rawFallbackDescendants(ctx, relativePath);
1223
- if (conflicted.length > 0) {
1224
- invalidateFileModules(ctx, conflicted);
1225
- ctx.invalidateExtractedModules();
1226
- for (const file of conflicted) ctx.rawExtensionFallbacks.delete(file);
1227
- throw new Error(`ANIMUS_COMPOSITION_RECOVERING: '${relativePath}' extracted while extension consumer(s) ${conflicted.join(", ")} are still serving the runtime fallback — recovering via reload; this resolves on the next request.`);
1228
1042
  }
1043
+ if (detectionFailed) throw new Error(`[animus-extract] the analysis of the new file ${relativePath} failed; see the error above.`);
1229
1044
  }
1045
+ const drop = droppedExtensions(ctx).get(relativePath);
1046
+ if (drop !== void 0) throw new Error((0, _animus_ui_extract_pipeline.droppedExtensionMessage)(drop));
1047
+ if (!ctx.storedManifest.files[relativePath]?.length) return null;
1230
1048
  try {
1231
1049
  const { transformFile } = ctx.engineApi();
1232
1050
  const result = transformFile(code, relativePath, ctx.storedManifestJson);
1233
- if (!result.hasComponents) {
1234
- ctx.recordFallbackState(relativePath, true);
1235
- return null;
1236
- }
1051
+ if (!result.hasComponents) throw new Error(`the analysis extracted components from ${relativePath}, but its transform found none`);
1237
1052
  if (ctx.tracing) {
1238
1053
  const compCount = ctx.storedManifest.files[relativePath]?.length ?? 0;
1239
1054
  ctx.trace(`transform ${relativePath}: ${compCount} components`);
@@ -1243,35 +1058,30 @@ async function transformSource(ctx, code, id) {
1243
1058
  outputCode = applyDevBridgeImport(result.code);
1244
1059
  ctx.recordTransformOutput(relativePath, outputCode);
1245
1060
  }
1246
- ctx.recordFallbackState(relativePath, false);
1247
1061
  return {
1248
1062
  code: outputCode,
1249
1063
  map: null
1250
1064
  };
1251
1065
  } catch (e) {
1252
- if (ctx.options.strict) ctx.strictFailure(`[animus-extract] Failed to transform ${id}: ${e}`, e);
1253
- else console.warn(`[animus-extract] Failed to transform ${id}:`, e);
1254
- ctx.recordFallbackState(relativePath, true);
1255
- return null;
1066
+ throw new Error(`[animus-extract] Failed to transform ${id}: ${e}`, { cause: e });
1256
1067
  }
1257
1068
  }
1258
1069
  //#endregion
1259
1070
  //#region src/hmr.ts
1260
1071
  /** Vite dispatches this hook once per environment for one file event: the
1261
- * analysis half is claimed by one dispatch, invalidation runs in each. */
1262
- async function handleHotUpdate(ctx, environment, options) {
1072
+ * dispatch that claims it submits the file to the Session, and invalidation
1073
+ * runs in each. */
1074
+ async function handleHotUpdate(ctx, environment, { type, file, timestamp, modules, read }) {
1263
1075
  if (ctx.isProd) return;
1264
- return runExclusiveAnalysis(ctx, () => handleHotUpdateExclusive(ctx, environment, options));
1265
- }
1266
- async function handleHotUpdateExclusive(ctx, environment, { type, file, timestamp, modules, read }) {
1267
1076
  const ownsEvent = ctx.hotUpdateEvents.claim(environment.name, file, timestamp);
1268
1077
  const absFile = (0, path.resolve)(file);
1269
1078
  ctx.trace(`hotUpdate ${type} ${(0, path.relative)(ctx.rootDir, absFile)} env=${environment.name} owns=${ownsEvent}`);
1270
1079
  if (ctx.isSystemDependency(absFile)) {
1271
- if (ownsEvent) {
1272
- await reconcileSourceEntry(ctx, absFile, type, read);
1273
- ctx.requestSystemReload((0, path.relative)(ctx.rootDir, absFile));
1274
- }
1080
+ if (ownsEvent) requestSystemReload(ctx, absFile, type);
1081
+ return [];
1082
+ }
1083
+ if (ctx.assetCopyDir !== null && ctx.isAssetSource(absFile)) {
1084
+ if (ownsEvent && type !== "delete") ctx.refreshAssetCopies(absFile);
1275
1085
  return [];
1276
1086
  }
1277
1087
  if (type === "delete") {
@@ -1295,31 +1105,74 @@ async function handleHotUpdateExclusive(ctx, environment, { type, file, timestam
1295
1105
  }
1296
1106
  return invalidateStaleModules(ctx, environment, modules, result);
1297
1107
  }
1298
- /** Prefers Vite's `read`, which retries on the empty content an atomic save
1299
- * exposes; the direct read serves hosts that supply no `read`. */
1300
- async function readChangedSource(absFile, read) {
1301
- return read ? await read() : (0, fs.readFileSync)(absFile, "utf-8");
1302
- }
1303
- /** Keeps the cache entry of a file that is both a system dependency and a
1304
- * discovered source; a file with no entry must never gain one here. */
1305
- async function reconcileSourceEntry(ctx, absFile, type, read) {
1306
- if (type === "delete") {
1307
- ctx.mutateFileCache((cache) => pruneFileCache(cache, ctx.rootDir, absFile));
1308
- return;
1108
+ /** Each context's pending system-dependency events, and the window they
1109
+ * wait out together. */
1110
+ const systemReloads = /* @__PURE__ */ new WeakMap();
1111
+ /** A system dependency's event waits out the coalescing window (SA19)
1112
+ * before the Session reads it: a watcher can report a write as it starts
1113
+ * and drop the event for its end, so an immediate read would load a
1114
+ * half-written module. Events in one window reload once. */
1115
+ function requestSystemReload(ctx, absFile, type) {
1116
+ let state = systemReloads.get(ctx);
1117
+ if (state === void 0) {
1118
+ const pending = /* @__PURE__ */ new Map();
1119
+ state = {
1120
+ pending,
1121
+ window: new _animus_ui_extract_session.CoalescingWindow(() => {
1122
+ if (pending.size === 0) return;
1123
+ const taken = [...pending];
1124
+ pending.clear();
1125
+ return reloadSystem(ctx, taken);
1126
+ })
1127
+ };
1128
+ systemReloads.set(ctx, state);
1309
1129
  }
1310
- const relPath = (0, path.relative)(ctx.rootDir, absFile);
1311
- if (!ctx.fileCache.has(relPath)) return;
1312
- let source;
1130
+ ctx.log(`HMR system reload scheduled: ${(0, path.relative)(ctx.rootDir, absFile)}`);
1131
+ state.pending.set(absFile, type);
1132
+ state.window.note();
1133
+ }
1134
+ /** Drops a pending system reload: the context is closing. */
1135
+ function cancelSystemReload(ctx) {
1136
+ const state = systemReloads.get(ctx);
1137
+ state?.window.cancel();
1138
+ state?.pending.clear();
1139
+ }
1140
+ /** Reloads the system through the Session over the window's events. It
1141
+ * runs from a timer, so it never throws: a failed reload keeps the last
1142
+ * good system served and warns; under `strict` the client overlay reports
1143
+ * it too. */
1144
+ async function reloadSystem(ctx, events) {
1145
+ const reloadStart = performance.now();
1146
+ const modifiedFiles = /* @__PURE__ */ new Set();
1147
+ const removedFiles = /* @__PURE__ */ new Set();
1148
+ for (const [path$1, type] of events) (type === "delete" ? removedFiles : modifiedFiles).add(path$1);
1149
+ let outcomes;
1313
1150
  try {
1314
- source = await readChangedSource(absFile, read);
1315
- } catch {
1316
- ctx.warn(`could not re-read ${relPath} after a system-dependency edit — system reloads will analyze its pre-edit text until a later edit is read successfully`);
1151
+ outcomes = await ctx.submitChanges({
1152
+ modifiedFiles,
1153
+ removedFiles
1154
+ });
1155
+ } catch (err) {
1156
+ ctx.warn(`[animus-extract] system reload failed: ${String(err)}`);
1157
+ if (ctx.options.strict) {
1158
+ const message = err instanceof Error ? err.message : String(err);
1159
+ const stack = err instanceof Error ? err.stack ?? "" : "";
1160
+ ctx.devServer?.hot?.send({
1161
+ type: "error",
1162
+ err: {
1163
+ message,
1164
+ stack,
1165
+ plugin: "animus-extract"
1166
+ }
1167
+ });
1168
+ }
1169
+ ctx.invalidateSystemReloadModules(true);
1317
1170
  return;
1318
1171
  }
1319
- ctx.mutateFileCache((cache) => cache.set(relPath, {
1320
- hash: (0, _animus_ui_extract_pipeline.contentHash)(source),
1321
- source
1322
- }));
1172
+ const merged = mergePublished(outcomes);
1173
+ applyPublished(ctx, merged);
1174
+ if (!merged.published) ctx.invalidateSystemReloadModules(false);
1175
+ else ctx.log(`HMR system reload complete: ${Math.round(performance.now() - reloadStart)}ms`);
1323
1176
  }
1324
1177
  /** The once-per-event analysis half; its result is what the remaining
1325
1178
  * environments act on. */
@@ -1327,76 +1180,47 @@ async function analyzeChangedFile(ctx, file, absFile, read) {
1327
1180
  const ext = (0, path.extname)(file);
1328
1181
  if (!ctx.extensionsSet.has(ext)) return { kind: "ignored" };
1329
1182
  const excludeMatcher = ctx.excludeMatcher;
1330
- const isExternalPkg = ctx.externalPackageDirs.some((dir) => (0, _animus_ui_extract_pipeline.isPathWithinRoot)(dir, file));
1331
- if (!isExternalPkg && excludeMatcher.matches(file, (0, path.relative)(ctx.rootDir, file))) return { kind: "ignored" };
1183
+ if (!ctx.externalPackageDirs.some((dir) => (0, _animus_ui_extract_pipeline.isPathWithinRoot)(dir, file)) && excludeMatcher.matches(file, (0, path.relative)(ctx.rootDir, file))) return { kind: "ignored" };
1332
1184
  const relPath = (0, path.relative)(ctx.rootDir, absFile);
1333
- let source;
1185
+ const outputDir = (0, _animus_ui_extract_pipeline.outputSourceGuard)(ctx.rootDir, ctx.outputExcludes)(absFile);
1186
+ if (outputDir !== null) {
1187
+ ctx.warn((0, _animus_ui_extract_pipeline.outputSourceMessage)(relPath, outputDir));
1188
+ return { kind: "ignored" };
1189
+ }
1190
+ let settled;
1334
1191
  try {
1335
- source = await readChangedSource(absFile, read);
1192
+ settled = read ? await read() : void 0;
1336
1193
  } catch {
1337
1194
  return { kind: "ignored" };
1338
1195
  }
1339
- const hash = (0, _animus_ui_extract_pipeline.contentHash)(source);
1340
- const cached = ctx.fileCache.get(relPath);
1341
- if (cached && cached.hash === hash) {
1196
+ const hmrStart = performance.now();
1197
+ const changes = changesOf("update", absFile);
1198
+ const outcomes = await ctx.submitChanges(settled === void 0 ? changes : {
1199
+ ...changes,
1200
+ sources: /* @__PURE__ */ new Map([[absFile, settled]])
1201
+ });
1202
+ const last = outcomes.at(-1)?.outcome;
1203
+ if (last === void 0) {
1342
1204
  ctx.trace(`HMR skip: ${relPath} (unchanged)`);
1343
1205
  return { kind: "unchanged" };
1344
1206
  }
1345
- const priorExternalOwner = ctx.externalFileOwners[relPath];
1346
- if (isExternalPkg && !priorExternalOwner) {
1347
- const owner = Object.entries(ctx.externalDirOwners).find(([dir]) => (0, _animus_ui_extract_pipeline.isPathWithinRoot)(dir, absFile));
1348
- if (owner) ctx.externalFileOwners[relPath] = owner[1];
1349
- }
1350
- ctx.mutateFileCache((cache) => cache.set(relPath, {
1351
- hash,
1352
- source
1353
- }));
1354
- const restoreEntry = () => {
1355
- ctx.mutateFileCache((cache) => {
1356
- if (cached) cache.set(relPath, cached);
1357
- else cache.delete(relPath);
1358
- });
1359
- if (priorExternalOwner) ctx.externalFileOwners[relPath] = priorExternalOwner;
1360
- else delete ctx.externalFileOwners[relPath];
1361
- };
1362
- const hmrStart = performance.now();
1363
- const prevPlans = (0, _animus_ui_extract_pipeline.snapshotFilePlans)(ctx.storedManifest);
1364
- const analysisStart = performance.now();
1365
- const systemPropsBefore = systemPropsModuleSource(ctx);
1366
- let analysis;
1367
- try {
1368
- analysis = await ctx.analyzeIngested();
1369
- } catch (e) {
1370
- restoreEntry();
1371
- throw e;
1372
- }
1373
- if (!analysis.ok) {
1374
- if (analysis.abortedOriginals) return analysis.abortedOriginals.has(relPath) ? { kind: "ignored" } : { kind: "unchanged" };
1375
- restoreEntry();
1376
- return { kind: "ignored" };
1377
- }
1378
- try {
1379
- await reconcileSourceCorpus(ctx);
1380
- } catch (e) {
1381
- restoreEntry();
1382
- throw e;
1383
- }
1384
- const systemPropsChanged = systemPropsModuleSource(ctx) !== systemPropsBefore;
1385
- const analysisMs = Math.round(performance.now() - analysisStart);
1386
- const nativeEntry = ctx.corpus.published.analysisEntries.get(relPath);
1387
- const presentationOnly = nativeEntry && !analysis.endedHold ? isPresentationOnlyEdit(ctx, relPath, nativeEntry.source) : false;
1388
- const redelivered = presentationOnly ? [] : [relPath, ...analysis.redeliver];
1207
+ if (last.kind === "held") return last.abortedOriginals.has(relPath) ? { kind: "ignored" } : { kind: "unchanged" };
1208
+ const merged = mergePublished([...outcomes, ...await reconcileSourceCorpus(ctx)]);
1209
+ applyPublished(ctx, merged);
1210
+ const published = ctx.session.corpus.published;
1211
+ const nativeEntry = published.analysisEntries.get(relPath);
1212
+ const presentationOnly = nativeEntry && !merged.endedHold ? isPresentationOnlyEdit(ctx, relPath, nativeEntry.source) : false;
1213
+ const redelivered = presentationOnly ? [] : [relPath, ...merged.redeliver];
1389
1214
  const staleDefinitionFiles = [.../* @__PURE__ */ new Set([
1390
- ...(0, _animus_ui_extract_pipeline.diffFilePlans)(prevPlans, (0, _animus_ui_extract_pipeline.snapshotFilePlans)(ctx.storedManifest)),
1391
- ...analysis.redeliver,
1392
- ...extendingFiles(ctx, redelivered.flatMap((path$1) => ctx.corpus.published.ownership[path$1]?.analysisPaths ?? [path$1]))
1215
+ ...merged.planChanged,
1216
+ ...merged.redeliver,
1217
+ ...extendingFiles(ctx, redelivered.flatMap((path$2) => published.ownership[path$2]?.analysisPaths ?? [path$2]))
1393
1218
  ])].filter((defFile) => (0, path.resolve)(ctx.rootDir, defFile) !== absFile);
1394
- const hmrMs = Math.round(performance.now() - hmrStart);
1395
- ctx.log(`HMR update: ${relPath} — analysis ${analysisMs}ms, total ${hmrMs}ms${presentationOnly ? " (presentation-only)" : ""}`);
1219
+ ctx.log(`HMR update: ${relPath} — total ${Math.round(performance.now() - hmrStart)}ms${presentationOnly ? " (presentation-only)" : ""}`);
1396
1220
  return {
1397
1221
  kind: "analyzed",
1398
1222
  staleDefinitionFiles,
1399
- systemPropsChanged,
1223
+ systemPropsChanged: merged.systemPropsChanged,
1400
1224
  presentationOnly
1401
1225
  };
1402
1226
  }
@@ -1415,17 +1239,14 @@ function isPresentationOnlyEdit(ctx, scannerRelPath, source) {
1415
1239
  return false;
1416
1240
  }
1417
1241
  }
1418
- /** Prune a deleted file from the cache: a surviving entry is re-fed to the
1419
- * engine forever, so the deleted component's CSS never disappears. */
1242
+ /** A deleted file leaves the Session's corpus, so its CSS stops being
1243
+ * re-emitted. */
1420
1244
  async function pruneDeletedFile(ctx, absFile) {
1421
- const relPath = (0, path.relative)(ctx.rootDir, absFile);
1422
- if (!ctx.fileCache.get(relPath)) return;
1423
- if (!ctx.mutateFileCache((cache) => pruneFileCache(cache, ctx.rootDir, absFile))) return;
1424
- const prevPlans = (0, _animus_ui_extract_pipeline.snapshotFilePlans)(ctx.storedManifest);
1425
- const { ok } = await ctx.analyzeIngested();
1426
- if (!ok) return;
1245
+ const merged = mergePublished(await ctx.submitChanges(changesOf("delete", absFile)));
1246
+ if (!merged.published) return;
1247
+ applyPublished(ctx, merged);
1427
1248
  ctx.trace(`Deleted file pruned: ${(0, path.relative)(ctx.rootDir, absFile)}`);
1428
- invalidateFileModules(ctx, (0, _animus_ui_extract_pipeline.diffFilePlans)(prevPlans, (0, _animus_ui_extract_pipeline.snapshotFilePlans)(ctx.storedManifest)));
1249
+ if (!merged.full) invalidateFileModules(ctx, [...merged.planChanged]);
1429
1250
  ctx.invalidateExtractedModules();
1430
1251
  }
1431
1252
  /** The per-environment invalidation half. Static CSS is left alone: it only
@@ -1534,7 +1355,7 @@ function loadVirtualModule(ctx, id) {
1534
1355
  return css;
1535
1356
  };
1536
1357
  if (id === RESOLVED_CSS_ID) {
1537
- if (!ctx.isProd && ctx.storedSheets) {
1358
+ if (!ctx.isProd && ctx.storedManifest) {
1538
1359
  const { variables, body } = (0, _animus_ui_extract_pipeline.assembleStylesheet)({
1539
1360
  layers: ctx.options.layers,
1540
1361
  variableCss: ctx.variableCss,
@@ -1604,24 +1425,26 @@ if (import.meta.hot) {
1604
1425
  }
1605
1426
  `;
1606
1427
  }
1607
- if (id === RESOLVED_SYSTEM_PROPS_ID) return systemPropsModuleSource(ctx);
1428
+ if (id === RESOLVED_SYSTEM_PROPS_ID) return ctx.systemPropsModule;
1608
1429
  return null;
1609
1430
  }
1610
1431
  //#endregion
1611
1432
  //#region src/index.ts
1612
- function sourceKitConfig(rootDir, serving) {
1613
- const kits = (0, _animus_ui_extract_pipeline.sourceKitDependencies)(rootDir).filter((kit) => kit.installed);
1614
- if (kits.length === 0) return void 0;
1615
- const names = kits.map((kit) => kit.name);
1616
- const config = { optimizeDeps: {
1617
- exclude: names,
1618
- include: kits.flatMap((kit) => kit.dependencies.map((dependency) => `${kit.name} > ${dependency}`))
1619
- } };
1620
- if (serving) config.ssr = { noExternal: names };
1621
- return config;
1433
+ async function sourceKitConfig(rootDir, systemPath, engine) {
1434
+ const kits = /* @__PURE__ */ new Map();
1435
+ for (const kit of [...(0, _animus_ui_extract_pipeline.sourceKitDependencies)(rootDir, engine), ...await (0, _animus_ui_extract_pipeline.systemKitDependencies)(rootDir, systemPath, engine)]) if (kit.installed && !kits.has(kit.name)) kits.set(kit.name, kit);
1436
+ if (kits.size === 0) return void 0;
1437
+ const names = [...kits.keys()];
1438
+ return {
1439
+ optimizeDeps: {
1440
+ exclude: names,
1441
+ include: [...kits.values()].flatMap((kit) => kit.dependencies.map((dependency) => `${kit.name} > ${dependency}`))
1442
+ },
1443
+ ssr: { noExternal: names }
1444
+ };
1622
1445
  }
1623
1446
  function animusExtract(options) {
1624
- (0, _animus_ui_extract_pipeline.assertNoRetiredEngineSelection)(options.engine);
1447
+ (0, _animus_ui_extract_pipeline.assertSupportedEngine)(options.engine);
1625
1448
  (0, _animus_ui_extract_pipeline.assertKnownOptionKeys)(options, ["verify", "appearanceBootstrap"], [{
1626
1449
  key: "root",
1627
1450
  reason: "the Vite driver derives its root from the resolved Vite config"
@@ -1633,10 +1456,11 @@ function animusExtract(options) {
1633
1456
  return {
1634
1457
  name: "animus-extract",
1635
1458
  enforce: "pre",
1636
- config(userConfig, env) {
1459
+ async config(userConfig, env) {
1637
1460
  const { mode } = (0, _animus_ui_extract_pipeline.resolveMode)(options.mode, () => env.command === "build" ? "production" : "development");
1638
1461
  const define = { __ANIMUS_DEV__: mode === "development" };
1639
- const kits = sourceKitConfig((0, path.resolve)(userConfig.root ?? process.cwd()), env.command === "serve");
1462
+ const root = (0, path.resolve)(userConfig.root ?? process.cwd());
1463
+ const kits = await sourceKitConfig(root, (0, path.resolve)(root, options.system), ctx.kitEngine());
1640
1464
  return kits ? {
1641
1465
  define,
1642
1466
  ...kits
@@ -1649,9 +1473,15 @@ function animusExtract(options) {
1649
1473
  configResolved(config) {
1650
1474
  applyResolvedConfig(ctx, config);
1651
1475
  },
1476
+ buildEnd() {
1477
+ if (!ctx.endBuild(this.environment ?? ctx.singleEnvironment)) return;
1478
+ cancelSystemReload(ctx);
1479
+ ctx.close();
1480
+ },
1652
1481
  async buildStart() {
1653
- await runBuildStart(ctx, async (specifier) => {
1654
- return (await this.resolve(specifier))?.id ?? null;
1482
+ await ctx.openBuild(this.environment ?? ctx.singleEnvironment);
1483
+ await runBuildStart(ctx, async (specifier, importer) => {
1484
+ return (await this.resolve(specifier, importer))?.id ?? null;
1655
1485
  }, ctx.isProd ? (fileName, source) => this.emitFile({
1656
1486
  type: "asset",
1657
1487
  name: fileName,