@animus-ui/vite-plugin 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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$9) => (0, vite.isFileLoadingAllowed)(config, path$9);
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. */
@@ -434,146 +326,273 @@ var PluginContext = class {
434
326
  emissionProd = false;
435
327
  rootDir = "";
436
328
  logger = null;
437
- system = emptySystemConfig();
438
- /** Whether a system has loaded, so a failed reload has one to keep. */
439
- systemLoaded = false;
440
329
  lcssTargets = {};
441
330
  pathAliasesJson = null;
442
331
  /** Vite's export conditions, so the loader reads the files Vite serves. */
443
332
  resolveConditions = [];
444
- /** Development's repeat of buildStart's discovery, with its resolver. */
445
- rediscoverSources = null;
333
+ /** Vite's resolved extension order; the built-in policy until
334
+ * `configResolved`. */
335
+ extensionPolicy = _animus_ui_extract_pipeline.BUILT_IN_EXTENSION_POLICY;
336
+ /** Vite's asset classification, captured at `configResolved`. */
337
+ assetClassification = (0, _animus_ui_extract_pipeline.builtInAssetClassification)("noHost");
338
+ /** How Vite's build expands computed loads, captured at
339
+ * `configResolved`; none where it cannot be reproduced exactly. */
340
+ loadExpander;
446
341
  extensionsSet;
342
+ /** Built once per buildStart, before any discovery, from the user's
343
+ * exclude list and the output directory. */
447
344
  excludeMatcher;
448
- storedManifest = null;
449
- storedManifestJson = "";
450
- globalCss = "";
451
- resolvedComponentCss = "";
452
- storedSheets = null;
345
+ /** Where Vite writes, every environment's output directories and files,
346
+ * as structural exclusions; none outside the root. Set in
347
+ * `configResolved`. */
348
+ outputExcludes = [];
349
+ /** The Session publication this context serves, adopted whole as the
350
+ * Session commits it; null until one commits, and a failed analysis
351
+ * leaves the previous one served. Every view below reads it. */
352
+ served = null;
353
+ /** What a context serves before a first publication. */
354
+ unpublishedSystem = emptySystemConfig();
355
+ get storedManifest() {
356
+ return this.served?.manifest ?? null;
357
+ }
358
+ get storedManifestJson() {
359
+ return this.served?.manifestJson ?? "";
360
+ }
361
+ get system() {
362
+ return this.served?.system ?? this.unpublishedSystem;
363
+ }
364
+ get reverseProvenance() {
365
+ return this.served?.manifest.reverse_provenance ?? {};
366
+ }
367
+ /** The exact source `virtual:animus/system-props` serves: the served
368
+ * publication's, so the served bytes and the HMR change decision cannot
369
+ * diverge. */
370
+ get systemPropsModule() {
371
+ return this.served?.systemPropsModule ?? EMPTY_SYSTEM_PROPS_MODULE;
372
+ }
373
+ get externalPackageDirs() {
374
+ return this.served?.externalPackageDirs ?? [];
375
+ }
376
+ /** specifier → whether its redirected source entry has side effects, from
377
+ * the owning package's `sideEffects`; absent leaves Vite's default. */
378
+ get externalSourceSideEffects() {
379
+ return this.served?.externalSourceSideEffects ?? /* @__PURE__ */ new Map();
380
+ }
381
+ get externalSourceEntries() {
382
+ return this.served?.externalSourceEntries ?? /* @__PURE__ */ new Map();
383
+ }
384
+ /** The analysis sheets keep their asset() placeholders; these views
385
+ * substitute the current asset URLs, so an edited asset changes its URL
386
+ * without re-analysis. */
387
+ get globalCss() {
388
+ return this.substitutedSheets().globalCss;
389
+ }
390
+ get resolvedComponentCss() {
391
+ return this.substitutedSheets().componentCss;
392
+ }
393
+ analysisSheets() {
394
+ return {
395
+ ...NO_SHEETS,
396
+ globalCss: this.served?.analysisGlobalCss ?? "",
397
+ componentCss: this.served?.analysisComponentCss ?? ""
398
+ };
399
+ }
400
+ substitutedSheets() {
401
+ return (0, _animus_ui_extract_pipeline.substituteSheetAssets)(this.analysisSheets(), this.assetUrlBySpecifier);
402
+ }
453
403
  layerDeclaration = "";
454
404
  transformOutputHashes = /* @__PURE__ */ new Map();
455
405
  recordTransformOutput(relativePath, code) {
456
406
  this.transformOutputHashes.set(relativePath, (0, _animus_ui_extract_pipeline.contentHash)(code));
457
407
  }
458
- 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();
485
408
  /** Original path → hash of bytes whose parse aborted in a rejected attempt;
486
409
  * non-empty while that rejection holds publication. Those bytes are served
487
410
  * raw so the host reports its own syntax error instead of receiving the
488
411
  * previous module from the retained engine. */
489
412
  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
413
  hotUpdateEvents = new HotUpdateEvents();
505
- pendingReloadTimer = null;
506
- packageMap = {};
414
+ /** Coalesces a burst of invalidations into one full reload. */
415
+ reloadWindow = new _animus_ui_extract_session.CoalescingWindow(() => {
416
+ this.devServer?.hot?.send({ type: "full-reload" });
417
+ });
507
418
  base = "/";
508
419
  assetUrlBySpecifier = /* @__PURE__ */ new Map();
420
+ /** Where dev asset() copies live, immutable and content-named; null in a
421
+ * build, or when the dev server does not serve the directory, which
422
+ * serves the live files instead. Set in `configResolved`. */
423
+ assetCopyDir = null;
424
+ /** specifier → the source file its dev copy was made from. */
425
+ assetSourceBySpecifier = /* @__PURE__ */ new Map();
426
+ /** source → its copies, oldest first: the current and the previous. */
427
+ assetCopyHistory = /* @__PURE__ */ new Map();
428
+ /** Specifiers whose copy the dev server does not serve, warned once. */
429
+ unservedAssetCopies = /* @__PURE__ */ new Set();
430
+ /** Whether the dev server serves `path`, by Vite's `server.fs` rule;
431
+ * set in `configResolved`. */
432
+ servesFile = () => true;
509
433
  assetResolutionFailures = /* @__PURE__ */ new Set();
510
434
  assetPassComplete = false;
511
- 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
- /** specifier → whether its redirected source entry has side effects, from
518
- * the owning package's `sideEffects`; absent leaves Vite's default. */
519
- externalSourceSideEffects = /* @__PURE__ */ new Map();
520
- externalFileOwners = {};
521
- externalSourceEntries = /* @__PURE__ */ new Map();
435
+ /** The Session's last external package outcomes, for the self-verify. */
522
436
  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
437
  devServer;
530
- resolvedSystemPath = null;
531
- systemDependencyKeys = /* @__PURE__ */ new Set();
532
- systemDependencyPaths = [];
533
- v2Engine = null;
534
- v2SentSources = null;
535
- v2DriftWarned = false;
438
+ /** The paths already registered, per watcher: a reused plugin instance
439
+ * whose dev server is replaced registers everything with the new one. */
440
+ watchedPaths = /* @__PURE__ */ new WeakMap();
536
441
  /** Every native extraction call goes through this one accessor. */
537
442
  engineApi;
443
+ /** The native module's stateless lookups, before the root resolves: the
444
+ * shared factory, which registers no handle. */
445
+ kitEngine = _animus_ui_extract_session.engineFactory;
446
+ engineKey = null;
447
+ /** This context's build: one Vite config instance, one build. */
448
+ build = (0, _animus_ui_extract_session.createBuildId)();
449
+ /** The build's Session, created once the Vite root resolves. */
450
+ session = null;
451
+ /** The outcomes each in-flight `submitChanges` collects. */
452
+ outcomeCollectors = /* @__PURE__ */ new Set();
538
453
  constructor(options, engineApiOverride) {
539
454
  this.options = options;
540
- this.staticCssJson = (0, _animus_ui_extract_pipeline.serializeStaticCss)(options.staticCss);
541
455
  const verbosity = (0, _animus_ui_extract_pipeline.resolveVerbosity)(options.verbose, process.env.ANIMUS_DEBUG);
542
456
  this.verbose = verbosity !== "quiet";
543
457
  this.tracing = verbosity === "trace";
544
458
  this.extensionsSet = new Set(options.extensions ?? _animus_ui_extract_pipeline.DEFAULT_EXTENSIONS);
545
- this.excludeMatcher = (0, _animus_ui_extract_pipeline.createExcludeMatcher)(options.exclude);
546
459
  if (engineApiOverride) {
547
460
  this.engineApi = engineApiOverride;
461
+ this.kitEngine = engineApiOverride;
548
462
  return;
549
463
  }
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
- }
464
+ const build = this.build;
465
+ const rehydrateFilesJson = (filesJsonRaw) => {
466
+ if (!filesJsonRaw.includes("\"source\":\"\"")) return filesJsonRaw;
467
+ const entries = (0, _animus_ui_extract_pipeline.parseFilesJson)(filesJsonRaw, "animus-extract");
468
+ const analysisEntries = this.session?.corpus.published.analysisEntries ?? /* @__PURE__ */ new Map();
469
+ for (const entry of entries) if (entry.source === "") entry.source = analysisEntries.get(entry.path)?.source ?? "";
470
+ return JSON.stringify(entries);
471
+ };
472
+ this.engineApi = () => {
473
+ if (!this.rootDir) throw new Error("[animus-extract] the engine was used before the Vite root resolved");
474
+ this.engineKey ??= (0, _animus_ui_extract_session.sessionKey)(build, this.rootDir);
475
+ return (0, _animus_ui_extract_session.engineHandle)(this.engineKey, {
476
+ label: "animus-extract",
477
+ rehydrateFilesJson
478
+ })();
479
+ };
480
+ }
481
+ /** Creates the build's Session from the resolved config: Vite's engine,
482
+ * logger and core options, kept in memory. */
483
+ createSession() {
484
+ this.session?.close();
485
+ const options = this.options;
486
+ const session = new _animus_ui_extract_session.ExtractionSession({
487
+ system: options.system,
488
+ exclude: options.exclude,
489
+ extensions: options.extensions,
490
+ strict: options.strict,
491
+ diagnostics: options.diagnostics,
492
+ verbose: options.verbose,
493
+ prefix: options.prefix,
494
+ prefixContextualVars: options.prefixContextualVars,
495
+ targets: options.targets,
496
+ minify: options.minify,
497
+ staticCss: options.staticCss,
498
+ layers: options.layers,
499
+ engine: options.engine,
500
+ runtimeImport: options.runtimeImport,
501
+ mode: this.emissionProd ? "production" : "development"
502
+ }, {
503
+ build: this.build,
504
+ unbundledComputedImports: true,
505
+ engine: () => this.engineApi(),
506
+ logger: {
507
+ info: (line) => (this.logger ?? console).info(line),
508
+ warn: (line) => (this.logger ?? console).warn(line),
509
+ error: (line) => (this.logger ?? console).error(line)
575
510
  }
576
511
  });
512
+ session.persistArtifacts = false;
513
+ session.hostSubstitutesAssets = true;
514
+ session.driverLabel = "animus-extract";
515
+ session.onOutcome = (outcome) => this.observeOutcome(session, outcome);
516
+ session.development = this.development;
517
+ session.rootDir = this.rootDir;
518
+ session.conditions = this.resolveConditions;
519
+ session.extensionPolicy = this.extensionPolicy;
520
+ session.assetClassification = this.assetClassification;
521
+ session.loadExpander = this.loadExpander;
522
+ session.pathAliasesJson = this.pathAliasesJson;
523
+ session.delivery = {
524
+ cssModuleId: VIRTUAL_CSS_ID,
525
+ systemPropsModuleId: VIRTUAL_SYSTEM_PROPS_ID
526
+ };
527
+ session.structuralExclude = this.outputExcludes;
528
+ this.session = session;
529
+ return session;
530
+ }
531
+ /** Serves each publication as the Session makes it, so every reader in
532
+ * between sees one generation. */
533
+ observeOutcome(session, outcome) {
534
+ let systemPropsChanged = false;
535
+ if (outcome.kind === "held") for (const [path$6, hash] of outcome.abortedOriginals) this.abortedParseHashes.set(path$6, hash);
536
+ else {
537
+ this.abortedParseHashes.clear();
538
+ const before = this.systemPropsModule;
539
+ this.adoptPublication(session);
540
+ systemPropsChanged = this.systemPropsModule !== before;
541
+ }
542
+ for (const collected of this.outcomeCollectors) collected.push({
543
+ outcome,
544
+ systemPropsChanged
545
+ });
546
+ }
547
+ /** Submits a dev change set to the Session. Until a first publication
548
+ * there is no generation to update: the full read covers the change. */
549
+ submitChanges(changes) {
550
+ return this.runAttempt((session) => session.publication === null ? session.runFullPipeline([], changes.sources) : session.handleWatchUpdate(changes));
551
+ }
552
+ /** Runs one Session attempt and returns the outcomes that settle it. A
553
+ * hold warns and resolves with its outcome (Vite reports the syntax
554
+ * error itself); any other failure rejects. Either way the watcher
555
+ * learns the paths the attempt's system load named. */
556
+ async runAttempt(attempt) {
557
+ const session = this.session ?? this.createSession();
558
+ const collected = [];
559
+ this.outcomeCollectors.add(collected);
560
+ try {
561
+ await attempt(session);
562
+ } catch (error) {
563
+ if (!collected.some(({ outcome }) => outcome.kind === "held")) throw error;
564
+ this.warn(error instanceof Error ? error.message : String(error));
565
+ } finally {
566
+ this.outcomeCollectors.delete(collected);
567
+ this.registerSystemWatchPaths();
568
+ }
569
+ return collected;
570
+ }
571
+ /** Serves the Session's last publication whole (asset placeholders
572
+ * kept; Vite substitutes them). */
573
+ adoptPublication(session) {
574
+ if (session.publication === null) return;
575
+ this.served = session.publication;
576
+ this.externalPackageOutcomes = session.lastExternalOutcomes;
577
+ this.applyAssetSubstitutions();
578
+ }
579
+ /** The context's environment builds: a custom `buildApp` builds a shared
580
+ * plugin's environments in parallel, so the context's build retires with
581
+ * the last of them, not the first. */
582
+ builds = new _animus_ui_extract_session.EnvironmentBuilds();
583
+ /** Retires this context's build when Vite closes it: the Session leaves
584
+ * the registry (with its manifest and watch state) and the engine handle
585
+ * is released, injected engine or not. The next build start rejoins. */
586
+ close() {
587
+ this.session?.close();
588
+ this.releaseEngine();
589
+ }
590
+ /** Releases this context's engine handle: a standalone one, which no
591
+ * joined build retires. The next use makes a fresh one. */
592
+ releaseEngine() {
593
+ if (this.engineKey === null) return;
594
+ (0, _animus_ui_extract_session.releaseEngineHandle)(this.engineKey);
595
+ this.engineKey = null;
577
596
  }
