@animus-ui/vite-plugin 0.1.1 → 0.1.2

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
@@ -2,71 +2,17 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- //#region \0rolldown/runtime.js
6
- var __create = Object.create;
7
- var __defProp = Object.defineProperty;
8
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
- var __getOwnPropNames = Object.getOwnPropertyNames;
10
- var __getProtoOf = Object.getPrototypeOf;
11
- var __hasOwnProp = Object.prototype.hasOwnProperty;
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
14
- key = keys[i];
15
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
16
- get: ((k) => from[k]).bind(null, key),
17
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
18
- });
19
- }
20
- return to;
21
- };
22
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
- value: mod,
24
- enumerable: true
25
- }) : target, mod));
26
- //#endregion
27
5
  let _animus_ui_extract_pipeline = require("@animus-ui/extract/pipeline");
28
- let browserslist = require("browserslist");
29
- browserslist = __toESM(browserslist);
30
- let crypto = require("crypto");
31
6
  let fs = require("fs");
32
- let lightningcss = require("lightningcss");
33
7
  let path = require("path");
34
- //#region src/analyze-project-args.ts
35
- /** @internal */
36
- function buildAnalyzeProjectArgs(inputs) {
37
- return [
38
- inputs.filesJson,
39
- inputs.scalesJson,
40
- inputs.variableMapJson,
41
- inputs.contextualVarsJson,
42
- inputs.propConfigJson,
43
- inputs.groupRegistryJson,
44
- inputs.packageResolutionJson,
45
- inputs.devMode,
46
- inputs.emitterConfigJson,
47
- inputs.selectorAliasesJson,
48
- null,
49
- inputs.globalStyleBlocksJson,
50
- inputs.pathAliasesJson,
51
- inputs.keyframesJson
52
- ];
53
- }
54
- //#endregion
55
- //#region src/manifest-diagnostics.ts
56
- function surfaceManifestDiagnostics(manifest, warn) {
57
- for (const diagnostic of manifest.diagnostics ?? []) if (diagnostic.kind === "bail") warn(`⚠ ${diagnostic.component} not extracted: ${diagnostic.message}`);
58
- else if (diagnostic.kind === "skip") warn(`⚠ ${diagnostic.component}: skipped ${diagnostic.message}`);
59
- else if (diagnostic.kind === "warn") warn(`⚠ ${diagnostic.file}: ${diagnostic.component}: ${diagnostic.message}`);
60
- }
61
- //#endregion
62
- //#region src/index.ts
8
+ let crypto = require("crypto");
9
+ //#region src/constants.ts
63
10
  const VIRTUAL_CSS_ID = "virtual:animus/styles.css";
64
11
  const RESOLVED_CSS_ID = "\0virtual:animus/styles.css";
65
12
  const VIRTUAL_COMPONENTS_ID = "virtual:animus/components.js";
66
13
  const RESOLVED_COMPONENTS_ID = "\0virtual:animus/components.js";
67
14
  const VIRTUAL_BRIDGE_ID = "virtual:animus/hmr-bridge.js";
68
15
  const RESOLVED_BRIDGE_ID = "\0virtual:animus/hmr-bridge.js";
69
- const VIRTUAL_SYSTEM_PROPS_ID = "virtual:animus/system-props";
70
16
  const RESOLVED_SYSTEM_PROPS_ID = "\0virtual:animus/system-props";
