@animus-ui/vite-plugin 0.1.1 → 0.1.3

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