578
597
  log(msg) {
579
598
  if (this.verbose) (this.logger ?? console).info(`[animus] ${msg}`);
@@ -604,214 +623,57 @@ var PluginContext = class {
604
623
  if (!report) throw new Error(message, cause === void 0 ? void 0 : { cause });
605
624
  report(message);
606
625
  }
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;
626
+ devFsUrl(absPath) {
627
+ return `${this.base.endsWith("/") ? this.base.slice(0, -1) : this.base}/@fs${absPath}`;
628
+ }
629
+ /** A dev asset() URL: an immutable content-named copy of `absPath`, so an
630
+ * edit gets a new URL, or the live file where copies are not served. */
631
+ devAssetUrl(specifier, absPath) {
632
+ if (this.assetCopyDir === null) return this.devFsUrl(absPath);
633
+ const { fileName } = (0, _animus_ui_extract_pipeline.writeAssetCopy)(absPath, this.assetCopyDir);
634
+ const copyPath = (0, path.join)(this.assetCopyDir, fileName);
635
+ if (!this.servesFile(copyPath)) {
636
+ if (!this.unservedAssetCopies.has(specifier)) {
637
+ this.unservedAssetCopies.add(specifier);
638
+ 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
639
  }
636
- console.warn(`[animus-extract] Failed to load system from ${this.resolvedSystemPath}:`, e);
640
+ return this.devFsUrl(absPath);
641
+ }
642
+ if (this.assetSourceBySpecifier.get(specifier) !== absPath) this.devServer?.watcher?.add(absPath);
643
+ this.assetSourceBySpecifier.set(specifier, absPath);
644
+ this.retainAssetCopy(absPath, fileName);
645
+ return this.devFsUrl(copyPath);
646
+ }
647
+ /** Keeps each source's current copy and the one before it; an asset
648
+ * edit reloads the page, so an older URL is never fetched again. A copy
649
+ * another source still keeps is never removed. */
650
+ retainAssetCopy(source, fileName) {
651
+ const history = this.assetCopyHistory.get(source) ?? [];
652
+ if (history[history.length - 1] !== fileName) history.push(fileName);
653
+ this.assetCopyHistory.set(source, history);
654
+ while (history.length > 2) {
655
+ const oldest = history.shift();
656
+ if (![...this.assetCopyHistory.values()].some((copies) => copies.includes(oldest)) && this.assetCopyDir !== null) (0, fs.rmSync)((0, path.join)(this.assetCopyDir, oldest), { force: true });
637
657
  }
638
658
  }
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;
659
+ /** Whether `absFile` is the source of a dev asset copy. */
660
+ isAssetSource(absFile) {
661
+ const keys = new Set((0, _animus_ui_extract_pipeline.toWatchKeys)(absFile));
662
+ return [...this.assetSourceBySpecifier.values()].some((source) => (0, _animus_ui_extract_pipeline.toWatchKeys)(source).some((key) => keys.has(key)));
663
+ }
664
+ /** Copies an edited asset under its new content name and redelivers the
665
+ * stylesheet that names it. */
666
+ refreshAssetCopies(absFile) {
667
+ const keys = new Set((0, _animus_ui_extract_pipeline.toWatchKeys)(absFile));
668
+ for (const [specifier, source] of this.assetSourceBySpecifier) {
669
+ if (!(0, _animus_ui_extract_pipeline.toWatchKeys)(source).some((key) => keys.has(key))) continue;
645
670
  try {
646
- (0, fs.statSync)((0, path.resolve)(this.rootDir, file));
647
- return true;
671
+ this.assetUrlBySpecifier.set(specifier, this.devAssetUrl(specifier, source));
648
672
  } catch (err) {
649
- return !(0, _animus_ui_extract_pipeline.isDeletedSource)(err);
673
+ this.warn(`[animus-extract] could not copy edited asset ${source}: ${String(err)}`);
650
674
  }
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
675
  }
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)));
804
- }
805
- if (reloadingSystem) this.invalidateSystemReloadModules(!systemHeld);
806
- }
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}`;
676
+ this.invalidateSystemReloadModules(true);
815
677
  }
816
678
  assetFallback(specifier, message, cause) {
817
679
  if (this.options.strict) this.strictFailure(message, cause);
@@ -823,8 +685,8 @@ var PluginContext = class {
823
685
  sheetAssetSpecifiers() {
824
686
  return (0, _animus_ui_extract_pipeline.findSheetAssetSpecifiers)({
825
687
  variableCss: this.system.variableCss,
826
- globalCss: this.globalCss,
827
- componentCss: this.resolvedComponentCss
688
+ globalCss: this.analysisSheets().globalCss,
689
+ componentCss: this.analysisSheets().componentCss
828
690
  });
829
691
  }
830
692
  /** `system.variableCss` with its asset() placeholders substituted from
@@ -837,18 +699,6 @@ var PluginContext = class {
837
699
  variableCss: this.system.variableCss
838
700
  }, this.assetUrlBySpecifier).variableCss;
839
701
  }
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
702
  /** Resolves asset specifiers a system reload introduced. Node-side only —
853
703
  * the plugin hook context is unavailable here, so results can differ. */
854
704
  applyAssetSubstitutions() {
@@ -856,44 +706,9 @@ var PluginContext = class {
856
706
  if (this.assetResolutionFailures.delete(specifier)) this.assetUrlBySpecifier.delete(specifier);
857
707
  if (this.assetUrlBySpecifier.has(specifier)) continue;
858
708
  const resolved = (0, _animus_ui_extract_pipeline.resolveAssetFile)(specifier, this.rootDir, this.pathAliasesJson);
859
- if (resolved) this.assetUrlBySpecifier.set(specifier, this.devFsUrl(resolved));
709
+ if (resolved) this.assetUrlBySpecifier.set(specifier, this.devAssetUrl(specifier, resolved));
860
710
  else this.assetFallback(specifier, `[animus-extract] unresolvable asset() specifier: ${specifier}`);
861
711
  }
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
712
  }
898
713
  invalidateSystemReloadModules(reloadClient) {
899
714
  const server = this.devServer;
@@ -909,15 +724,36 @@ var PluginContext = class {
909
724
  if (reloadClient) server.hot?.send({ type: "full-reload" });
910
725
  }
911
726
  isSystemDependency(absFile) {
912
- return (0, _animus_ui_extract_pipeline.toWatchKeys)(absFile).some((key) => this.systemDependencyKeys.has(key));
913
- }
914
- /** Watch every loader-reported dependency path. Vite hard-ignores
915
- * node_modules, so dependencies installed there produce no events. */
727
+ return this.session?.isSystemDependency(absFile) ?? false;
728
+ }
729
+ /** Registers the Session's watch plan with Vite's watcher, each path
730
+ * once per watcher. Vite also watches its packages' own directories beside the
731
+ * plan's canonical roots, and an absent path either covers (or the root
732
+ * does) is left to their watch: a watch on an absent file inside a
733
+ * watched directory stops that directory's events under inotify
734
+ * (chokidar's non-fsevents handler). Vite hard-ignores node_modules, so
735
+ * dependencies installed there produce no events. */
916
736
  registerSystemWatchPaths() {
917
737
  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);
738
+ const session = this.session;
739
+ if (!watcher || !session) return;
740
+ const directories = [.../* @__PURE__ */ new Set([...session.watchPlan().directories, ...session.externalPackageDirs])];
741
+ const covered = this.rootDir ? [this.rootDir, ...directories] : directories;
742
+ const plan = session.watchPlan(covered);
743
+ let registered = this.watchedPaths.get(watcher);
744
+ if (registered === void 0) {
745
+ registered = /* @__PURE__ */ new Set();
746
+ this.watchedPaths.set(watcher, registered);
747
+ }
748
+ for (const paths of [
749
+ plan.files,
750
+ plan.missing,
751
+ directories
752
+ ]) {
753
+ const added = paths.filter((path$7) => !registered.has(path$7));
754
+ for (const path$8 of added) registered.add(path$8);
755
+ if (added.length > 0) watcher.add(added);
756
+ }
921
757
  }
922
758
  /** Invalidate the component CSS and system-props modules, then reload. The
923
759
  * reload alone rescues nothing: Vite keeps serving its cached transform.
@@ -930,33 +766,7 @@ var PluginContext = class {
930
766
  const mod = server.moduleGraph.getModuleById(moduleId);
931
767
  if (mod) server.moduleGraph.invalidateModule(mod);
932
768
  }
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
- });
769
+ this.reloadWindow.note();
960
770
  }
961
771
  runSelfVerify() {
962
772
  const failures = (0, _animus_ui_extract_pipeline.runStructuralSelfCheck)({
@@ -981,131 +791,113 @@ var PluginContext = class {
981
791
  }
982
792
  };
983
793
  //#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
- }
794
+ //#region src/module-invalidation.ts
1015
795
  /**
1016
- * Folds undiscovered on-disk files when the manifest reports an unresolved
1017
- * parent, then re-analyzes; the iteration cap is containment, not semantics.
796
+ * Completeness is load-bearing: the file-plan diff and the sources re-delivered
797
+ * when a held publication ends are the only invalidation candidate sources, so
798
+ * a node shape missed here stays stale for the session.
1018
799
  */
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
- });
800
+ function invalidateFileModules(ctx, relPaths) {
801
+ const server = ctx.devServer;
802
+ if (!server || relPaths.length === 0) return 0;
803
+ const graphs = server.environments ? Object.values(server.environments).map((env) => env?.moduleGraph).filter(Boolean) : [server.moduleGraph].filter(Boolean);
804
+ let total = 0;
805
+ for (const relPath of relPaths) {
806
+ const absPath = (0, path.resolve)(ctx.rootDir, relPath);
807
+ let fileCount = 0;
808
+ for (const graph of graphs) {
809
+ const nodes = /* @__PURE__ */ new Set();
810
+ const byFile = graph.getModulesByFile?.(absPath);
811
+ if (byFile) for (const mod of byFile) nodes.add(mod);
812
+ const byId = graph.getModuleById?.(absPath);
813
+ if (byId) nodes.add(byId);
814
+ for (const mod of nodes) {
815
+ graph.invalidateModule(mod);
816
+ fileCount++;
817
+ }
1055
818
  }
1056
- if (!published) return reanalyzed;
819
+ if (fileCount > 0) ctx.trace(`HMR invalidate: ${relPath} (plan changed, ${fileCount} nodes)`);
820
+ total += fileCount;
1057
821
  }
1058
- if (unresolvedDropFiles(ctx).size > 0) ctx.warn("source-corpus rediscovery did not stabilize after bounded retries");
1059
- return reanalyzed;
822
+ return total;
1060
823
  }
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;
824
+ //#endregion
825
+ //#region src/rediscovery.ts
826
+ const NO_DROPS = /* @__PURE__ */ new Map();
827
+ const dropsByManifest = /* @__PURE__ */ new WeakMap();
828
+ /** The current manifest's extensions dropped over their parent, by file:
829
+ * the engine's typed missing-input evidence (spec §12.2 row 11). */
830
+ function droppedExtensions(ctx) {
831
+ const manifest = ctx.storedManifest;
832
+ if (!manifest) return NO_DROPS;
833
+ let drops = dropsByManifest.get(manifest);
834
+ if (!drops) {
835
+ drops = (0, _animus_ui_extract_pipeline.servedFiles)(manifest).dropped;
836
+ dropsByManifest.set(manifest, drops);
837
+ }
838
+ return drops;
1090
839
  }
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
840
  /** Per-context (file, parent, condition) verdicts already warned — each
1095
841
  * condition warns once; cleared when the drops disappear. */
1096
842
  const warnedVerdicts = /* @__PURE__ */ new WeakMap();
1097
- const PARENT_PROBE_EXTENSIONS = /* @__PURE__ */ new Set([
1098
- ".tsx",
1099
- ".ts",
1100
- ".jsx",
1101
- ".js"
1102
- ]);
843
+ /** Where a parent the analysis does not hold is on disk, probed from its
844
+ * base under the host's extension order: one no analysed module answers
845
+ * (`absent`), or one the host opens but the analysis never read
846
+ * (`incomplete`), such as a file the watcher has not reported yet. */
847
+ function parentOnDisk(ctx, input) {
848
+ if (input.outcome !== "absent" && input.outcome !== "incomplete" || input.base === null) return { kind: "none" };
849
+ const probed = (0, _animus_ui_extract_pipeline.probeAbsolutePathSpecifier)((0, path.resolve)(ctx.rootDir, input.base), void 0, ctx.rootDir, ctx.extensionPolicy);
850
+ if (probed.kind === "absent") return { kind: "none" };
851
+ if (probed.kind === "case-mismatch") return {
852
+ kind: "inadmissible",
853
+ reason: `is unresolved: ${(0, _animus_ui_extract_pipeline.caseMismatchReason)(`the import '${input.specifier}'`, {
854
+ candidate: (0, path.relative)(ctx.rootDir, probed.candidate),
855
+ actual: (0, path.relative)(ctx.rootDir, probed.actual)
856
+ }, "spell the import as the file is spelled.")}`
857
+ };
858
+ const path$5 = probed.path;
859
+ const relPath = (0, path.relative)(ctx.rootDir, path$5);
860
+ const excludedBy = ctx.excludeMatcher.explain(path$5, relPath);
861
+ 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;
862
+ if (reason === void 0) return {
863
+ kind: "fold",
864
+ path: path$5
865
+ };
866
+ if (reason === null) return { kind: "none" };
867
+ return {
868
+ kind: "inadmissible",
869
+ reason: `resolves to ${relPath}, ${reason}`
870
+ };
871
+ }
1103
872
  /**
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.
873
+ * Folds the on-disk files the manifest's dropped extensions name as missing
874
+ * parents into the Session, in one bounded pass, and returns the outcomes
875
+ * of that attempt. A parent no file answers, or one that cannot be
876
+ * admitted, leaves its drop standing: the transform then fails rather than
877
+ * serving builder code (SA1).
1106
878
  */
1107
- function warnInadmissibleParents(ctx, drops) {
1108
- const excludeMatcher = ctx.excludeMatcher;
879
+ async function reconcileSourceCorpus(ctx) {
880
+ if (ctx.isProd || ctx.session === null) return [];
881
+ const drops = [...droppedExtensions(ctx).values()];
882
+ if (drops.length === 0) {
883
+ warnedVerdicts.get(ctx)?.clear();
884
+ return [];
885
+ }
886
+ const folded = /* @__PURE__ */ new Set();
887
+ const inadmissible = [];
888
+ for (const drop of drops) {
889
+ const onDisk = parentOnDisk(ctx, drop);
890
+ if (onDisk.kind === "fold") folded.add(onDisk.path);
891
+ else if (onDisk.kind === "inadmissible") inadmissible.push([drop, onDisk.reason]);
892
+ }
893
+ warnInadmissibleParents(ctx, drops, inadmissible);
894
+ if (folded.size === 0) return [];
895
+ ctx.log(`rediscovery: folded ${folded.size} on-disk file(s) a dropped extension names as its parent`);
896
+ return ctx.submitChanges({ modifiedFiles: folded });
897
+ }
898
+ /** Warns, once per condition, for parents that resolve on disk but cannot
899
+ * be admitted. */
900
+ function warnInadmissibleParents(ctx, drops, inadmissible) {
1109
901
  let warned = warnedVerdicts.get(ctx);
1110
902
  if (!warned) {
1111
903
  warned = /* @__PURE__ */ new Set();
@@ -1113,31 +905,51 @@ function warnInadmissibleParents(ctx, drops) {
1113
905
  }
1114
906
  const active = new Set(drops.map((d) => `${d.file}\0${d.parent}`));
1115
907
  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;
908
+ for (const [drop, reason] of inadmissible) {
1127
909
  const verdictKey = `${drop.file}\0${drop.parent}\0${reason}`;
1128
910
  if (warned.has(verdictKey)) continue;
1129
911
  warned.add(verdictKey);
1130
- ctx.warn(`cannot extract '${drop.component}': parent '${drop.parent}' resolves to ${relPath}, ${reason}`);
912
+ ctx.warn(`cannot extract '${drop.component}': parent '${drop.parent}' ${reason}`);
1131
913
  }
1132
914
  }
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];
915
+ //#endregion
916
+ //#region src/session-outcomes.ts
917
+ /** The file event as a Session change set. */
918
+ function changesOf(type, absFile) {
919
+ const files = /* @__PURE__ */ new Set([absFile]);
920
+ return type === "delete" ? { removedFiles: files } : { modifiedFiles: files };
921
+ }
922
+ /** What the published outcomes of one submission change, merged. */
923
+ function mergePublished(outcomes) {
924
+ const merged = {
925
+ published: false,
926
+ full: false,
927
+ endedHold: false,
928
+ systemPropsChanged: false,
929
+ planChanged: /* @__PURE__ */ new Set(),
930
+ redeliver: /* @__PURE__ */ new Set()
931
+ };
932
+ for (const { outcome, systemPropsChanged } of outcomes) {
933
+ if (outcome.kind !== "published") continue;
934
+ merged.published = true;
935
+ merged.full ||= outcome.pass === "full";
936
+ merged.endedHold ||= outcome.endedHold;
937
+ merged.systemPropsChanged ||= systemPropsChanged;
938
+ for (const path of outcome.planChanged) merged.planChanged.add(path);
939
+ for (const path of outcome.redeliver) merged.redeliver.add(path);
940
+ }
941
+ return merged;
942
+ }
943
+ /** Acts on what every submission's publications owe the module graph: a
944
+ * publication that ends a hold re-delivers the originals whose bytes
945
+ * changed meanwhile, and a full pass (a system reload) reloads the
946
+ * client. */
947
+ function applyPublished(ctx, merged) {
948
+ if (merged.redeliver.size > 0) invalidateFileModules(ctx, [...merged.redeliver]);
949
+ if (merged.full) {
950
+ invalidateFileModules(ctx, [...merged.planChanged]);
951
+ ctx.invalidateSystemReloadModules(true);
1139
952
  }
1140
- return null;
1141
953
  }
1142
954
  //#endregion
1143
955
  //#region src/transform.ts
@@ -1154,7 +966,7 @@ function applyDevBridgeImport(code) {
1154
966
  function extendingFiles(ctx, analysisPaths) {
1155
967
  const manifest = ctx.storedManifest;
1156
968
  const files = /* @__PURE__ */ new Set();
1157
- const queue = analysisPaths.flatMap((path$2) => manifest?.files[path$2] ?? []);
969
+ const queue = analysisPaths.flatMap((path$3) => manifest?.files[path$3] ?? []);
1158
970
  const seen = new Set(queue);
1159
971
  while (queue.length > 0) {
1160
972
  const id = queue.shift();
@@ -1168,12 +980,11 @@ function extendingFiles(ctx, analysisPaths) {
1168
980
  }
1169
981
  return files;
1170
982
  }
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;
983
+ async function transformSource(ctx, code, requestId) {
984
+ if (requestId.startsWith("\0") || requestId.startsWith("virtual:animus/")) return null;
985
+ const query = requestId.indexOf("?");
986
+ if (query !== -1 && !/^v=[\w-]+$/.test(requestId.slice(query + 1))) return null;
987
+ const id = query === -1 ? requestId : requestId.slice(0, query);
1177
988
  const isExternalPkg = ctx.externalPackageDirs.some((dir) => (0, _animus_ui_extract_pipeline.isPathWithinRoot)(dir, id));
1178
989
  const relativePath = (0, path.relative)(ctx.rootDir, id);
1179
990
  if (!isExternalPkg) {
@@ -1183,57 +994,36 @@ async function transformSource(ctx, code, id) {
1183
994
  }
1184
995
  const abortedHash = ctx.abortedParseHashes.get(relativePath);
1185
996
  if (abortedHash !== void 0 && abortedHash === (0, _animus_ui_extract_pipeline.contentHash)(code)) return null;
997
+ if (!ctx.storedManifest) throw new Error(`[animus-extract] no analysis is available to transform ${relativePath}: the project analysis failed; see the error above.`);
998
+ let detectionFailed = false;
1186
999
  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;
1000
+ const session = ctx.session;
1001
+ if (!ctx.isProd && session !== null && !session.heldSources.has(relativePath)) {
1002
+ let outcomes = [];
1201
1003
  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));
1004
+ outcomes = await ctx.submitChanges(changesOf("create", id));
1005
+ } catch (error) {
1006
+ ctx.warn(error instanceof Error ? error.message : String(error));
1007
+ detectionFailed = true;
1207
1008
  }
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 }));
1009
+ if (mergePublished(outcomes).published) {
1010
+ const merged = mergePublished([...outcomes, ...await reconcileSourceCorpus(ctx)]);
1011
+ applyPublished(ctx, merged);
1012
+ if (!merged.full) invalidateFileModules(ctx, [...merged.planChanged].filter((path$4) => path$4 !== relativePath));
1211
1013
  const compCount = ctx.storedManifest?.files[relativePath]?.length ?? 0;
1212
1014
  ctx.info(`New file detected: ${relativePath} — ${compCount ? `${compCount} components extracted` : "no components"}`);
1213
1015
  ctx.invalidateExtractedModules();
1214
1016
  }
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
1017
  }
1018
+ if (detectionFailed) throw new Error(`[animus-extract] the analysis of the new file ${relativePath} failed; see the error above.`);
1229
1019
  }
1020
+ const drop = droppedExtensions(ctx).get(relativePath);
1021
+ if (drop !== void 0) throw new Error((0, _animus_ui_extract_pipeline.droppedExtensionMessage)(drop));
1022
+ if (!ctx.storedManifest.files[relativePath]?.length) return null;
1230
1023
  try {
1231
1024
  const { transformFile } = ctx.engineApi();
1232
1025
  const result = transformFile(code, relativePath, ctx.storedManifestJson);
1233
- if (!result.hasComponents) {
1234
- ctx.recordFallbackState(relativePath, true);
1235
- return null;
1236
- }
1026
+ if (!result.hasComponents) throw new Error(`the analysis extracted components from ${relativePath}, but its transform found none`);
1237
1027
  if (ctx.tracing) {
1238
1028
  const compCount = ctx.storedManifest.files[relativePath]?.length ?? 0;
1239
1029
  ctx.trace(`transform ${relativePath}: ${compCount} components`);
@@ -1243,35 +1033,30 @@ async function transformSource(ctx, code, id) {
1243
1033
  outputCode = applyDevBridgeImport(result.code);
1244
1034
  ctx.recordTransformOutput(relativePath, outputCode);
1245
1035
  }
1246
- ctx.recordFallbackState(relativePath, false);
1247
1036
  return {
1248
1037
  code: outputCode,
1249
1038
  map: null
1250
1039
  };
1251
1040
  } 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;
1041
+ throw new Error(`[animus-extract] Failed to transform ${id}: ${e}`, { cause: e });
1256
1042
  }
1257
1043
  }
1258
1044
  //#endregion
1259
1045
  //#region src/hmr.ts
1260
1046
  /** 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) {
1047
+ * dispatch that claims it submits the file to the Session, and invalidation
1048
+ * runs in each. */
1049
+ async function handleHotUpdate(ctx, environment, { type, file, timestamp, modules, read }) {
1263
1050
  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
1051
  const ownsEvent = ctx.hotUpdateEvents.claim(environment.name, file, timestamp);
1268
1052
  const absFile = (0, path.resolve)(file);
1269
1053
  ctx.trace(`hotUpdate ${type} ${(0, path.relative)(ctx.rootDir, absFile)} env=${environment.name} owns=${ownsEvent}`);
1270
1054
  if (ctx.isSystemDependency(absFile)) {
1271
- if (ownsEvent) {
1272
- await reconcileSourceEntry(ctx, absFile, type, read);
1273
- ctx.requestSystemReload((0, path.relative)(ctx.rootDir, absFile));
1274
- }
1055
+ if (ownsEvent) requestSystemReload(ctx, absFile, type);
1056
+ return [];
1057
+ }
1058
+ if (ctx.assetCopyDir !== null && ctx.isAssetSource(absFile)) {
1059
+ if (ownsEvent && type !== "delete") ctx.refreshAssetCopies(absFile);
1275
1060
  return [];
1276
1061
  }
1277
1062
  if (type === "delete") {
@@ -1295,31 +1080,74 @@ async function handleHotUpdateExclusive(ctx, environment, { type, file, timestam
1295
1080
  }
1296
1081
  return invalidateStaleModules(ctx, environment, modules, result);
1297
1082
  }
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;
1083
+ /** Each context's pending system-dependency events, and the window they
1084
+ * wait out together. */
1085
+ const systemReloads = /* @__PURE__ */ new WeakMap();
1086
+ /** A system dependency's event waits out the coalescing window (SA19)
1087
+ * before the Session reads it: a watcher can report a write as it starts
1088
+ * and drop the event for its end, so an immediate read would load a
1089
+ * half-written module. Events in one window reload once. */
1090
+ function requestSystemReload(ctx, absFile, type) {
1091
+ let state = systemReloads.get(ctx);
1092
+ if (state === void 0) {
1093
+ const pending = /* @__PURE__ */ new Map();
1094
+ state = {
1095
+ pending,
1096
+ window: new _animus_ui_extract_session.CoalescingWindow(() => {
1097
+ if (pending.size === 0) return;
1098
+ const taken = [...pending];
1099
+ pending.clear();
1100
+ return reloadSystem(ctx, taken);
1101
+ })
1102
+ };
1103
+ systemReloads.set(ctx, state);
1309
1104
  }
1310
- const relPath = (0, path.relative)(ctx.rootDir, absFile);
1311
- if (!ctx.fileCache.has(relPath)) return;
1312
- let source;
1105
+ ctx.log(`HMR system reload scheduled: ${(0, path.relative)(ctx.rootDir, absFile)}`);
1106
+ state.pending.set(absFile, type);
1107
+ state.window.note();
1108
+ }
1109
+ /** Drops a pending system reload: the context is closing. */
1110
+ function cancelSystemReload(ctx) {
1111
+ const state = systemReloads.get(ctx);
1112
+ state?.window.cancel();
1113
+ state?.pending.clear();
1114
+ }
1115
+ /** Reloads the system through the Session over the window's events. It
1116
+ * runs from a timer, so it never throws: a failed reload keeps the last
1117
+ * good system served and warns; under `strict` the client overlay reports
1118
+ * it too. */
1119
+ async function reloadSystem(ctx, events) {
1120
+ const reloadStart = performance.now();
1121
+ const modifiedFiles = /* @__PURE__ */ new Set();
1122
+ const removedFiles = /* @__PURE__ */ new Set();
1123
+ for (const [path$1, type] of events) (type === "delete" ? removedFiles : modifiedFiles).add(path$1);
1124
+ let outcomes;
1313
1125
  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`);