71
17
  const DEFAULT_EXCLUDE = [
72
18
  "node_modules",
@@ -74,70 +20,295 @@ const DEFAULT_EXCLUDE = [
74
20
  ".test.",
75
21
  ".spec."
76
22
  ];
77
- function discoverFiles(dir, rootDir, excludePatterns, extensionsSet) {
78
- const results = [];
79
- let entries;
80
- try {
81
- entries = (0, fs.readdirSync)(dir, { encoding: "utf8" });
82
- } catch {
83
- return results;
23
+ //#endregion
24
+ //#region src/build-start.ts
25
+ /**
26
+ * buildStart: load the system, discover and ingest sources (local +
27
+ * external packages), run whole-project analysis, and log the report.
28
+ * `resolveSpecifier` is the bundler seam — Vite's `this.resolve` mapped to
29
+ * an absolute id.
30
+ */
31
+ async function runBuildStart(ctx, resolveSpecifier) {
32
+ (0, _animus_ui_extract_pipeline.clearEngineCache)(ctx.engineApi);
33
+ let t0 = performance.now();
34
+ ctx.loadSystem();
35
+ if (ctx.options.layers) {
36
+ (0, _animus_ui_extract_pipeline.validateLayerOrder)(ctx.options.layers);
37
+ ctx.log(`Custom layers: [${ctx.options.layers.join(", ")}]`);
84
38
  }
85
- for (const entry of entries) {
86
- const fullPath = (0, path.join)(dir, entry);
87
- const relativePath = (0, path.relative)(rootDir, fullPath);
88
- if (excludePatterns.some((pattern) => fullPath.includes(pattern) || relativePath.includes(pattern))) continue;
89
- let stat;
90
- try {
91
- stat = (0, fs.statSync)(fullPath);
92
- } catch {
93
- continue;
39
+ if (ctx.verbose) {
40
+ const propCount = Object.keys(JSON.parse(ctx.system.propConfigJson)).length;
41
+ const groupCount = Object.keys(JSON.parse(ctx.system.groupRegistryJson)).length;
42
+ ctx.log(`System loaded: ${propCount} props, ${groupCount} groups (${Math.round(performance.now() - t0)}ms)`);
43
+ }
44
+ t0 = performance.now();
45
+ const excludePatterns = ctx.options.exclude ?? DEFAULT_EXCLUDE;
46
+ ctx.extensionsSet = new Set(ctx.options.extensions ?? _animus_ui_extract_pipeline.DEFAULT_EXTENSIONS);
47
+ const shouldHandleMdx = ctx.extensionsSet.has(".mdx");
48
+ let mdxMissingDepWarned = false;
49
+ const preprocessMdxEntry = async (source, relPath) => {
50
+ const result = await (0, _animus_ui_extract_pipeline.preprocessMdx)(source, relPath);
51
+ if (result.kind === "missing-dep") {
52
+ if (!mdxMissingDepWarned) {
53
+ ctx.warn("⚠ .mdx in extensions but @mdx-js/mdx not installed; MDX files skipped");
54
+ mdxMissingDepWarned = true;
55
+ }
56
+ return null;
57
+ }
58
+ if (result.kind === "error") {
59
+ ctx.warn(`⚠ MDX preprocessing failed for ${relPath}: ${result.error}`);
60
+ return null;
61
+ }
62
+ return {
63
+ source: result.source,
64
+ relPath: relPath + ".tsx"
65
+ };
66
+ };
67
+ const filePaths = (0, _animus_ui_extract_pipeline.discoverFiles)(ctx.rootDir, ctx.rootDir, excludePatterns, ctx.extensionsSet);
68
+ const fileEntries = [];
69
+ for (const filePath of filePaths) try {
70
+ let source = (0, fs.readFileSync)(filePath, "utf-8");
71
+ let relPath = (0, path.relative)(ctx.rootDir, filePath);
72
+ if (shouldHandleMdx && (0, path.extname)(filePath) === ".mdx") {
73
+ const processed = await preprocessMdxEntry(source, relPath);
74
+ if (!processed) continue;
75
+ source = processed.source;
76
+ relPath = processed.relPath;
77
+ }
78
+ const hash = !ctx.isProd ? (0, _animus_ui_extract_pipeline.contentHash)(source) : void 0;
79
+ fileEntries.push({
80
+ path: relPath,
81
+ source,
82
+ hash
83
+ });
84
+ if (!ctx.isProd && hash) ctx.fileCache.set(relPath, {
85
+ hash,
86
+ source
87
+ });
88
+ } catch {}
89
+ const localFileCount = fileEntries.length;
90
+ const packageSpecifiers = (0, _animus_ui_extract_pipeline.extractSystemFilePackages)(ctx.resolvedSystemPath);
91
+ ctx.externalSourceEntries.clear();
92
+ const collected = await (0, _animus_ui_extract_pipeline.collectExternalPackageSources)({
93
+ specifiers: packageSpecifiers,
94
+ resolveSpecifier,
95
+ rootDir: ctx.rootDir,
96
+ extensionsSet: ctx.extensionsSet,
97
+ hasEntry: (relPath) => fileEntries.some((e) => e.path === relPath),
98
+ preprocessFile: async (source, relPath, absPath) => {
99
+ if (shouldHandleMdx && (0, path.extname)(absPath) === ".mdx") return preprocessMdxEntry(source, relPath);
100
+ return {
101
+ source,
102
+ relPath
103
+ };
104
+ },
105
+ onUnreadable: (relPath, err) => ctx.warn(`skipped unreadable package file ${relPath}: ${String(err)}`)
106
+ });
107
+ ctx.packageMap = collected.packageMap;
108
+ ctx.externalPackageOutcomes = collected.outcomes;
109
+ for (const [specifier, srcEntry] of collected.sourceEntries) ctx.externalSourceEntries.set(specifier, srcEntry);
110
+ for (const entry of collected.entries) {
111
+ const hash = !ctx.isProd ? (0, _animus_ui_extract_pipeline.contentHash)(entry.source) : void 0;
112
+ fileEntries.push({
113
+ path: entry.path,
114
+ source: entry.source,
115
+ hash
116
+ });
117
+ if (!ctx.isProd && hash) ctx.fileCache.set(entry.path, {
118
+ hash,
119
+ source: entry.source
120
+ });
121
+ }
122
+ ctx.externalPackageDirs = collected.packageDirs;
123
+ const packageFileCount = fileEntries.length - localFileCount;
124
+ ctx.log(`Discovered ${fileEntries.length} files (${packageFileCount} from packages) (${Math.round(performance.now() - t0)}ms)`);
125
+ t0 = performance.now();
126
+ ctx.runAnalysis(fileEntries);
127
+ if (ctx.storedManifest) {
128
+ const report = ctx.storedManifest.report;
129
+ if (report) {
130
+ ctx.log(`Extracted ${report.components_extracted}/${report.components_total} components (${Math.round(performance.now() - t0)}ms)`);
131
+ ctx.logTimingWaterfall(ctx.storedManifest.timing ?? {});
132
+ ctx.log(`Reconciliation: ${report.components_extracted} kept, ${report.variants_eliminated} variants pruned, ${report.states_eliminated} states pruned`);
133
+ const details = report.eliminated_details || [];
134
+ for (const d of details) if (d.kind === "component") ctx.warn(`⚠ ${d.component} eliminated: ${d.reason}`);
135
+ else if (d.kind === "prospective_component") ctx.warn(`⚠ ${d.component} would be eliminated in production: ${d.reason}`);
136
+ else if (d.kind === "variant") ctx.warn(`⚠ ${d.component} variant '${d.name}' pruned: ${d.reason}`);
137
+ else if (d.kind === "state") ctx.warn(`⚠ ${d.component} state '${d.name}' pruned: ${d.reason}`);
94
138
  }
95
- if (stat.isDirectory()) results.push(...discoverFiles(fullPath, rootDir, excludePatterns, extensionsSet));
96
- else if (extensionsSet.has((0, path.extname)(entry))) results.push(fullPath);
139
+ ctx.log(`CSS: ${ctx.resolvedComponentCss.length} bytes (${Object.keys(ctx.storedManifest.components || {}).length} components)`);
140
+ if (!ctx.isProd && ctx.storedSheets) {
141
+ const staticSize = (0, _animus_ui_extract_pipeline.assembleStylesheet)({
142
+ layers: ctx.options.layers,
143
+ variableCss: ctx.system.variableCss,
144
+ globalCss: ctx.globalCss
145
+ }).length;
146
+ const componentSize = ctx.resolvedComponentCss.length;
147
+ ctx.log(`Delivery: split mode — static ${staticSize} bytes, components ${componentSize} bytes (adopted stylesheet)`);
148
+ } else ctx.log("Delivery: single file mode (production)");
97
149
  }
98
- return results;
150
+ const { declaration } = (0, _animus_ui_extract_pipeline.assembleStylesheet)({
151
+ layers: ctx.options.layers,
152
+ variableCss: "",
153
+ globalCss: "",
154
+ split: true
155
+ });
156
+ ctx.layerDeclaration = declaration;
157
+ if (ctx.options.verify) ctx.runSelfVerify();
99
158
  }
159
+ //#endregion
160
+ //#region src/config.ts
100
161
  /**
101
- * Resolve browser targets for Lightning CSS.
102
- * Priority: explicit config → project browserslist → 'defaults' fallback.
162
+ * configResolved: capture build mode, root, and logger; resolve Lightning
163
+ * CSS targets; harvest resolve.alias into the Rust `pathAliasesJson`
164
+ * contract via the shared encoder.
103
165
  */
104
- function resolveLightningTargets(explicitTargets, rootDir) {
105
- let queries;
106
- if (explicitTargets) queries = Array.isArray(explicitTargets) ? explicitTargets : [explicitTargets];
107
- else {
108
- const detected = (0, browserslist.default)(void 0, { path: rootDir });
109
- queries = detected.length > 0 ? detected : (0, browserslist.default)("defaults");
110
- }
111
- return (0, lightningcss.browserslistToTargets)(Array.isArray(queries) && typeof queries[0] === "string" && queries[0].includes(" ") ? (0, browserslist.default)(queries) : queries);
166
+ function applyResolvedConfig(ctx, config) {
167
+ ctx.isProd = config.command === "build";
168
+ ctx.rootDir = config.root;
169
+ ctx.logger = config.logger;
170
+ ctx.lcssTargets = (0, _animus_ui_extract_pipeline.resolveLightningTargets)(ctx.options.targets, ctx.rootDir);
171
+ ctx.log(`Lightning CSS targets resolved (${Object.keys(ctx.lcssTargets).length} browsers)`);
172
+ const rawAlias = config.resolve?.alias;
173
+ if (rawAlias) {
174
+ const pairs = [];
175
+ if (Array.isArray(rawAlias)) {
176
+ for (const entry of rawAlias) if (typeof entry.find === "string" && typeof entry.replacement === "string") pairs.push({
177
+ pattern: entry.find,
178
+ target: entry.replacement,
179
+ kind: "prefix"
180
+ });
181
+ } else if (typeof rawAlias === "object" && rawAlias !== null) {
182
+ for (const [key, value] of Object.entries(rawAlias)) if (typeof value === "string") pairs.push({
183
+ pattern: key,
184
+ target: value
185
+ });
186
+ }
187
+ const built = (0, _animus_ui_extract_pipeline.buildPathAliasesJson)(pairs, ctx.rootDir);
188
+ if (built) {
189
+ ctx.pathAliasesJson = built.json;
190
+ ctx.log(`Path aliases forwarded: ${built.count} entries`);
191
+ }
192
+ }
112
193
  }
194
+ //#endregion
195
+ //#region src/hot-update-events.ts
113
196
  /**
114
- * Post-process CSS with Lightning CSS: autoprefixing + optional minification.
115
- * On failure, returns the original CSS and logs a warning.
197
+ * Once-per-file-event coordination across the `hotUpdate` dispatches.
198
+ *
199
+ * Vite 8 calls `hotUpdate` once per environment for a single file event — the
200
+ * client environment first, then every non-client environment (see
201
+ * `handleHMRUpdate` in vite/dist/node/chunks/node.js). Analysis work (cache
202
+ * mutation, engine re-analysis, geological reset scheduling) must happen
203
+ * exactly once per event; module invalidation must happen per environment
204
+ * against that environment's own graph.
205
+ *
206
+ * The client dispatch always claims — it is dispatched first, so nothing can
207
+ * have claimed the event before it, and claiming unconditionally keeps a
208
+ * same-millisecond `(file, timestamp)` collision from ever costing a client
209
+ * analysis. A non-client dispatch claims only when the event carries no
210
+ * recorded claim at all, which happens when the plugin is filtered out of the
211
+ * client environment.
212
+ *
213
+ * Keys are retained for a bounded number of recent events so that interleaved
214
+ * file events (Vite does not serialize watcher handlers) still find their own
215
+ * result.
116
216
  */
117
- function postProcessCss(css, opts) {
118
- if (!css) return css;
119
- try {
120
- return (0, lightningcss.transform)({
121
- filename: "animus-extracted.css",
122
- code: Buffer.from(css),
123
- minify: opts.minify,
124
- targets: opts.targets
125
- }).code.toString();
126
- } catch (e) {
127
- const msg = e instanceof Error ? e.message : String(e);
128
- (opts.warnFn ?? console.warn)(`[animus] Lightning CSS post-processing failed: ${msg}`);
129
- return css;
217
+ var HotUpdateEvents = class {
218
+ results = /* @__PURE__ */ new Map();
219
+ constructor(historyLimit = 16) {
220
+ this.historyLimit = historyLimit;
130
221
  }
222
+ /** True when this environment dispatch owns the event's analysis work. */
223
+ claim(environmentName, file, timestamp) {
224
+ const key = eventKey(file, timestamp);
225
+ if (environmentName !== "client" && this.results.has(key)) return false;
226
+ this.results.set(key, { kind: "ignored" });
227
+ while (this.results.size > this.historyLimit) {
228
+ const oldest = this.results.keys().next().value;
229
+ if (oldest === void 0) break;
230
+ this.results.delete(oldest);
231
+ }
232
+ return true;
233
+ }
234
+ /** Publish the owning dispatch's decision to the other environments. */
235
+ record(file, timestamp, result) {
236
+ this.results.set(eventKey(file, timestamp), result);
237
+ }
238
+ /** The decision for this event — `ignored` when the event was evicted. */
239
+ resultOf(file, timestamp) {
240
+ return this.results.get(eventKey(file, timestamp)) ?? { kind: "ignored" };
241
+ }
242
+ };
243
+ /** A path can hold any character but NUL, so NUL is the safe separator. */
244
+ function eventKey(file, timestamp) {
245
+ return `${file}\u0000${timestamp}`;
131
246
  }
132
- /** Compute MD5 content hash for a string. */
133
- function contentHash(source) {
134
- return (0, crypto.createHash)("md5").update(source).digest("hex");
247
+ //#endregion
248
+ //#region src/reset-coalescer.ts
249
+ /**
250
+ * Coalesces bursts of system-dependency events into single geological
251
+ * resets: a quiescence window before starting, at most one reset in
252
+ * flight, and exactly one follow-up when requests arrive mid-reset. A
253
+ * package build regenerating its dist fires one reset, not one per file.
254
+ *
255
+ * Timer functions are injected seams — `vi.mock` on node builtins silently
256
+ * no-ops under vite-plus-test, so tests drive a manual scheduler instead.
257
+ */
258
+ var ResetCoalescer = class {
259
+ timer = null;
260
+ running = false;
261
+ dirty = false;
262
+ constructor(run, quietMs = 60, schedule = (fn, ms) => setTimeout(fn, ms), cancel = (timer) => clearTimeout(timer)) {
263
+ this.run = run;
264
+ this.quietMs = quietMs;
265
+ this.schedule = schedule;
266
+ this.cancel = cancel;
267
+ }
268
+ /** Ask for a reset; bursts within the quiescence window collapse. */
269
+ request() {
270
+ if (this.running) {
271
+ this.dirty = true;
272
+ return;
273
+ }
274
+ if (this.timer !== null) this.cancel(this.timer);
275
+ this.timer = this.schedule(() => {
276
+ this.timer = null;
277
+ this.running = true;
278
+ try {
279
+ this.run();
280
+ } finally {
281
+ this.running = false;
282
+ if (this.dirty) {
283
+ this.dirty = false;
284
+ this.request();
285
+ }
286
+ }
287
+ }, this.quietMs);
288
+ }
289
+ };
290
+ //#endregion
291
+ //#region src/context.ts
292
+ /** Pre-load / failed-load defaults — the plugin's historical initial state. */
293
+ function emptySystemConfig() {
294
+ return {
295
+ propConfigJson: "{}",
296
+ groupRegistryJson: "{}",
297
+ scalesJson: "{}",
298
+ variableMapJson: "{}",
299
+ variableCss: "",
300
+ contextualVarsJson: "{}",
301
+ selectorAliasesJson: null,
302
+ globalStyleBlocksJson: null,
303
+ keyframesJson: null
304
+ };
135
305
  }
136
306
  /**
137
307
  * Reconstruct file entries from cache, including content hashes.
138
308
  * For unchanged files (hash matches changedPath), sends empty source
139
309
  * to avoid serializing full source text across the NAPI boundary.
140
- * Rust cache-hit path never reads file.source, so empty string is safe.
310
+ * The engine adapter's `rehydrateFilesJson` refills empty sources from
311
+ * this same cache before analyze.
141
312
  */
142
313
  function buildFileEntriesFromCache(cache, changedPath) {
143
314
  const entries = [];
@@ -148,223 +319,276 @@ function buildFileEntriesFromCache(cache, changedPath) {
148
319
  });
149
320
  return entries;
150
321
  }
151
- function animusExtract(options) {
152
- const resolvedEngine = options.engine ?? "v2";
153
- const engineModuleId = resolvedEngine === "v2" ? "@animus-ui/extract/engine-v2" : "@animus-ui/extract";
154
- const requireEngine = () => require(engineModuleId);
155
- let v2Engine = null;
156
- const v2SentSources = /* @__PURE__ */ new Map();
157
- let v2DriftWarned = false;
158
- const engineApi = () => {
159
- if (resolvedEngine !== "v2") return requireEngine();
160
- const native = requireEngine();
161
- return {
162
- loadSystemModule: (...args) => native.loadSystemModule(...args),
163
- analyzeProject: (filesJsonRaw, themeJson_, variableMapJson_, contextualVarsJson_, configJson_, groupRegistryJson_, packageResolutionJson_, devMode, emitterConfigJson, selectorAliasesJson_, _selectorOrderJson, globalStyleBlocksJson_, pathAliasesJson_, keyframesJson_) => {
164
- let filesJson = filesJsonRaw;
165
- if (filesJsonRaw.includes("\"source\":\"\"")) {
166
- const entries = JSON.parse(filesJsonRaw);
167
- for (const entry of entries) if (entry.source === "") entry.source = fileCache.get(entry.path)?.source ?? "";
168
- filesJson = JSON.stringify(entries);
169
- }
170
- v2SentSources.clear();
171
- for (const entry of JSON.parse(filesJson)) v2SentSources.set(entry.path, entry.source);
172
- const emitterConfig = emitterConfigJson ? JSON.parse(emitterConfigJson) : {};
173
- v2Engine = null;
174
- const engine = new native.ExtractEngine({
175
- runtimeImport: emitterConfig.runtime_import ?? void 0,
176
- cssModuleId: emitterConfig.css_module_id ?? void 0,
177
- systemPropsModuleId: emitterConfig.system_props_module_id ?? void 0,
178
- themeJson: themeJson_,
179
- variableMapJson: variableMapJson_,
180
- contextualVarsJson: contextualVarsJson_ ?? void 0,
181
- configJson: configJson_,
182
- groupRegistryJson: groupRegistryJson_,
183
- selectorAliasesJson: selectorAliasesJson_ ?? void 0,
184
- globalStyleBlocksJson: globalStyleBlocksJson_ ?? void 0,
185
- keyframesJson: keyframesJson_ ?? void 0,
186
- packageResolutionJson: packageResolutionJson_ ?? void 0,
187
- pathAliasesJson: pathAliasesJson_ ?? void 0,
188
- devMode
189
- });
190
- v2Engine = engine;
191
- return engine.analyze(filesJson);
322
+ /**
323
+ * Drop a deleted (or renamed-away) file from the dev file cache so its
324
+ * last-known source stops riding along as a ghost entry on every later
325
+ * re-analysis. Both key forms are tried: the plain rootDir-relative path, and
326
+ * the `.tsx` suffix MDX sources carry after preprocessing. External package
327
+ * entries are rootDir-relative too (with leading `..` segments) and prune the
328
+ * same way. Returns whether an entry was actually removed.
329
+ */
330
+ function pruneFileCache(cache, rootDir, absPath) {
331
+ const rel = (0, path.relative)(rootDir, (0, path.resolve)(absPath));
332
+ return cache.delete(rel) || cache.delete(rel + ".tsx");
333
+ }
334
+ /**
335
+ * Per-plugin-instance state and the pipeline operations over it. Hook
336
+ * bodies live in their own modules (build-start, virtual-modules,
337
+ * transform, hmr) and receive this context — the plugin factory in
338
+ * index.ts only wires Vite hooks to those functions.
339
+ *
340
+ * A class rather than closure variables so each hook module names exactly
341
+ * the state it touches, and the engine store (DEF-1: per-instance, never
342
+ * module-level) is explicit.
343
+ */
344
+ var PluginContext = class {
345
+ options;
346
+ verbose;
347
+ /** Serialized staticCss forced-emission declarations (stable key order). */
348
+ staticCssJson;
349
+ isProd = false;
350
+ rootDir = "";
351
+ logger = null;
352
+ /** System-derived config (shared SystemConfig shape). */
353
+ system = emptySystemConfig();
354
+ lcssTargets = {};
355
+ pathAliasesJson = null;
356
+ extensionsSet;
357
+ storedManifest = null;
358
+ storedManifestJson = "";
359
+ globalCss = "";
360
+ resolvedComponentCss = "";
361
+ storedSheets = null;
362
+ layerDeclaration = "";
363
+ fragmentCache = /* @__PURE__ */ new Map();
364
+ reverseProvenance = {};
365
+ storedSystemPropMapJson = "{}";
366
+ storedDynamicPropsJson = "{}";
367
+ storedTransformsSource = "{}";
368
+ fileCache = /* @__PURE__ */ new Map();
369
+ hotUpdateEvents = new HotUpdateEvents();
370
+ packageMap = {};
371
+ externalPackageDirs = [];
372
+ externalSourceEntries = /* @__PURE__ */ new Map();
373
+ externalPackageOutcomes = [];
374
+ devServer;
375
+ bridgeInjected = false;
376
+ resolvedSystemPath = null;
377
+ systemDependencyKeys = /* @__PURE__ */ new Set();
378
+ systemDependencyPaths = [];
379
+ v2Engine = null;
380
+ v2SentSources = null;
381
+ v2DriftWarned = false;
382
+ /** Single engine choke-point for every native extraction call. */
383
+ engineApi;
384
+ constructor(options) {
385
+ this.options = options;
386
+ this.staticCssJson = (0, _animus_ui_extract_pipeline.serializeStaticCss)(options.staticCss);
387
+ this.verbose = options.verbose || process.env.ANIMUS_DEBUG === "1" || process.env.ANIMUS_DEBUG === "true";
388
+ this.extensionsSet = new Set(options.extensions ?? _animus_ui_extract_pipeline.DEFAULT_EXTENSIONS);
389
+ const engineModuleId = "@animus-ui/extract";
390
+ this.engineApi = (0, _animus_ui_extract_pipeline.createV2EngineApi)({
391
+ label: "animus-extract",
392
+ isV2: () => true,
393
+ loadNativeEngine: () => require(engineModuleId),
394
+ rehydrateFilesJson: (filesJsonRaw) => {
395
+ if (!filesJsonRaw.includes("\"source\":\"\"")) return filesJsonRaw;
396
+ const entries = JSON.parse(filesJsonRaw);
397
+ for (const entry of entries) if (entry.source === "") entry.source = this.fileCache.get(entry.path)?.source ?? "";
398
+ return JSON.stringify(entries);
192
399
  },
193
- transformFile: (source, path$2, _manifest) => {
194
- if (!v2Engine) throw new Error("[animus-extract] v2 transform before analyze — engine state is per-instance");
195
- const sent = v2SentSources.get(path$2);
196
- if (sent !== void 0 && sent !== source && !v2DriftWarned) {
197
- v2DriftWarned = true;
198
- console.warn(`[animus-extract] v2: transform-time source for ${path$2} differs from analyze-time source — an upstream plugin transform may be reverted (engine v2 emits from analyzed sources)`);
400
+ store: {
401
+ getEngine: () => this.v2Engine,
402
+ setEngine: (engine) => {
403
+ this.v2Engine = engine;
404
+ },
405
+ getSentSources: () => this.v2SentSources,
406
+ setSentSources: (sources) => {
407
+ this.v2SentSources = sources;
408
+ },
409
+ getDriftWarned: () => this.v2DriftWarned,
410
+ setDriftWarned: (value) => {
411
+ this.v2DriftWarned = value;
199
412
  }
200
- return JSON.parse(v2Engine.transformFile(path$2));
201
- },
202
- clearAnalysisCache: () => {
203
- if (v2Engine) v2Engine.clearCache();
204
- v2Engine = null;
205
413
  }
206
- };
207
- };
208
- let themeJson = "{}";
209
- let variableMapJson = "{}";
210
- let contextualVarsJson = "{}";
211
- let configJson = "{}";
212
- let groupRegistryJson = "{}";
213
- let selectorAliasesJson = null;
214
- let isProd = false;
215
- let rootDir = "";
216
- const verbose = options.verbose || process.env.ANIMUS_DEBUG === "1" || process.env.ANIMUS_DEBUG === "true";
217
- let logger = null;
218
- function log(msg) {
219
- if (verbose) (logger ?? console).info(`[animus] ${msg}`);
220
- }
221
- function warn(msg) {
222
- (logger ?? console).warn(`[animus] ${msg}`);
223
- }
224
- function logTimingWaterfall(timing) {
225
- if (!verbose) return;
226
- for (const [key, label] of [
227
- ["parseAndWalk", "parse+walk"],
228
- ["importResolution", "imports"],
229
- ["extensionProvenance", "provenance"],
230
- ["topologicalSort", "topo-sort"],
231
- ["chainEvaluation", "chains"],
232
- ["jsxScanning", "jsx-scan"],
233
- ["systemPropAggregation", "sys-props"],
234
- ["usageLedger", "usage"],
235
- ["reconciliation", "reconcile"],
236
- ["cssGeneration", "css-gen"],
237
- ["manifestSerialization", "serialize"]
238
- ]) {
239
- const ms = timing[key] ?? 0;
240
- const pad = " ".repeat(Math.max(0, 15 - label.length));
241
- const extra = key === "parseAndWalk" ? ` (${timing.fileCount ?? 0} files, ${timing.cacheHits ?? 0} cached)` : "";
242
- log(` ${label}${pad}${String(ms).padStart(5)}ms${extra}`);
243
- }
414
+ });
415
+ }
416
+ log(msg) {
417
+ if (this.verbose) (this.logger ?? console).info(`[animus] ${msg}`);
418
+ }
419
+ warn(msg) {
420
+ (this.logger ?? console).warn(`[animus] ${msg}`);
421
+ }
422
+ logTimingWaterfall(timing) {
423
+ if (!this.verbose) return;
424
+ for (const line of (0, _animus_ui_extract_pipeline.formatRustTimingWaterfall)(timing, {
425
+ indent: " ",
426
+ labelWidth: 15
427
+ })) this.log(line);
244
428
  }
245
- let lcssTargets = {};
246
- let variableCss = "";
247
- let globalCss = "";
248
- let globalStyleBlocksJson = null;
249
- let keyframesBlocksJson = null;
250
- let pathAliasesJson = null;
251
- let extensionsSet = new Set(options.extensions ?? _animus_ui_extract_pipeline.DEFAULT_EXTENSIONS);
252
- let storedManifest = null;
253
- let storedManifestJson = "";
254
- let resolvedComponentCss = "";
255
- let storedSheets = null;
256
- let layerDeclaration = "";
257
- let fragmentCache = /* @__PURE__ */ new Map();
258
- let reverseProvenance = {};
259
- let storedSystemPropMapJson = "{}";
260
- let storedDynamicPropsJson = "{}";
261
- let storedTransformsSource = "{}";
262
- const fileCache = /* @__PURE__ */ new Map();
263
- let packageMap = {};
264
- let externalPackageDirs = [];
265
- const externalSourceEntries = /* @__PURE__ */ new Map();
266
- let devServer;
267
- let bridgeInjected = false;
268
- let resolvedSystemPath = null;
269
429
  /**
270
- * Load a SystemInstance via Rust NAPI (rquickjs bundled eval).
271
- * The Rust crate reads the system file, strips TS types via OXC, resolves
272
- * workspace package dependencies, bundles all modules, evaluates with
273
- * rquickjs, and returns serialized config directly — no subprocess needed.
430
+ * Load a SystemInstance via Rust NAPI (rquickjs bundled eval) into
431
+ * `this.system`. On failure the previous config is kept (strict mode
432
+ * throws instead).
274
433
  */
275
- function loadSystem() {
276
- resolvedSystemPath = (0, path.resolve)(rootDir, options.system);
434
+ loadSystem() {
435
+ this.resolvedSystemPath = (0, path.resolve)(this.rootDir, this.options.system);
436
+ for (const key of (0, _animus_ui_extract_pipeline.toWatchKeys)(this.resolvedSystemPath)) this.systemDependencyKeys.add(key);
277
437
  try {
278
- const { loadSystemModule } = engineApi();
279
- const config = loadSystemModule(resolvedSystemPath, rootDir);
280
- configJson = config.propConfig;
281
- groupRegistryJson = config.groupRegistry;
282
- selectorAliasesJson = config.selectorAliases || null;
283
- themeJson = config.scalesJson;
284
- variableMapJson = config.variableMapJson;
285
- variableCss = config.variableCss;
286
- contextualVarsJson = config.contextualVarsJson;
287
- globalStyleBlocksJson = config.globalStyleBlocks || null;
288
- keyframesBlocksJson = config.keyframesBlocks || null;
438
+ this.system = (0, _animus_ui_extract_pipeline.loadSystemConfig)(this.engineApi, {
439
+ systemPath: this.resolvedSystemPath,
440
+ rootDir: this.rootDir,
441
+ prefix: this.options.prefix
442
+ });
443
+ const deps = this.system.dependencies ?? [];
444
+ const keys = /* @__PURE__ */ new Set();
445
+ for (const key of (0, _animus_ui_extract_pipeline.toWatchKeys)(this.resolvedSystemPath)) keys.add(key);
446
+ for (const dep of deps) for (const key of (0, _animus_ui_extract_pipeline.toWatchKeys)(dep)) keys.add(key);
447
+ this.systemDependencyKeys = keys;
448
+ this.systemDependencyPaths = deps;
449
+ this.registerSystemWatchPaths();
289
450
  } catch (e) {
290
- if (options.strict) throw new Error(`[animus-extract] Failed to load system from ${resolvedSystemPath}: ${e}`, { cause: e });
291
- console.warn(`[animus-extract] Failed to load system from ${resolvedSystemPath}:`, e);
451
+ if (this.options.strict) throw new Error(`[animus-extract] Failed to load system from ${this.resolvedSystemPath}: ${e}`, { cause: e });
452
+ console.warn(`[animus-extract] Failed to load system from ${this.resolvedSystemPath}:`, e);
292
453
  }
293
454
  }
294
455
  /**
295
- * Run project analysis and update the manifest.
296
- *
297
- * Pipeline: NAPI analyzeProject → unit fallback.
298
- * Transforms are resolved in-process via boa_engine during Rust extraction.
299
- * No subprocess needed.
456
+ * Run project analysis via the shared `runProjectAnalysis` and update
457
+ * all manifest-derived state.
300
458
  */
301
- function runAnalysis(fileEntries) {
459
+ runAnalysis(fileEntries) {
302
460
  try {
303
- const { analyzeProject } = engineApi();
304
- const emitterConfig = JSON.stringify({
305
- runtime_import: "@animus-ui/system",
306
- css_module_id: "virtual:animus/styles.css"
461
+ const result = (0, _animus_ui_extract_pipeline.runProjectAnalysis)(this.engineApi, {
462
+ fileEntries,
463
+ packageMap: this.packageMap,
464
+ system: this.system,
465
+ emitter: {
466
+ runtimeImport: "@animus-ui/system",
467
+ cssModuleId: VIRTUAL_CSS_ID
468
+ },
469
+ pathAliasesJson: this.pathAliasesJson,
470
+ staticCssJson: this.staticCssJson,
471
+ devMode: !this.isProd,
472
+ warn: (m) => this.warn(m)
307
473
  });
308
- const manifestJson = analyzeProject(...buildAnalyzeProjectArgs({
309
- filesJson: JSON.stringify(fileEntries),
310
- scalesJson: themeJson,
311
- variableMapJson,
312
- contextualVarsJson: contextualVarsJson || null,
313
- propConfigJson: configJson,
314
- groupRegistryJson,
315
- packageResolutionJson: JSON.stringify(packageMap),
316
- devMode: !isProd,
317
- emitterConfigJson: emitterConfig,
318
- selectorAliasesJson,
319
- globalStyleBlocksJson,
320
- pathAliasesJson,
321
- keyframesJson: keyframesBlocksJson
322
- }));
323
- storedManifest = JSON.parse(manifestJson);
324
- storedManifestJson = manifestJson;
325
- surfaceManifestDiagnostics(storedManifest, warn);
326
- storedSystemPropMapJson = JSON.stringify(storedManifest?.system_prop_map ?? {});
327
- const dynamicProps = storedManifest?.dynamic_props ?? {};
328
- const newDynamicPropsJson = JSON.stringify(dynamicProps);
329
- if (newDynamicPropsJson !== storedDynamicPropsJson) {
330
- storedDynamicPropsJson = newDynamicPropsJson;
331
- const usedTransformNames = /* @__PURE__ */ new Set();
332
- for (const meta of Object.values(dynamicProps)) if (meta.transform_name) usedTransformNames.add(meta.transform_name);
333
- if (storedManifest?.components) {
334
- for (const comp of Object.values(storedManifest.components)) if (comp.replacement) {
335
- const matches = comp.replacement.matchAll(/transforms\.(\w+)/g);
336
- for (const match of matches) usedTransformNames.add(match[1]);
337
- }
338
- }
339
- storedTransformsSource = "{}";
340
- }
341
- bridgeInjected = false;
342
- const newFragments = storedManifest?.component_fragments;
474
+ this.storedManifest = result.manifest;
475
+ this.storedManifestJson = result.manifestJson;
476
+ this.storedSystemPropMapJson = JSON.stringify(result.manifest?.system_prop_map ?? {});
477
+ this.storedDynamicPropsJson = JSON.stringify(result.manifest?.dynamic_props ?? {});
478
+ this.bridgeInjected = false;
479
+ const newFragments = result.manifest?.component_fragments;
343
480
  if (newFragments && typeof newFragments === "object") {
344
- fragmentCache.clear();
345
- for (const [id, sheets] of Object.entries(newFragments)) fragmentCache.set(id, sheets);
481
+ this.fragmentCache.clear();
482
+ for (const [id, sheets] of Object.entries(newFragments)) this.fragmentCache.set(id, sheets);
346
483
  }
347
- reverseProvenance = storedManifest?.reverse_provenance ?? {};
348
- storedSheets = storedManifest?.sheets ?? null;
349
- globalCss = storedManifest?.sheets?.global || "";
350
- resolvedComponentCss = storedManifest?.css || "";
351
- resolvedComponentCss = (0, _animus_ui_extract_pipeline.applyUnitFallback)(resolvedComponentCss);
484
+ this.reverseProvenance = result.manifest?.reverse_provenance ?? {};
485
+ this.storedSheets = result.manifest?.sheets ?? null;
486
+ this.globalCss = result.globalCss;
487
+ this.resolvedComponentCss = result.componentCss;
352
488
  } catch (e) {
353
- if (options.strict) throw new Error(`[animus-extract] analyzeProject failed: ${e}`, { cause: e });
489
+ if (this.options.strict) throw new Error(`[animus-extract] analyzeProject failed: ${e}`, { cause: e });
354
490
  console.warn("[animus-extract] analyzeProject failed:", e);
355
491
  }
356
492
  }
357
- function runSelfVerify() {
493
+ resetCoalescer = null;
494
+ /**
495
+ * Schedule a geological reset through the burst coalescer. N dependency
496
+ * events within the quiescence window produce one reset; events during a
497
+ * running reset produce exactly one follow-up.
498
+ */
499
+ requestGeologicalReset(trigger) {
500
+ this.log(`HMR geological reset scheduled: ${trigger}`);
501
+ this.resetCoalescer ??= new ResetCoalescer(() => this.performGeologicalReset());
502
+ this.resetCoalescer.request();
503
+ }
504
+ /**
505
+ * The geological reset: reload the system (refreshing the dependency
506
+ * set), clear the Rust per-file cache, re-analyze everything with full
507
+ * sources, then invalidate the static/component/system-prop virtual
508
+ * modules and reload the client.
509
+ */
510
+ performGeologicalReset() {
511
+ const resetStart = performance.now();
512
+ this.loadSystem();
513
+ (0, _animus_ui_extract_pipeline.clearEngineCache)(this.engineApi);
514
+ const fileEntries = [];
515
+ for (const [path$2, { hash, source }] of this.fileCache) fileEntries.push({
516
+ path: path$2,
517
+ source,
518
+ hash
519
+ });
520
+ this.runAnalysis(fileEntries);
521
+ this.log(`HMR geological reset complete: ${Math.round(performance.now() - resetStart)}ms`);
522
+ const server = this.devServer;
523
+ if (!server) return;
524
+ for (const moduleId of [
525
+ RESOLVED_CSS_ID,
526
+ RESOLVED_COMPONENTS_ID,
527
+ RESOLVED_SYSTEM_PROPS_ID
528
+ ]) {
529
+ const mod = server.moduleGraph.getModuleById(moduleId);
530
+ if (mod) server.moduleGraph.invalidateModule(mod);
531
+ }
532
+ server.hot?.send({ type: "full-reload" });
533
+ }
534
+ /**
535
+ * True when the file is a member of the system's evaluated module-file
536
+ * set. Tested BEFORE component-scan exclude filters: a system dependency
537
+ * edit invalidates the compiler registry regardless of what the user
538
+ * excluded from component scanning.
539
+ */
540
+ isSystemDependency(absFile) {
541
+ return (0, _animus_ui_extract_pipeline.toWatchKeys)(absFile).some((key) => this.systemDependencyKeys.has(key));
542
+ }
543
+ /**
544
+ * Ask the dev watcher to watch every loader-reported dependency path.
545
+ * Covers system modules unreachable from app imports (workspace package
546
+ * config files). Vite's watcher hard-ignores `node_modules`, so
547
+ * node_modules-installed system dependencies produce no events — a
548
+ * documented limitation; workspace deps resolve to real paths outside
549
+ * node_modules and are watchable. No-ops without a dev server.
550
+ */
551
+ registerSystemWatchPaths() {
552
+ const watcher = this.devServer?.watcher;
553
+ if (!watcher || this.systemDependencyPaths.length === 0) return;
554
+ watcher.add(this.systemDependencyPaths);
555
+ }
556
+ /**
557
+ * Invalidate the component CSS and system-props virtual modules, then
558
+ * reload the client. Shared by the two out-of-band re-analysis paths — a
559
+ * file created after buildStart (transform) and a file deleted during dev
560
+ * (the `hotUpdate` delete event) — which both mutate the cache, re-run
561
+ * analysis, and then need the client to pick up the regenerated CSS.
562
+ * No-ops outside dev.
563
+ *
564
+ * `devServer.moduleGraph` is Vite's back-compat mixed graph: its
565
+ * `getModuleById` searches the client AND ssr environment graphs and its
566
+ * `invalidateModule` invalidates both instances behind the returned node,
567
+ * which is exactly the reach this path wants. It stays the seam here.
568
+ */
569
+ invalidateExtractedModules() {
570
+ const server = this.devServer;
571
+ if (!server) return;
572
+ for (const moduleId of [RESOLVED_COMPONENTS_ID, RESOLVED_SYSTEM_PROPS_ID]) {
573
+ const mod = server.moduleGraph.getModuleById(moduleId);
574
+ if (mod) server.moduleGraph.invalidateModule(mod);
575
+ }
576
+ setTimeout(() => {
577
+ this.devServer?.hot?.send({ type: "full-reload" });
578
+ }, 100);
579
+ }
580
+ runSelfVerify() {
358
581
  const failures = [];
359
- if (resolvedComponentCss.trim().length === 0) failures.push("No component CSS produced — check system file and include patterns");
360
- if (!variableCss.includes(":root")) failures.push("No :root variable block found in variable CSS");
361
- if (`${variableCss}\n${globalCss}\n${resolvedComponentCss}`.includes("__TRANSFORM__")) failures.push("Unresolved __TRANSFORM__ placeholders found in CSS output");
362
- if (storedManifest && resolvedComponentCss.length > 0) {
582
+ if (Object.keys(this.storedManifest?.components ?? {}).length === 0) failures.push("No component CSS produced — check the system file and its includes list");
583
+ for (const { specifier, outcome } of this.externalPackageOutcomes) if (outcome === "empty") failures.push(`include '${specifier}' resolved but discovered no component sources`);
584
+ if (!this.system.variableCss.includes(":root")) failures.push("No :root variable block found in variable CSS");
585
+ if (`${this.system.variableCss}\n${this.globalCss}\n${this.resolvedComponentCss}`.includes("__TRANSFORM__")) failures.push("Unresolved __TRANSFORM__ placeholders found in CSS output");
586
+ if (this.storedManifest && this.resolvedComponentCss.length > 0) {
363
587
  const assembled = (0, _animus_ui_extract_pipeline.assembleStylesheet)({
364
- layers: options.layers,
365
- variableCss,
366
- globalCss,
367
- componentCss: resolvedComponentCss
588
+ layers: this.options.layers,
589
+ variableCss: this.system.variableCss,
590
+ globalCss: this.globalCss,
591
+ componentCss: this.resolvedComponentCss
368
592
  });
369
593
  const baseIdx = assembled.search(/@layer\s+anm-base\s*\{/);
370
594
  const variantsIdx = assembled.search(/@layer\s+anm-variants\s*\{/);
@@ -372,265 +596,323 @@ function animusExtract(options) {
372
596
  }
373
597
  for (const message of failures) {
374
598
  const line = `[animus:verify] ${message}`;
375
- if (options.strict) throw new Error(line);
376
- if (logger) logger.warn(line, { timestamp: true });
599
+ if (this.options.strict) throw new Error(line);
600
+ if (this.logger) this.logger.warn(line, { timestamp: true });
377
601
  else console.warn(line);
378
602
  }
379
- if (failures.length === 0) log("[animus:verify] structural self-check passed");
603
+ if (failures.length === 0) this.log("[animus:verify] structural self-check passed");
380
604
  }
381
- return {
382
- name: "animus-extract",
383
- enforce: "pre",
384
- configureServer(server) {
385
- devServer = server;
386
- },
387
- configResolved(config) {
388
- isProd = config.command === "build";
389
- rootDir = config.root;
390
- logger = config.logger;
391
- lcssTargets = resolveLightningTargets(options.targets, rootDir);
392
- log(`Lightning CSS targets resolved (${Object.keys(lcssTargets).length} browsers)`);
393
- const rawAlias = config.resolve?.alias;
394
- if (rawAlias) {
395
- const entries = [];
396
- if (Array.isArray(rawAlias)) {
397
- for (const entry of rawAlias) if (typeof entry.find === "string" && typeof entry.replacement === "string") {
398
- const replacement = entry.replacement.startsWith(rootDir) ? entry.replacement.slice(rootDir.length + 1) : entry.replacement;
399
- entries.push({
400
- pattern: entry.find.endsWith("/") ? entry.find : entry.find + "/",
401
- replacement: replacement.endsWith("/") ? replacement : replacement + "/",
402
- type: "prefix"
403
- });
404
- }
405
- } else if (typeof rawAlias === "object" && rawAlias !== null) {
406
- for (const [key, value] of Object.entries(rawAlias)) if (typeof value === "string") {
407
- const replacement = value.startsWith(rootDir) ? value.slice(rootDir.length + 1) : value;
408
- if (/\.\w+$/.test(replacement)) entries.push({
409
- pattern: key,
410
- replacement,
411
- type: "exact"
412
- });
413
- else entries.push({
414
- pattern: key.endsWith("/") ? key : key + "/",
415
- replacement: replacement.endsWith("/") ? replacement : replacement + "/",
416
- type: "prefix"
417
- });
418
- }
419
- }
420
- entries.sort((a, b) => b.pattern.length - a.pattern.length);
421
- if (entries.length > 0) {
422
- pathAliasesJson = JSON.stringify({ aliases: entries });
423
- log(`Path aliases forwarded: ${entries.length} entries`);
424
- }
425
- }
426
- },
427
- async buildStart() {
428
- try {
429
- const { clearAnalysisCache } = engineApi();
430
- clearAnalysisCache();
431
- } catch {}
432
- let t0 = performance.now();
433
- loadSystem();
434
- if (options.layers) {
435
- (0, _animus_ui_extract_pipeline.validateLayerOrder)(options.layers);
436
- log(`Custom layers: [${options.layers.join(", ")}]`);
437
- }
438
- if (verbose) {
439
- const propCount = Object.keys(JSON.parse(configJson)).length;
440
- const groupCount = Object.keys(JSON.parse(groupRegistryJson)).length;
441
- log(`System loaded: ${propCount} props, ${groupCount} groups (${Math.round(performance.now() - t0)}ms)`);
442
- }
443
- t0 = performance.now();
444
- const excludePatterns = options.exclude ?? DEFAULT_EXCLUDE;
445
- extensionsSet = new Set(options.extensions ?? _animus_ui_extract_pipeline.DEFAULT_EXTENSIONS);
446
- const shouldHandleMdx = extensionsSet.has(".mdx");
447
- let mdxMissingDepWarned = false;
448
- const filePaths = discoverFiles(rootDir, rootDir, excludePatterns, extensionsSet);
449
- const fileEntries = [];
450
- for (const filePath of filePaths) try {
451
- let source = (0, fs.readFileSync)(filePath, "utf-8");
452
- let relPath = (0, path.relative)(rootDir, filePath);
453
- if (shouldHandleMdx && (0, path.extname)(filePath) === ".mdx") {
454
- const result = await (0, _animus_ui_extract_pipeline.preprocessMdx)(source, relPath);
455
- if (result.kind === "missing-dep") {
456
- if (!mdxMissingDepWarned) {
457
- warn("⚠ .mdx in extensions but @mdx-js/mdx not installed; MDX files skipped");
458
- mdxMissingDepWarned = true;
459
- }
460
- continue;
461
- }
462
- if (result.kind === "error") {
463
- warn(`⚠ MDX preprocessing failed for ${relPath}: ${result.error}`);
464
- continue;
465
- }
466
- source = result.source;
467
- relPath = relPath + ".tsx";
468
- }
469
- const hash = !isProd ? contentHash(source) : void 0;
470
- fileEntries.push({
471
- path: relPath,
472
- source,
473
- hash
474
- });
475
- if (!isProd && hash) fileCache.set(relPath, {
476
- hash,
477
- source
478
- });
479
- } catch {}
480
- const localFileCount = fileEntries.length;
481
- const packageSpecifiers = (0, _animus_ui_extract_pipeline.extractSystemFilePackages)(resolvedSystemPath);
482
- packageMap = {};
483
- externalSourceEntries.clear();
484
- const collectedPkgDirs = [];
485
- for (const specifier of packageSpecifiers) try {
486
- const resolved = await this.resolve(specifier);
487
- if (resolved && resolved.id) {
488
- const absPath = resolved.id;
489
- let pkgRoot = (0, path.dirname)(absPath);
490
- while (pkgRoot !== (0, path.dirname)(pkgRoot) && !(0, fs.existsSync)((0, path.join)(pkgRoot, "package.json"))) pkgRoot = (0, path.dirname)(pkgRoot);
491
- const srcDir = (0, path.join)(pkgRoot, "src");
492
- if ((0, fs.existsSync)(srcDir)) {
493
- collectedPkgDirs.push(srcDir);
494
- const pkgFiles = discoverFiles(srcDir, rootDir, excludePatterns, extensionsSet);
495
- for (const pkgFile of pkgFiles) {
496
- let pkgRelPath = (0, path.relative)(rootDir, pkgFile);
497
- if (!fileEntries.some((e) => e.path === pkgRelPath)) {
498
- let pkgSource = (0, fs.readFileSync)(pkgFile, "utf-8");
499
- if (shouldHandleMdx && (0, path.extname)(pkgFile) === ".mdx") {
500
- const result = await (0, _animus_ui_extract_pipeline.preprocessMdx)(pkgSource, pkgRelPath);
501
- if (result.kind === "missing-dep") {
502
- if (!mdxMissingDepWarned) {
503
- warn("⚠ .mdx in extensions but @mdx-js/mdx not installed; MDX files skipped");
504
- mdxMissingDepWarned = true;
505
- }
506
- continue;
507
- }
508
- if (result.kind === "error") {
509
- warn(`⚠ MDX preprocessing failed for ${pkgRelPath}: ${result.error}`);
510
- continue;
511
- }
512
- pkgSource = result.source;
513
- pkgRelPath = pkgRelPath + ".tsx";
514
- }
515
- const pkgHash = !isProd ? contentHash(pkgSource) : void 0;
516
- fileEntries.push({
517
- path: pkgRelPath,
518
- source: pkgSource,
519
- hash: pkgHash
520
- });
521
- if (!isProd && pkgHash) fileCache.set(pkgRelPath, {
522
- hash: pkgHash,
523
- source: pkgSource
524
- });
525
- }
526
- }
527
- const srcEntry = (0, path.join)(srcDir, "index.ts");
528
- if ((0, fs.existsSync)(srcEntry)) {
529
- packageMap[specifier] = (0, path.relative)(rootDir, srcEntry);
530
- externalSourceEntries.set(specifier, srcEntry);
531
- } else packageMap[specifier] = (0, path.relative)(rootDir, absPath);
532
- } else {
533
- const relPath = (0, path.relative)(rootDir, absPath);
534
- if (!fileEntries.some((e) => e.path === relPath)) {
535
- const entrySource = (0, fs.readFileSync)(absPath, "utf-8");
536
- const entryHash = !isProd ? contentHash(entrySource) : void 0;
537
- fileEntries.push({
538
- path: relPath,
539
- source: entrySource,
540
- hash: entryHash
541
- });
542
- if (!isProd && entryHash) fileCache.set(relPath, {
543
- hash: entryHash,
544
- source: entrySource
545
- });
546
- }
547
- collectedPkgDirs.push((0, path.dirname)(absPath));
548
- packageMap[specifier] = relPath;
549
- }
550
- }
551
- } catch {}
552
- externalPackageDirs = collectedPkgDirs;
553
- const packageFileCount = fileEntries.length - localFileCount;
554
- log(`Discovered ${fileEntries.length} files (${packageFileCount} from packages) (${Math.round(performance.now() - t0)}ms)`);
555
- t0 = performance.now();
556
- runAnalysis(fileEntries);
557
- if (storedManifest) {
558
- const report = storedManifest.report;
559
- if (report) {
560
- log(`Extracted ${report.components_extracted}/${report.components_total} components (${Math.round(performance.now() - t0)}ms)`);
561
- logTimingWaterfall(storedManifest.timing ?? {});
562
- log(`Reconciliation: ${report.components_extracted} kept, ${report.variants_eliminated} variants pruned, ${report.states_eliminated} states pruned`);
563
- const details = report.eliminated_details || [];
564
- for (const d of details) if (d.kind === "component") warn(`⚠ ${d.component} eliminated: ${d.reason}`);
565
- else if (d.kind === "prospective_component") warn(`⚠ ${d.component} would be eliminated in production: ${d.reason}`);
566
- else if (d.kind === "variant") warn(`⚠ ${d.component} variant '${d.name}' pruned: ${d.reason}`);
567
- else if (d.kind === "state") warn(`⚠ ${d.component} state '${d.name}' pruned: ${d.reason}`);
568
- }
569
- log(`CSS: ${resolvedComponentCss.length} bytes (${Object.keys(storedManifest.components || {}).length} components)`);
570
- if (!isProd && storedSheets) {
571
- const staticSize = (0, _animus_ui_extract_pipeline.assembleStylesheet)({
572
- layers: options.layers,
573
- variableCss,
574
- globalCss
575
- }).length;
576
- const componentSize = resolvedComponentCss.length;
577
- log(`Delivery: split mode — static ${staticSize} bytes, components ${componentSize} bytes (adopted stylesheet)`);
578
- } else log("Delivery: single file mode (production)");
605
+ };
606
+ //#endregion
607
+ //#region src/hmr.ts
608
+ /**
609
+ * hotUpdate: the single dev file-event hook. Handles system-dependency
610
+ * membership (geological reset), content-hash diffing with incremental
611
+ * re-analysis, deleted-file cache pruning, and targeted module invalidation
612
+ * (component CSS, system props, and definition files whose replacement
613
+ * changed).
614
+ *
615
+ * Vite 8 dispatches this hook once per environment for ONE file event — the
616
+ * client environment first, then every non-client environment (see
617
+ * `handleHMRUpdate` in vite/dist/node/chunks/node.js). The analysis half is
618
+ * therefore claimed by exactly one dispatch (`ctx.hotUpdateEvents`), while the
619
+ * invalidation half runs in every environment against its own module graph —
620
+ * what the mixed-graph `handleHotUpdate` used to achieve implicitly by
621
+ * invalidating the client and SSR instance behind one module node.
622
+ *
623
+ * The hook fires for every watched file whether or not it has modules in any
624
+ * graph, so system dependencies registered through `watcher.add` outside the
625
+ * root still reach the reset branch, with an empty `modules` list.
626
+ */
627
+ async function handleHotUpdate(ctx, environment, { type, file, timestamp, modules }) {
628
+ if (ctx.isProd) return;
629
+ const ownsEvent = ctx.hotUpdateEvents.claim(environment.name, file, timestamp);
630
+ const absFile = (0, path.resolve)(file);
631
+ ctx.log(`hotUpdate ${type} ${(0, path.relative)(ctx.rootDir, absFile)} env=${environment.name} owns=${ownsEvent}`);
632
+ if (ctx.isSystemDependency(absFile)) {
633
+ if (ownsEvent) ctx.requestGeologicalReset((0, path.relative)(ctx.rootDir, absFile));
634
+ return [];
635
+ }
636
+ if (type === "create") return;
637
+ if (type === "delete") {
638
+ if (ownsEvent) pruneDeletedFile(ctx, absFile);
639
+ return;
640
+ }
641
+ if (ownsEvent) ctx.hotUpdateEvents.record(file, timestamp, await analyzeChangedFile(ctx, file, absFile));
642
+ const result = ctx.hotUpdateEvents.resultOf(file, timestamp);
643
+ if (result.kind === "ignored") return;
644
+ if (result.kind === "unchanged") return [];
645
+ return invalidateStaleModules(ctx, environment, modules, result.staleDefinitionFiles);
646
+ }
647
+ /**
648
+ * The once-per-event analysis half: gate the file, diff its content hash,
649
+ * refresh the cache, and re-run project analysis. The returned result is what
650
+ * the remaining environments act on.
651
+ */
652
+ async function analyzeChangedFile(ctx, file, absFile) {
653
+ const ext = (0, path.extname)(file);
654
+ if (!ctx.extensionsSet.has(ext)) return { kind: "ignored" };
655
+ const excludePatterns = ctx.options.exclude ?? DEFAULT_EXCLUDE;
656
+ if (!ctx.externalPackageDirs.some((dir) => file.startsWith(dir + path.sep) || file === dir) && excludePatterns.some((pattern) => file.includes(pattern) || (0, path.relative)(ctx.rootDir, file).includes(pattern))) return { kind: "ignored" };
657
+ const relPath = (0, path.relative)(ctx.rootDir, absFile);
658
+ let source;
659
+ try {
660
+ source = (0, fs.readFileSync)(absFile, "utf-8");
661
+ } catch {
662
+ return { kind: "ignored" };
663
+ }
664
+ let scannerRelPath = relPath;
665
+ if (ext === ".mdx") {
666
+ const result = await (0, _animus_ui_extract_pipeline.preprocessMdx)(source, relPath);
667
+ if (result.kind === "missing-dep") {
668
+ ctx.warn("⚠ .mdx HMR skipped: @mdx-js/mdx not installed; restart dev server after installing");
669
+ return { kind: "ignored" };
670
+ }
671
+ if (result.kind === "error") {
672
+ ctx.warn(`⚠ MDX preprocessing failed for ${relPath}: ${result.error}`);
673
+ return { kind: "ignored" };
674
+ }
675
+ source = result.source;
676
+ scannerRelPath = relPath + ".tsx";
677
+ }
678
+ const hash = (0, _animus_ui_extract_pipeline.contentHash)(source);
679
+ const cached = ctx.fileCache.get(scannerRelPath);
680
+ if (cached && cached.hash === hash) {
681
+ ctx.log(`HMR skip: ${scannerRelPath} (unchanged)`);
682
+ return { kind: "unchanged" };
683
+ }
684
+ ctx.fileCache.set(scannerRelPath, {
685
+ hash,
686
+ source
687
+ });
688
+ const hmrStart = performance.now();
689
+ const prevReplacements = /* @__PURE__ */ new Map();
690
+ if (ctx.storedManifest?.components) for (const [id, desc] of Object.entries(ctx.storedManifest.components)) prevReplacements.set(id, desc.replacement ?? "");
691
+ const directComponentIds = ctx.storedManifest?.files?.[scannerRelPath] ?? [];
692
+ const invalidatedIds = new Set(directComponentIds);
693
+ const queue = [...directComponentIds];
694
+ while (queue.length > 0) {
695
+ const parentId = queue.shift();
696
+ const children = ctx.reverseProvenance[parentId];
697
+ if (children) {
698
+ for (const childId of children) if (!invalidatedIds.has(childId)) {
699
+ invalidatedIds.add(childId);
700
+ queue.push(childId);
579
701
  }
580
- const { declaration } = (0, _animus_ui_extract_pipeline.assembleStylesheet)({
581
- layers: options.layers,
582
- variableCss: "",
583
- globalCss: "",
702
+ }
703
+ }
704
+ if (ctx.verbose && invalidatedIds.size > directComponentIds.length) ctx.log(`HMR: ${directComponentIds.length} direct + ${invalidatedIds.size - directComponentIds.length} transitive components invalidated`);
705
+ const analysisStart = performance.now();
706
+ const fileEntries = buildFileEntriesFromCache(ctx.fileCache, relPath);
707
+ ctx.runAnalysis(fileEntries);
708
+ const analysisMs = Math.round(performance.now() - analysisStart);
709
+ const staleDefinitionFiles = [];
710
+ if (ctx.storedManifest?.components) {
711
+ const staleFiles = /* @__PURE__ */ new Set();
712
+ for (const [id, desc] of Object.entries(ctx.storedManifest.components)) if ((desc.replacement ?? "") !== (prevReplacements.get(id) ?? "")) staleFiles.add(desc.file);
713
+ for (const defFile of staleFiles) {
714
+ if ((0, path.resolve)(ctx.rootDir, defFile) === absFile) continue;
715
+ staleDefinitionFiles.push(defFile);
716
+ }
717
+ }
718
+ const hmrMs = Math.round(performance.now() - hmrStart);
719
+ ctx.log(`HMR update: ${relPath} — analysis ${analysisMs}ms, total ${hmrMs}ms`);
720
+ ctx.logTimingWaterfall(ctx.storedManifest?.timing ?? {});
721
+ return {
722
+ kind: "analyzed",
723
+ staleDefinitionFiles
724
+ };
725
+ }
726
+ /**
727
+ * Reconcile a deleted file in dev.
728
+ *
729
+ * Without this the removed file's last-known source stays in `ctx.fileCache`,
730
+ * and `buildFileEntriesFromCache` re-feeds that ghost entry to the engine on
731
+ * every later re-analysis — the deleted component's CSS survives for the life
732
+ * of the process.
733
+ */
734
+ function pruneDeletedFile(ctx, absFile) {
735
+ if (!pruneFileCache(ctx.fileCache, ctx.rootDir, absFile)) return;
736
+ ctx.runAnalysis(buildFileEntriesFromCache(ctx.fileCache));
737
+ ctx.log(`Deleted file pruned: ${(0, path.relative)(ctx.rootDir, absFile)}`);
738
+ ctx.invalidateExtractedModules();
739
+ }
740
+ /**
741
+ * The per-environment invalidation half: invalidate the modules this
742
+ * environment serves and widen its update set with them. Static CSS
743
+ * (virtual:animus/styles.css) is NOT invalidated here — it only changes on a
744
+ * geological reset (vars/globals are stable during dev).
745
+ */
746
+ function invalidateStaleModules(ctx, environment, modules, staleDefinitionFiles) {
747
+ const graph = environment.moduleGraph;
748
+ const modulesToUpdate = [...modules];
749
+ for (const moduleId of [RESOLVED_COMPONENTS_ID, RESOLVED_SYSTEM_PROPS_ID]) {
750
+ const mod = graph.getModuleById(moduleId);
751
+ if (mod) {
752
+ graph.invalidateModule(mod);
753
+ modulesToUpdate.push(mod);
754
+ }
755
+ }
756
+ for (const defFile of staleDefinitionFiles) {
757
+ const absDefPath = (0, path.resolve)(ctx.rootDir, defFile);
758
+ const defModule = graph.getModuleById(absDefPath) ?? graph.getModulesByFile(absDefPath)?.values().next().value;
759
+ if (defModule) {
760
+ ctx.log(`HMR invalidate: ${defFile} (replacement changed)`);
761
+ graph.invalidateModule(defModule);
762
+ modulesToUpdate.push(defModule);
763
+ }
764
+ }
765
+ const invalidated = modulesToUpdate.length - modules.length;
766
+ if (invalidated > 0) {
767
+ ctx.log(`HMR (${environment.name}): ${invalidated} modules invalidated`);
768
+ return modulesToUpdate;
769
+ }
770
+ }
771
+ //#endregion
772
+ //#region src/index-html.ts
773
+ /**
774
+ * transformIndexHtml: build the head-prepend tag set.
775
+ *
776
+ * Two independent tags, both delivery-only:
777
+ * - the appearance bootstrap script, when the host build passed a
778
+ * pre-generated artifact via `appearanceBootstrap` (absent otherwise);
779
+ * - the `@layer` declaration style, when `buildStart` computed one.
780
+ *
781
+ * Both branches are guarded on non-empty CONTENT, not on mere presence: an
782
+ * artifact whose `code` is empty is a caller defect, and emitting
783
+ * `<script data-animus-bootstrap><\/script>` for it would break the
784
+ * unconfigured-parity contract in spirit (a tag that does nothing) while
785
+ * hiding the defect. Symmetric with `layerDeclaration`.
786
+ *
787
+ * Ordering: every descriptor rides the same `head-prepend` bucket, which Vite
788
+ * serializes in array order (`serializeTags`) and splices in immediately after
789
+ * `<head>`. The bootstrap script is therefore FIRST in the array so it runs
790
+ * before the layer declaration and — since built stylesheet links are appended
791
+ * at `</head>` — before any stylesheet reference.
792
+ *
793
+ * When neither input is set the array is empty, so an unconfigured build emits
794
+ * no tag, attribute, or whitespace of its own.
795
+ */
796
+ function buildIndexHtmlTags(ctx) {
797
+ const tags = [];
798
+ const bootstrap = ctx.options.appearanceBootstrap;
799
+ if (bootstrap?.code) tags.push({
800
+ tag: "script",
801
+ attrs: { "data-animus-bootstrap": "" },
802
+ children: bootstrap.code,
803
+ injectTo: "head-prepend"
804
+ });
805
+ if (ctx.layerDeclaration) tags.push({
806
+ tag: "style",
807
+ attrs: { "data-animus-layers": "" },
808
+ children: ctx.layerDeclaration,
809
+ injectTo: "head-prepend"
810
+ });
811
+ return tags;
812
+ }
813
+ //#endregion
814
+ //#region src/transform.ts
815
+ /**
816
+ * transform: replace builder chains with `createComponent()` calls using
817
+ * the pre-built manifest; inject the HMR bridge import once in dev; detect
818
+ * files created after buildStart and fold them into the analysis.
819
+ */
820
+ function transformSource(ctx, code, id) {
821
+ if (!ctx.storedManifest) return null;
822
+ if (!ctx.externalPackageDirs.some((dir) => id.startsWith(dir + path.sep) || id === dir)) {
823
+ if (!/\.[jt]sx?$/.test(id)) return null;
824
+ if (id.includes("node_modules")) return null;
825
+ }
826
+ const relativePath = (0, path.relative)(ctx.rootDir, id);
827
+ if (!ctx.storedManifest.files?.[relativePath]?.length) {
828
+ if (!ctx.isProd && !ctx.fileCache.has(relativePath)) {
829
+ const hash = (0, _animus_ui_extract_pipeline.contentHash)(code);
830
+ ctx.fileCache.set(relativePath, {
831
+ hash,
832
+ source: code
833
+ });
834
+ const fileEntries = buildFileEntriesFromCache(ctx.fileCache, relativePath);
835
+ ctx.runAnalysis(fileEntries);
836
+ const compCount = ctx.storedManifest.files?.[relativePath]?.length ?? 0;
837
+ ctx.log(`New file detected: ${relativePath} — ${compCount ? `${compCount} components extracted` : "no components"}`);
838
+ if (compCount) ctx.invalidateExtractedModules();
839
+ }
840
+ if (!ctx.storedManifest.files?.[relativePath]?.length) return null;
841
+ }
842
+ try {
843
+ const { transformFile } = ctx.engineApi();
844
+ const result = transformFile(code, relativePath, ctx.storedManifestJson);
845
+ if (!result.hasComponents) return null;
846
+ let transformedCode = result.code;
847
+ if (!ctx.isProd && !ctx.bridgeInjected && ctx.storedSheets) {
848
+ transformedCode = `import '${VIRTUAL_BRIDGE_ID}';\n${transformedCode}`;
849
+ ctx.bridgeInjected = true;
850
+ ctx.log("HMR bridge injected via transform");
851
+ }
852
+ if (ctx.verbose) {
853
+ const compCount = ctx.storedManifest.files?.[relativePath]?.length ?? 0;
854
+ ctx.log(`transform ${relativePath}: ${compCount} components`);
855
+ }
856
+ return {
857
+ code: transformedCode,
858
+ map: null
859
+ };
860
+ } catch (e) {
861
+ if (ctx.options.strict) throw new Error(`[animus-extract] Failed to transform ${id}: ${e}`, { cause: e });
862
+ console.warn(`[animus-extract] Failed to transform ${id}:`, e);
863
+ return null;
864
+ }
865
+ }
866
+ //#endregion
867
+ //#region src/virtual-modules.ts
868
+ /** resolveId: map virtual ids and redirect external DS package imports. */
869
+ function resolveVirtualId(ctx, id) {
870
+ if (id === "virtual:animus/styles.css") return RESOLVED_CSS_ID;
871
+ if (id === "virtual:animus/components.js") return RESOLVED_COMPONENTS_ID;
872
+ if (id === "virtual:animus/hmr-bridge.js") return RESOLVED_BRIDGE_ID;
873
+ if (id === "virtual:animus/system-props") return RESOLVED_SYSTEM_PROPS_ID;
874
+ const srcEntry = ctx.externalSourceEntries.get(id);
875
+ if (srcEntry) return srcEntry;
876
+ return null;
877
+ }
878
+ /** load: serve the virtual stylesheet, components CSS, HMR bridge, and
879
+ * system-props modules from manifest-derived state. */
880
+ function loadVirtualModule(ctx, id) {
881
+ const lcssOpts = {
882
+ minify: ctx.options.minify ?? ctx.isProd,
883
+ targets: ctx.lcssTargets,
884
+ warnFn: (msg) => ctx.warn(msg)
885
+ };
886
+ if (id === "\0virtual:animus/styles.css") {
887
+ if (!ctx.isProd && ctx.storedSheets) {
888
+ const { variables, body } = (0, _animus_ui_extract_pipeline.assembleStylesheet)({
889
+ layers: ctx.options.layers,
890
+ variableCss: ctx.system.variableCss,
891
+ globalCss: ctx.globalCss,
584
892
  split: true
585
893
  });
586
- layerDeclaration = declaration;
587
- if (options.verify) runSelfVerify();
588
- },
589
- resolveId(id) {
590
- if (id === VIRTUAL_CSS_ID) return RESOLVED_CSS_ID;
591
- if (id === VIRTUAL_COMPONENTS_ID) return RESOLVED_COMPONENTS_ID;
592
- if (id === VIRTUAL_BRIDGE_ID) return RESOLVED_BRIDGE_ID;
593
- if (id === VIRTUAL_SYSTEM_PROPS_ID) return RESOLVED_SYSTEM_PROPS_ID;
594
- const srcEntry = externalSourceEntries.get(id);
595
- if (srcEntry) return srcEntry;
596
- return null;
597
- },
598
- load(id) {
599
- const lcssOpts = {
600
- minify: options.minify ?? isProd,
601
- targets: lcssTargets,
602
- warnFn: warn
603
- };
604
- if (id === RESOLVED_CSS_ID) {
605
- if (!isProd && storedSheets) {
606
- const { variables, body } = (0, _animus_ui_extract_pipeline.assembleStylesheet)({
607
- layers: options.layers,
608
- variableCss,
609
- globalCss,
610
- split: true
611
- });
612
- return [variables, postProcessCss(body, {
613
- ...lcssOpts,
614
- minify: false
615
- })].filter(Boolean).join("\n");
616
- }
617
- const { variables, body } = (0, _animus_ui_extract_pipeline.assembleStylesheet)({
618
- layers: options.layers,
619
- variableCss,
620
- globalCss,
621
- componentCss: resolvedComponentCss,
622
- split: true
623
- });
624
- return [variables, postProcessCss(body, lcssOpts)].filter(Boolean).join("\n");
625
- }
626
- if (id === RESOLVED_COMPONENTS_ID) return `export default \`${postProcessCss((0, _animus_ui_extract_pipeline.stripLeadingLayerDeclaration)(resolvedComponentCss || ""), {
894
+ return [variables, (0, _animus_ui_extract_pipeline.postProcessCss)(body, {
627
895
  ...lcssOpts,
628
896
  minify: false
629
- }).replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$")}\`;`;
630
- if (id === RESOLVED_BRIDGE_ID) return `
897
+ })].filter(Boolean).join("\n");
898
+ }
899
+ const { variables, body } = (0, _animus_ui_extract_pipeline.assembleStylesheet)({
900
+ layers: ctx.options.layers,
901
+ variableCss: ctx.system.variableCss,
902
+ globalCss: ctx.globalCss,
903
+ componentCss: ctx.resolvedComponentCss,
904
+ split: true
905
+ });
906
+ return [variables, (0, _animus_ui_extract_pipeline.postProcessCss)(body, lcssOpts)].filter(Boolean).join("\n");
907
+ }
908
+ if (id === "\0virtual:animus/components.js") return `export default \`${(0, _animus_ui_extract_pipeline.postProcessCss)((0, _animus_ui_extract_pipeline.stripLeadingLayerDeclaration)(ctx.resolvedComponentCss || ""), {
909
+ ...lcssOpts,
910
+ minify: false
911
+ }).replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$")}\`;`;
912
+ if (id === "\0virtual:animus/hmr-bridge.js") return `
631
913
  import css from '${VIRTUAL_COMPONENTS_ID}';
632
914
 
633
- const GLOBAL_KEY = '__animus_sheet_${(0, crypto.createHash)("md5").update(options.system).digest("hex").slice(0, 8)}__';
915
+ const GLOBAL_KEY = '__animus_sheet_${(0, crypto.createHash)("md5").update(ctx.options.system).digest("hex").slice(0, 8)}__';
634
916
  let sheet = globalThis[GLOBAL_KEY] || null;
635
917
 
636
918
  if (typeof CSSStyleSheet !== 'undefined' && 'adoptedStyleSheets' in document) {
@@ -662,211 +944,65 @@ if (import.meta.hot) {
662
944
  });
663
945
  }
664
946
  `;
665
- if (id === RESOLVED_SYSTEM_PROPS_ID) {
666
- let moduleSource = `export const systemPropMap = ${storedSystemPropMapJson};\nexport const systemPropGroups = ${groupRegistryJson};`;
667
- if (storedDynamicPropsJson !== "{}") {
668
- const dynamicProps = JSON.parse(storedDynamicPropsJson);
669
- const configEntries = {};
670
- for (const [propName, meta] of Object.entries(dynamicProps)) configEntries[propName] = {
671
- varName: meta.var_name,
672
- slotClass: meta.slot_class,
673
- ...meta.transform_name ? { transformName: meta.transform_name } : {},
674
- ...meta.scale_values && Object.keys(meta.scale_values).length > 0 ? { scaleValues: meta.scale_values } : {}
675
- };
676
- moduleSource += `\nexport const dynamicPropConfig = ${JSON.stringify(configEntries)};`;
677
- } else moduleSource += `\nexport const dynamicPropConfig = {};`;
678
- moduleSource += `\nexport const transforms = ${storedTransformsSource};`;
679
- return moduleSource;
680
- }
681
- return null;
947
+ if (id === "\0virtual:animus/system-props") return (0, _animus_ui_extract_pipeline.buildSystemPropsModule)({
948
+ systemPropMapJson: ctx.storedSystemPropMapJson,
949
+ groupRegistryJson: ctx.system.groupRegistryJson,
950
+ dynamicProps: JSON.parse(ctx.storedDynamicPropsJson),
951
+ transformsSource: ctx.storedTransformsSource
952
+ });
953
+ return null;
954
+ }
955
+ //#endregion
956
+ //#region src/index.ts
957
+ /**
958
+ * Vite adapter for the extraction pipeline. State and pipeline operations
959
+ * live in PluginContext; hook bodies live in their own modules — this
960
+ * factory only validates options and wires Vite hooks to those functions.
961
+ */
962
+ function animusExtract(options) {
963
+ (0, _animus_ui_extract_pipeline.assertNoRetiredEngineSelection)(options.engine);
964
+ const ctx = new PluginContext(options);
965
+ return {
966
+ name: "animus-extract",
967
+ enforce: "pre",
968
+ configureServer(server) {
969
+ ctx.devServer = server;
970
+ ctx.registerSystemWatchPaths();
971
+ },
972
+ configResolved(config) {
973
+ applyResolvedConfig(ctx, config);
974
+ },
975
+ async buildStart() {
976
+ await runBuildStart(ctx, async (specifier) => {
977
+ return (await this.resolve(specifier))?.id ?? null;
978
+ });
979
+ },
980
+ resolveId(id) {
981
+ return resolveVirtualId(ctx, id);
982
+ },
983
+ load(id) {
984
+ return loadVirtualModule(ctx, id);
682
985
  },
683
986
  transform(code, id) {
684
- if (!storedManifest) return null;
685
- if (!externalPackageDirs.some((dir) => id.startsWith(dir))) {
686
- if (!/\.[jt]sx?$/.test(id)) return null;
687
- if (id.includes("node_modules")) return null;
688
- }
689
- const relativePath = (0, path.relative)(rootDir, id);
690
- if (!storedManifest.files?.[relativePath]?.length) {
691
- if (!isProd && !fileCache.has(relativePath)) {
692
- const hash = contentHash(code);
693
- fileCache.set(relativePath, {
694
- hash,
695
- source: code
696
- });
697
- runAnalysis(buildFileEntriesFromCache(fileCache, relativePath));
698
- const compCount = storedManifest.files?.[relativePath]?.length ?? 0;
699
- log(`New file detected: ${relativePath} — ${compCount ? `${compCount} components extracted` : "no components"}`);
700
- if (compCount && devServer) {
701
- const compModule = devServer.moduleGraph.getModuleById(RESOLVED_COMPONENTS_ID);
702
- if (compModule) devServer.moduleGraph.invalidateModule(compModule);
703
- const sysPropModule = devServer.moduleGraph.getModuleById(RESOLVED_SYSTEM_PROPS_ID);
704
- if (sysPropModule) devServer.moduleGraph.invalidateModule(sysPropModule);
705
- setTimeout(() => {
706
- devServer.hot.send({ type: "full-reload" });
707
- }, 100);
708
- }
709
- }
710
- if (!storedManifest.files?.[relativePath]?.length) return null;
711
- }
712
- try {
713
- const { transformFile } = engineApi();
714
- const result = transformFile(code, relativePath, storedManifestJson);
715
- if (!result.hasComponents) return null;
716
- let transformedCode = result.code;
717
- if (!isProd && !bridgeInjected && storedSheets) {
718
- transformedCode = `import '${VIRTUAL_BRIDGE_ID}';\n${transformedCode}`;
719
- bridgeInjected = true;
720
- log("HMR bridge injected via transform");
721
- }
722
- if (verbose) log(`transform ${relativePath}: ${storedManifest.files?.[relativePath]?.length ?? 0} components`);
723
- return {
724
- code: transformedCode,
725
- map: null
726
- };
727
- } catch (e) {
728
- if (options.strict) throw new Error(`[animus-extract] Failed to transform ${id}: ${e}`, { cause: e });
729
- console.warn(`[animus-extract] Failed to transform ${id}:`, e);
730
- return null;
731
- }
987
+ return transformSource(ctx, code, id);
732
988
  },
733
989
  transformIndexHtml: {
734
990
  order: "pre",
735
991
  handler() {
736
- if (!layerDeclaration) return [];
737
- return [{
738
- tag: "style",
739
- attrs: { "data-animus-layers": "" },
740
- children: layerDeclaration,
741
- injectTo: "head-prepend"
742
- }];
992
+ return buildIndexHtmlTags(ctx);
743
993
  }
744
994
  },
745
- async handleHotUpdate({ file, server: hmrServer, modules }) {
746
- if (isProd) return;
747
- const ext = (0, path.extname)(file);
748
- if (!extensionsSet.has(ext)) return;
749
- const excludePatterns = options.exclude ?? DEFAULT_EXCLUDE;
750
- if (!externalPackageDirs.some((dir) => file.startsWith(dir)) && excludePatterns.some((pattern) => file.includes(pattern) || (0, path.relative)(rootDir, file).includes(pattern))) return;
751
- const absFile = (0, path.resolve)(file);
752
- const relPath = (0, path.relative)(rootDir, absFile);
753
- if (resolvedSystemPath && absFile === (0, path.resolve)(resolvedSystemPath)) {
754
- const resetStart = performance.now();
755
- log(`HMR geological reset: ${relPath}`);
756
- loadSystem();
757
- try {
758
- const { clearAnalysisCache } = engineApi();
759
- clearAnalysisCache();
760
- } catch {}
761
- const fileEntries = [];
762
- for (const [path$3, { hash, source }] of fileCache) fileEntries.push({
763
- path: path$3,
764
- source,
765
- hash
766
- });
767
- runAnalysis(fileEntries);
768
- log(`HMR geological reset complete: ${Math.round(performance.now() - resetStart)}ms`);
769
- const geologicalModules = [...modules];
770
- const cssModule = hmrServer.moduleGraph.getModuleById(RESOLVED_CSS_ID);
771
- if (cssModule) {
772
- hmrServer.moduleGraph.invalidateModule(cssModule);
773
- geologicalModules.push(cssModule);
774
- }
775
- const compModule = hmrServer.moduleGraph.getModuleById(RESOLVED_COMPONENTS_ID);
776
- if (compModule) {
777
- hmrServer.moduleGraph.invalidateModule(compModule);
778
- geologicalModules.push(compModule);
779
- }
780
- const sysPropModule = hmrServer.moduleGraph.getModuleById(RESOLVED_SYSTEM_PROPS_ID);
781
- if (sysPropModule) {
782
- hmrServer.moduleGraph.invalidateModule(sysPropModule);
783
- geologicalModules.push(sysPropModule);
784
- }
785
- return geologicalModules;
786
- }
787
- let source;
788
- try {
789
- source = (0, fs.readFileSync)(absFile, "utf-8");
790
- } catch {
791
- return;
792
- }
793
- let scannerRelPath = relPath;
794
- if (ext === ".mdx") {
795
- const result = await (0, _animus_ui_extract_pipeline.preprocessMdx)(source, relPath);
796
- if (result.kind === "missing-dep") {
797
- warn("⚠ .mdx HMR skipped: @mdx-js/mdx not installed; restart dev server after installing");
798
- return;
799
- }
800
- if (result.kind === "error") {
801
- warn(`⚠ MDX preprocessing failed for ${relPath}: ${result.error}`);
802
- return;
803
- }
804
- source = result.source;
805
- scannerRelPath = relPath + ".tsx";
806
- }
807
- const hash = contentHash(source);
808
- const cached = fileCache.get(scannerRelPath);
809
- if (cached && cached.hash === hash) {
810
- log(`HMR skip: ${scannerRelPath} (unchanged)`);
811
- return [];
812
- }
813
- fileCache.set(scannerRelPath, {
814
- hash,
815
- source
816
- });
817
- const hmrStart = performance.now();
818
- const prevReplacements = /* @__PURE__ */ new Map();
819
- if (storedManifest?.components) for (const [id, desc] of Object.entries(storedManifest.components)) prevReplacements.set(id, desc.replacement ?? "");
820
- const directComponentIds = storedManifest?.files?.[scannerRelPath] ?? [];
821
- const invalidatedIds = new Set(directComponentIds);
822
- const queue = [...directComponentIds];
823
- while (queue.length > 0) {
824
- const parentId = queue.shift();
825
- const children = reverseProvenance[parentId];
826
- if (children) {
827
- for (const childId of children) if (!invalidatedIds.has(childId)) {
828
- invalidatedIds.add(childId);
829
- queue.push(childId);
830
- }
831
- }
832
- }
833
- if (verbose && invalidatedIds.size > directComponentIds.length) log(`HMR: ${directComponentIds.length} direct + ${invalidatedIds.size - directComponentIds.length} transitive components invalidated`);
834
- const analysisStart = performance.now();
835
- runAnalysis(buildFileEntriesFromCache(fileCache, relPath));
836
- const analysisMs = Math.round(performance.now() - analysisStart);
837
- const modulesToUpdate = [...modules];
838
- const compModule = hmrServer.moduleGraph.getModuleById(RESOLVED_COMPONENTS_ID);
839
- if (compModule) {
840
- hmrServer.moduleGraph.invalidateModule(compModule);
841
- modulesToUpdate.push(compModule);
842
- }
843
- const sysPropModule = hmrServer.moduleGraph.getModuleById(RESOLVED_SYSTEM_PROPS_ID);
844
- if (sysPropModule) {
845
- hmrServer.moduleGraph.invalidateModule(sysPropModule);
846
- modulesToUpdate.push(sysPropModule);
847
- }
848
- if (storedManifest?.components) {
849
- const staleFiles = /* @__PURE__ */ new Set();
850
- for (const [id, desc] of Object.entries(storedManifest.components)) if ((desc.replacement ?? "") !== (prevReplacements.get(id) ?? "")) staleFiles.add(desc.file);
851
- for (const defFile of staleFiles) {
852
- const absDefPath = (0, path.resolve)(rootDir, defFile);
853
- if (absDefPath === absFile) continue;
854
- const defModule = hmrServer.moduleGraph.getModuleById(absDefPath) ?? hmrServer.moduleGraph.getModulesByFile(absDefPath)?.values().next().value;
855
- if (defModule) {
856
- log(`HMR invalidate: ${defFile} (replacement changed)`);
857
- hmrServer.moduleGraph.invalidateModule(defModule);
858
- modulesToUpdate.push(defModule);
859
- }
860
- }
861
- }
862
- const hmrMs = Math.round(performance.now() - hmrStart);
863
- log(`HMR update: ${relPath} — analysis ${analysisMs}ms, ${modulesToUpdate.length - modules.length} modules invalidated, total ${hmrMs}ms`);
864
- logTimingWaterfall(storedManifest?.timing ?? {});
865
- if (modulesToUpdate.length > modules.length) return modulesToUpdate;
995
+ async hotUpdate(hmr) {
996
+ return handleHotUpdate(ctx, this.environment, hmr);
866
997
  }
867
998
  };
868
999
  }
869
1000
  //#endregion
870
1001
  exports.animusExtract = animusExtract;
871
1002
  exports.default = animusExtract;
872
- exports.discoverFiles = discoverFiles;
1003
+ Object.defineProperty(exports, "discoverFiles", {
1004
+ enumerable: true,
1005
+ get: function() {
1006
+ return _animus_ui_extract_pipeline.discoverFiles;
1007
+ }
1008
+ });