1126
+ outcomes = await ctx.submitChanges({
1127
+ modifiedFiles,
1128
+ removedFiles
1129
+ });
1130
+ } catch (err) {
1131
+ ctx.warn(`[animus-extract] system reload failed: ${String(err)}`);
1132
+ if (ctx.options.strict) {
1133
+ const message = err instanceof Error ? err.message : String(err);
1134
+ const stack = err instanceof Error ? err.stack ?? "" : "";
1135
+ ctx.devServer?.hot?.send({
1136
+ type: "error",
1137
+ err: {
1138
+ message,
1139
+ stack,
1140
+ plugin: "animus-extract"
1141
+ }
1142
+ });
1143
+ }
1144
+ ctx.invalidateSystemReloadModules(true);
1317
1145
  return;
1318
1146
  }
1319
- ctx.mutateFileCache((cache) => cache.set(relPath, {
1320
- hash: (0, _animus_ui_extract_pipeline.contentHash)(source),
1321
- source
1322
- }));
1147
+ const merged = mergePublished(outcomes);
1148
+ applyPublished(ctx, merged);
1149
+ if (!merged.published) ctx.invalidateSystemReloadModules(false);
1150
+ else ctx.log(`HMR system reload complete: ${Math.round(performance.now() - reloadStart)}ms`);
1323
1151
  }
1324
1152
  /** The once-per-event analysis half; its result is what the remaining
1325
1153
  * environments act on. */
@@ -1327,76 +1155,47 @@ async function analyzeChangedFile(ctx, file, absFile, read) {
1327
1155
  const ext = (0, path.extname)(file);
1328
1156
  if (!ctx.extensionsSet.has(ext)) return { kind: "ignored" };
1329
1157
  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" };
1158
+ 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
1159
  const relPath = (0, path.relative)(ctx.rootDir, absFile);
1333
- let source;
1160
+ const outputDir = (0, _animus_ui_extract_pipeline.outputSourceGuard)(ctx.rootDir, ctx.outputExcludes)(absFile);
1161
+ if (outputDir !== null) {
1162
+ ctx.warn((0, _animus_ui_extract_pipeline.outputSourceMessage)(relPath, outputDir));
1163
+ return { kind: "ignored" };
1164
+ }
1165
+ let settled;
1334
1166
  try {
1335
- source = await readChangedSource(absFile, read);
1167
+ settled = read ? await read() : void 0;
1336
1168
  } catch {
1337
1169
  return { kind: "ignored" };
1338
1170
  }
1339
- const hash = (0, _animus_ui_extract_pipeline.contentHash)(source);
1340
- const cached = ctx.fileCache.get(relPath);
1341
- if (cached && cached.hash === hash) {
1171
+ const hmrStart = performance.now();
1172
+ const changes = changesOf("update", absFile);
1173
+ const outcomes = await ctx.submitChanges(settled === void 0 ? changes : {
1174
+ ...changes,
1175
+ sources: /* @__PURE__ */ new Map([[absFile, settled]])
1176
+ });
1177
+ const last = outcomes.at(-1)?.outcome;
1178
+ if (last === void 0) {
1342
1179
  ctx.trace(`HMR skip: ${relPath} (unchanged)`);
1343
1180
  return { kind: "unchanged" };
1344
1181
  }
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];
1182
+ if (last.kind === "held") return last.abortedOriginals.has(relPath) ? { kind: "ignored" } : { kind: "unchanged" };
1183
+ const merged = mergePublished([...outcomes, ...await reconcileSourceCorpus(ctx)]);
1184
+ applyPublished(ctx, merged);
1185
+ const published = ctx.session.corpus.published;
1186
+ const nativeEntry = published.analysisEntries.get(relPath);
1187
+ const presentationOnly = nativeEntry && !merged.endedHold ? isPresentationOnlyEdit(ctx, relPath, nativeEntry.source) : false;
1188
+ const redelivered = presentationOnly ? [] : [relPath, ...merged.redeliver];
1389
1189
  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]))
1190
+ ...merged.planChanged,
1191
+ ...merged.redeliver,
1192
+ ...extendingFiles(ctx, redelivered.flatMap((path$2) => published.ownership[path$2]?.analysisPaths ?? [path$2]))
1393
1193
  ])].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)" : ""}`);
1194
+ ctx.log(`HMR update: ${relPath} — total ${Math.round(performance.now() - hmrStart)}ms${presentationOnly ? " (presentation-only)" : ""}`);
1396
1195
  return {
1397
1196
  kind: "analyzed",
1398
1197
  staleDefinitionFiles,
1399
- systemPropsChanged,
1198
+ systemPropsChanged: merged.systemPropsChanged,
1400
1199
  presentationOnly
1401
1200
  };
1402
1201
  }
@@ -1415,17 +1214,14 @@ function isPresentationOnlyEdit(ctx, scannerRelPath, source) {
1415
1214
  return false;
1416
1215
  }
1417
1216
  }
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. */
1217
+ /** A deleted file leaves the Session's corpus, so its CSS stops being
1218
+ * re-emitted. */
1420
1219
  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;
1220
+ const merged = mergePublished(await ctx.submitChanges(changesOf("delete", absFile)));
1221
+ if (!merged.published) return;
1222
+ applyPublished(ctx, merged);
1427
1223
  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)));
1224
+ if (!merged.full) invalidateFileModules(ctx, [...merged.planChanged]);
1429
1225
  ctx.invalidateExtractedModules();
1430
1226
  }
1431
1227
  /** The per-environment invalidation half. Static CSS is left alone: it only
@@ -1534,7 +1330,7 @@ function loadVirtualModule(ctx, id) {
1534
1330
  return css;
1535
1331
  };
1536
1332
  if (id === RESOLVED_CSS_ID) {
1537
- if (!ctx.isProd && ctx.storedSheets) {
1333
+ if (!ctx.isProd && ctx.storedManifest) {
1538
1334
  const { variables, body } = (0, _animus_ui_extract_pipeline.assembleStylesheet)({
1539
1335
  layers: ctx.options.layers,
1540
1336
  variableCss: ctx.variableCss,
@@ -1604,24 +1400,26 @@ if (import.meta.hot) {
1604
1400
  }
1605
1401
  `;
1606
1402
  }
1607
- if (id === RESOLVED_SYSTEM_PROPS_ID) return systemPropsModuleSource(ctx);
1403
+ if (id === RESOLVED_SYSTEM_PROPS_ID) return ctx.systemPropsModule;
1608
1404
  return null;
1609
1405
  }
1610
1406
  //#endregion
1611
1407
  //#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;
1408
+ async function sourceKitConfig(rootDir, systemPath, engine) {
1409
+ const kits = /* @__PURE__ */ new Map();
1410
+ 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);
1411
+ if (kits.size === 0) return void 0;
1412
+ const names = [...kits.keys()];
1413
+ return {
1414
+ optimizeDeps: {
1415
+ exclude: names,
1416
+ include: [...kits.values()].flatMap((kit) => kit.dependencies.map((dependency) => `${kit.name} > ${dependency}`))
1417
+ },
1418
+ ssr: { noExternal: names }
1419
+ };
1622
1420
  }
1623
1421
  function animusExtract(options) {
1624
- (0, _animus_ui_extract_pipeline.assertNoRetiredEngineSelection)(options.engine);
1422
+ (0, _animus_ui_extract_pipeline.assertSupportedEngine)(options.engine);
1625
1423
  (0, _animus_ui_extract_pipeline.assertKnownOptionKeys)(options, ["verify", "appearanceBootstrap"], [{
1626
1424
  key: "root",
1627
1425
  reason: "the Vite driver derives its root from the resolved Vite config"
@@ -1633,10 +1431,11 @@ function animusExtract(options) {
1633
1431
  return {
1634
1432
  name: "animus-extract",
1635
1433
  enforce: "pre",
1636
- config(userConfig, env) {
1434
+ async config(userConfig, env) {
1637
1435
  const { mode } = (0, _animus_ui_extract_pipeline.resolveMode)(options.mode, () => env.command === "build" ? "production" : "development");
1638
1436
  const define = { __ANIMUS_DEV__: mode === "development" };
1639
- const kits = sourceKitConfig((0, path.resolve)(userConfig.root ?? process.cwd()), env.command === "serve");
1437
+ const root = (0, path.resolve)(userConfig.root ?? process.cwd());
1438
+ const kits = await sourceKitConfig(root, (0, path.resolve)(root, options.system), ctx.kitEngine());
1640
1439
  return kits ? {
1641
1440
  define,
1642
1441
  ...kits
@@ -1649,9 +1448,15 @@ function animusExtract(options) {
1649
1448
  configResolved(config) {
1650
1449
  applyResolvedConfig(ctx, config);
1651
1450
  },
1451
+ buildEnd() {
1452
+ if (!ctx.builds.end(this.environment ?? ctx.builds.single)) return;
1453
+ cancelSystemReload(ctx);
1454
+ ctx.close();
1455
+ },
1652
1456
  async buildStart() {
1653
- await runBuildStart(ctx, async (specifier) => {
1654
- return (await this.resolve(specifier))?.id ?? null;
1457
+ await ctx.builds.open(this.environment ?? ctx.builds.single);
1458
+ await runBuildStart(ctx, async (specifier, importer) => {
1459
+ return (await this.resolve(specifier, importer))?.id ?? null;
1655
1460
  }, ctx.isProd ? (fileName, source) => this.emitFile({
1656
1461
  type: "asset",
1657
1462
  name: fileName,