@animus-ui/extract 0.1.1-beta.2 → 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.
Files changed (55) hide show
  1. package/README.md +12 -11
  2. package/crates/extract-v2/animus-extract-v2.darwin-arm64.node +0 -0
  3. package/crates/extract-v2/animus-extract-v2.linux-arm64-gnu.node +0 -0
  4. package/crates/extract-v2/animus-extract-v2.linux-x64-gnu.node +0 -0
  5. package/crates/extract-v2/index.d.ts +22 -0
  6. package/dist/analyze-project-args.d.ts +50 -0
  7. package/dist/analyze-project-args.d.ts.map +1 -0
  8. package/dist/content-hash.d.ts +7 -0
  9. package/dist/content-hash.d.ts.map +1 -0
  10. package/dist/discover-files.d.ts +10 -0
  11. package/dist/discover-files.d.ts.map +1 -0
  12. package/dist/discover-packages.d.ts +94 -2
  13. package/dist/discover-packages.d.ts.map +1 -1
  14. package/dist/dynamic-prop-config.d.ts +23 -0
  15. package/dist/dynamic-prop-config.d.ts.map +1 -0
  16. package/dist/engine-adapter.d.ts +95 -0
  17. package/dist/engine-adapter.d.ts.map +1 -0
  18. package/dist/engine-retirement.d.ts +22 -0
  19. package/dist/engine-retirement.d.ts.map +1 -0
  20. package/dist/index.cjs +927 -179
  21. package/dist/index.d.ts +26 -3
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.mjs +888 -177
  24. package/dist/manifest-diagnostics.d.ts +17 -0
  25. package/dist/manifest-diagnostics.d.ts.map +1 -0
  26. package/dist/path-aliases.d.ts +34 -0
  27. package/dist/path-aliases.d.ts.map +1 -0
  28. package/dist/post-process-css.d.ts +17 -0
  29. package/dist/post-process-css.d.ts.map +1 -0
  30. package/dist/run-analysis.d.ts +67 -0
  31. package/dist/run-analysis.d.ts.map +1 -0
  32. package/dist/static-css.d.ts +25 -0
  33. package/dist/static-css.d.ts.map +1 -0
  34. package/dist/system-config.d.ts +44 -0
  35. package/dist/system-config.d.ts.map +1 -0
  36. package/dist/system-props-module.d.ts +18 -0
  37. package/dist/system-props-module.d.ts.map +1 -0
  38. package/dist/timing-waterfall.d.ts +17 -0
  39. package/dist/timing-waterfall.d.ts.map +1 -0
  40. package/dist/tsconfig-paths.d.ts +8 -0
  41. package/dist/tsconfig-paths.d.ts.map +1 -0
  42. package/dist/watch-keys.d.ts +12 -0
  43. package/dist/watch-keys.d.ts.map +1 -0
  44. package/index-v2.js +6 -5
  45. package/package.json +14 -35
  46. package/animus-extract.darwin-arm64.node +0 -0
  47. package/animus-extract.linux-arm64-gnu.node +0 -0
  48. package/animus-extract.linux-x64-gnu.node +0 -0
  49. package/crates/extract-v2/index.js +0 -594
  50. package/dist/resolve-global-styles.d.ts +0 -25
  51. package/dist/resolve-global-styles.d.ts.map +0 -1
  52. package/dist/resolve-transforms.d.ts +0 -9
  53. package/dist/resolve-transforms.d.ts.map +0 -1
  54. package/index.d.ts +0 -65
  55. package/index.js +0 -594
package/dist/index.mjs CHANGED
@@ -1,5 +1,15 @@
1
- import { readFileSync } from "fs";
1
+ import { createRequire } from "node:module";
2
+ import { createHash } from "crypto";
3
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
4
+ import { dirname, extname, isAbsolute, join, relative, resolve } from "path";
5
+ import browserslist from "browserslist";
6
+ import { browserslistToTargets, transform } from "lightningcss";
2
7
  import { UNITLESS_PROPERTIES } from "@animus-ui/properties";
8
+ import { realpathSync } from "node:fs";
9
+ import { basename, dirname as dirname$1, join as join$1, normalize } from "node:path";
10
+ //#region \0rolldown/runtime.js
11
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
12
+ //#endregion
3
13
  //#region pipeline/assemble-stylesheet.ts
4
14
  /**
5
15
  * The 7 Animus cascade layers. Always prefixed with `anm-` to avoid
@@ -72,8 +82,319 @@ function assembleStylesheet(options) {
72
82
  ].filter(Boolean).join("\n");
73
83
  }
74
84
  //#endregion
85
+ //#region pipeline/analyze-project-args.ts
86
+ /** @internal */
87
+ function buildAnalyzeProjectArgs(inputs) {
88
+ return [
89
+ inputs.filesJson,
90
+ inputs.scalesJson,
91
+ inputs.variableMapJson,
92
+ inputs.contextualVarsJson,
93
+ inputs.propConfigJson,
94
+ inputs.groupRegistryJson,
95
+ inputs.packageResolutionJson,
96
+ inputs.devMode,
97
+ inputs.emitterConfigJson,
98
+ inputs.selectorAliasesJson,
99
+ null,
100
+ inputs.globalStyleBlocksJson,
101
+ inputs.pathAliasesJson,
102
+ inputs.keyframesJson,
103
+ inputs.staticCssJson,
104
+ inputs.conditionAliasesJson
105
+ ];
106
+ }
107
+ //#endregion
108
+ //#region pipeline/engine-adapter.ts
109
+ /**
110
+ * Build the engine-agnostic `engineApi()` both plugins call. Returns a function
111
+ * (invoked per call site) so `isV2()` and `loadNativeEngine()` re-evaluate each
112
+ * time — next reads its engine choice and native module dynamically from
113
+ * globalThis.
114
+ */
115
+ function createV2EngineApi(deps) {
116
+ const { label, isV2, loadNativeEngine, store } = deps;
117
+ return () => {
118
+ if (!isV2()) return loadNativeEngine();
119
+ const native = loadNativeEngine();
120
+ return {
121
+ loadSystemModule: (...args) => native.loadSystemModule(...args),
122
+ analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson) => {
123
+ const filesJson = deps.rehydrateFilesJson ? deps.rehydrateFilesJson(filesJsonRaw) : filesJsonRaw;
124
+ const sent = /* @__PURE__ */ new Map();
125
+ for (const entry of JSON.parse(filesJson)) sent.set(entry.path, entry.source);
126
+ store.setSentSources(sent);
127
+ const emitterConfig = emitterConfigJson ? JSON.parse(emitterConfigJson) : {};
128
+ store.setEngine(null);
129
+ const config = {
130
+ runtimeImport: emitterConfig.runtime_import ?? void 0,
131
+ cssModuleId: emitterConfig.css_module_id ?? void 0,
132
+ systemPropsModuleId: emitterConfig.system_props_module_id ?? void 0,
133
+ themeJson: scalesJson,
134
+ variableMapJson,
135
+ contextualVarsJson: contextualVarsJson ?? void 0,
136
+ configJson: propConfigJson,
137
+ groupRegistryJson,
138
+ selectorAliasesJson: selectorAliasesJson ?? void 0,
139
+ conditionAliasesJson: conditionAliasesJson ?? void 0,
140
+ globalStyleBlocksJson: globalStyleBlocksJson ?? void 0,
141
+ keyframesJson: keyframesJson ?? void 0,
142
+ packageResolutionJson: packageResolutionJson ?? void 0,
143
+ pathAliasesJson: pathAliasesJson ?? void 0,
144
+ staticCssJson: staticCssJson ?? void 0,
145
+ devMode
146
+ };
147
+ const engine = new native.ExtractEngine(config);
148
+ store.setEngine(engine);
149
+ return engine.analyze(filesJson);
150
+ },
151
+ transformFile: (source, path, _manifest) => {
152
+ const engine = store.getEngine();
153
+ if (!engine) throw new Error(`[${label}] v2 transform before analyze — engine instance not initialized`);
154
+ const sentMap = store.getSentSources();
155
+ if (deps.passThroughUnknownPaths && sentMap && !sentMap.has(path)) return {
156
+ code: source,
157
+ hasComponents: false
158
+ };
159
+ const sent = sentMap?.get(path);
160
+ if (sent !== void 0 && sent !== source && !store.getDriftWarned()) {
161
+ store.setDriftWarned(true);
162
+ console.warn(`[${label}] v2: transform-time source for ${path} differs from analyze-time source — an upstream transform may be reverted`);
163
+ }
164
+ return JSON.parse(engine.transformFile(path));
165
+ },
166
+ clearAnalysisCache: () => {
167
+ const engine = store.getEngine();
168
+ if (engine) engine.clearCache();
169
+ store.setEngine(null);
170
+ }
171
+ };
172
+ };
173
+ }
174
+ //#endregion
175
+ //#region pipeline/engine-retirement.ts
176
+ /**
177
+ * Loud rejection of the retired v1 extraction engine (openspec:
178
+ * retire-extract-v1).
179
+ *
180
+ * v2 is the only engine. Selecting v1 — via a plugin `engine: 'v1'` option or
181
+ * the `ANIMUS_ENGINE=v1` fixture env override — is a build-stopping error, never
182
+ * a silent upgrade to v2 (design D1: a silent flip would let a pinned consumer's
183
+ * build change engines without notice). Both extraction plugins call this before
184
+ * any engine selection so the rejection is uniform and the message is defined
185
+ * once.
186
+ */
187
+ /** The one canonical retirement message, shared verbatim by both plugins and
188
+ * the env path (tasks.md cross-cutting: define once, reuse). */
189
+ const RETIRED_ENGINE_MESSAGE = "animus: extraction engine 'v1' was retired (openspec: retire-extract-v1) — v2 is the only engine; remove the engine option / ANIMUS_ENGINE override";
190
+ /**
191
+ * Throw the canonical retirement error when v1 is selected through either
192
+ * surface. Accepts the raw engine option as a string because the option type no
193
+ * longer admits `'v1'` — callers cast it so a stale `'v1'` config still fails
194
+ * loud at runtime instead of being silently coerced.
195
+ */
196
+ function assertNoRetiredEngineSelection(engineOption) {
197
+ if (engineOption === "v1" || process.env.ANIMUS_ENGINE === "v1") throw new Error(RETIRED_ENGINE_MESSAGE);
198
+ }
199
+ //#endregion
200
+ //#region pipeline/content-hash.ts
201
+ /**
202
+ * MD5 content hash used for file-change detection (HMR diffing) by both
203
+ * extraction plugins. The algorithm/encoding is a cross-plugin contract:
204
+ * cache keys written by one build path must compare equal in the next.
205
+ */
206
+ function contentHash(source) {
207
+ return createHash("md5").update(source).digest("hex");
208
+ }
209
+ //#endregion
210
+ //#region pipeline/discover-files.ts
211
+ /**
212
+ * Recursively discover source files under `dir`, skipping any path that
213
+ * contains one of `excludePatterns` (matched as substrings of both the full
214
+ * and root-relative path) and keeping files whose extension is in
215
+ * `extensionsSet`.
216
+ *
217
+ * Single authoritative copy for both extraction plugins.
218
+ */
219
+ function discoverFiles(dir, rootDir, excludePatterns, extensionsSet) {
220
+ const results = [];
221
+ let entries;
222
+ try {
223
+ entries = readdirSync(dir, { encoding: "utf8" });
224
+ } catch {
225
+ return results;
226
+ }
227
+ for (const entry of entries) {
228
+ const fullPath = join(dir, entry);
229
+ const relativePath = relative(rootDir, fullPath);
230
+ if (excludePatterns.some((pattern) => fullPath.includes(pattern) || relativePath.includes(pattern))) continue;
231
+ let stat;
232
+ try {
233
+ stat = statSync(fullPath);
234
+ } catch {
235
+ continue;
236
+ }
237
+ if (stat.isDirectory()) results.push(...discoverFiles(fullPath, rootDir, excludePatterns, extensionsSet));
238
+ else if (extensionsSet.has(extname(entry))) results.push(fullPath);
239
+ }
240
+ return results;
241
+ }
242
+ //#endregion
75
243
  //#region pipeline/discover-packages.ts
76
244
  /**
245
+ * Walk up from a resolved package entry file to the nearest directory
246
+ * containing a package.json (the package root). Stops at the filesystem
247
+ * root and returns it if no package.json is found on the way up.
248
+ */
249
+ function findPackageRoot(absEntryPath) {
250
+ let pkgRoot = dirname(absEntryPath);
251
+ while (pkgRoot !== dirname(pkgRoot) && !existsSync(join(pkgRoot, "package.json"))) pkgRoot = dirname(pkgRoot);
252
+ return pkgRoot;
253
+ }
254
+ /**
255
+ * Exclusion fragments for discovery inside an external package's src/,
256
+ * matched against paths RELATIVE TO the src dir. Per spec
257
+ * external-package-file-discovery, the filters apply "within the package" —
258
+ * an npm-installed package whose own location is under node_modules (or a
259
+ * repo path containing 'dist') must still have its sources discovered.
260
+ */
261
+ const PACKAGE_SRC_EXCLUDES = [
262
+ "dist",
263
+ "node_modules",
264
+ ".test.",
265
+ ".spec."
266
+ ];
267
+ const isFile = (path) => {
268
+ try {
269
+ return statSync(path).isFile();
270
+ } catch {
271
+ return false;
272
+ }
273
+ };
274
+ /**
275
+ * Resolve an absolute-path specifier (a relative `includes` entry already
276
+ * resolved against the system file) without a bundler: the path itself when it
277
+ * is a file, otherwise the extension and directory-index candidates built from
278
+ * the caller's extension set. Used only as a fallback — a bundler resolver that
279
+ * answers first still wins, since it also knows aliases and `.js`→`.ts` mapping.
280
+ */
281
+ function resolveAbsolutePathSpecifier(absSpecifier, extensionsSet) {
282
+ return [
283
+ absSpecifier,
284
+ ...Array.from(extensionsSet, (ext) => absSpecifier + ext),
285
+ ...Array.from(extensionsSet, (ext) => join(absSpecifier, `index${ext}`))
286
+ ].find(isFile) ?? null;
287
+ }
288
+ /**
289
+ * Shared external-package source collection (spec:
290
+ * external-package-file-discovery), consumed by both extraction plugins.
291
+ * For each specifier: resolve to an absolute entry (null/throw → silently
292
+ * skip; an absolute-path specifier falls back to a filesystem probe), walk up
293
+ * to the package root, then either discover sources under src/ (redirecting
294
+ * module resolution to src/index.ts when present) or fall back to ingesting
295
+ * the resolved entry file itself.
296
+ *
297
+ * The only bundler-specific seams are callbacks: specifier resolution,
298
+ * dedup against already-ingested files, per-file preprocessing (MDX), and
299
+ * the unreadable-file warning. Hashing and caching stay in the plugins —
300
+ * their cache policies legitimately differ.
301
+ *
302
+ * Alongside the aggregates, one `ExternalPackageOutcome` per declared
303
+ * specifier records what that specifier produced. Collection never reports on
304
+ * them — any warning or gate is the caller's policy.
305
+ */
306
+ async function collectExternalPackageSources(opts) {
307
+ const { specifiers, resolveSpecifier, rootDir, extensionsSet, hasEntry, preprocessFile, onUnreadable } = opts;
308
+ const entries = [];
309
+ const pushed = /* @__PURE__ */ new Set();
310
+ const packageMap = {};
311
+ const sourceEntries = /* @__PURE__ */ new Map();
312
+ const packageDirs = [];
313
+ const outcomes = [];
314
+ const alreadyIngested = (relPath) => hasEntry(relPath) || pushed.has(relPath);
315
+ for (const specifier of specifiers) {
316
+ let absEntry;
317
+ try {
318
+ absEntry = await resolveSpecifier(specifier);
319
+ } catch {
320
+ absEntry = null;
321
+ }
322
+ if (!absEntry && isAbsolute(specifier)) absEntry = resolveAbsolutePathSpecifier(specifier, extensionsSet);
323
+ if (!absEntry) {
324
+ outcomes.push({
325
+ specifier,
326
+ outcome: "unresolvable",
327
+ fileCount: 0
328
+ });
329
+ continue;
330
+ }
331
+ const srcDir = join(findPackageRoot(absEntry), "src");
332
+ let fileCount = 0;
333
+ if (existsSync(srcDir)) {
334
+ packageDirs.push(srcDir);
335
+ const srcEntry = join(srcDir, "index.ts");
336
+ if (existsSync(srcEntry)) {
337
+ packageMap[specifier] = relative(rootDir, srcEntry);
338
+ sourceEntries.set(specifier, srcEntry);
339
+ } else packageMap[specifier] = relative(rootDir, absEntry);
340
+ const pkgFiles = discoverFiles(srcDir, srcDir, [], extensionsSet).filter((absPath) => {
341
+ const inPkg = relative(srcDir, absPath);
342
+ return !PACKAGE_SRC_EXCLUDES.some((pattern) => inPkg.includes(pattern));
343
+ });
344
+ for (const pkgFile of pkgFiles) {
345
+ const relPath = relative(rootDir, pkgFile);
346
+ if (alreadyIngested(relPath)) {
347
+ fileCount++;
348
+ continue;
349
+ }
350
+ let source;
351
+ try {
352
+ source = readFileSync(pkgFile, "utf-8");
353
+ } catch (err) {
354
+ onUnreadable(relPath, err);
355
+ continue;
356
+ }
357
+ const processed = await preprocessFile(source, relPath, pkgFile);
358
+ if (!processed) continue;
359
+ entries.push({
360
+ path: processed.relPath,
361
+ source: processed.source
362
+ });
363
+ pushed.add(processed.relPath);
364
+ fileCount++;
365
+ }
366
+ } else {
367
+ packageDirs.push(dirname(absEntry));
368
+ const relPath = relative(rootDir, absEntry);
369
+ packageMap[specifier] = relPath;
370
+ if (alreadyIngested(relPath)) fileCount++;
371
+ else try {
372
+ const source = readFileSync(absEntry, "utf-8");
373
+ entries.push({
374
+ path: relPath,
375
+ source
376
+ });
377
+ pushed.add(relPath);
378
+ fileCount++;
379
+ } catch (err) {
380
+ onUnreadable(relPath, err);
381
+ }
382
+ }
383
+ outcomes.push({
384
+ specifier,
385
+ outcome: fileCount > 0 ? "resolved" : "empty",
386
+ fileCount
387
+ });
388
+ }
389
+ return {
390
+ entries,
391
+ packageMap,
392
+ sourceEntries,
393
+ packageDirs,
394
+ outcomes
395
+ };
396
+ }
397
+ /**
77
398
  * Extract external DS package names from `includes` declarations in the system file.
78
399
  *
79
400
  * Supports two forms:
@@ -81,8 +402,11 @@ function assembleStylesheet(options) {
81
402
  * - Legacy: `.includes([identifier, ...])` chain method (RC migration fallback)
82
403
  *
83
404
  * For each identifier found, traces back to its import declaration and returns
84
- * the import specifier. Only packages explicitly declared via `includes` are treated
85
- * as external DS dependencies.
405
+ * the import specifier: a bare specifier normalized to its package name, a
406
+ * relative specifier resolved against the system file's directory into an
407
+ * absolute path (so a sibling package referenced by path contributes discovery
408
+ * too). Only packages explicitly declared via `includes` are treated as
409
+ * external DS dependencies.
86
410
  *
87
411
  * Falls back to empty array if no `includes` declaration is found.
88
412
  */
@@ -122,10 +446,13 @@ function extractSystemFilePackages(systemFilePath) {
122
446
  if (localName) importMap.set(localName, specifier);
123
447
  }
124
448
  }
449
+ const systemFileDir = dirname(systemFilePath);
125
450
  const packages = /* @__PURE__ */ new Set();
126
451
  for (const id of identifiers) {
127
452
  const specifier = importMap.get(id);
128
- if (specifier && !specifier.startsWith(".")) {
453
+ if (!specifier) continue;
454
+ if (specifier.startsWith(".")) packages.add(resolve(systemFileDir, specifier));
455
+ else {
129
456
  const pkgName = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
130
457
  packages.add(pkgName);
131
458
  }
@@ -133,6 +460,525 @@ function extractSystemFilePackages(systemFilePath) {
133
460
  return Array.from(packages);
134
461
  }
135
462
  //#endregion
463
+ //#region pipeline/path-aliases.ts
464
+ /**
465
+ * Normalize alias pairs: strip a `rootDir` prefix from targets, classify
466
+ * exact (file) vs prefix (directory) matches, force trailing slashes on
467
+ * prefix entries, and sort longest pattern first for correct matching
468
+ * priority. Returns null when no entries survive.
469
+ */
470
+ function buildPathAliasesJson(pairs, rootDir) {
471
+ const entries = [];
472
+ for (const { pattern, target, kind } of pairs) {
473
+ const replacement = target.startsWith(rootDir) ? target.slice(rootDir.length + 1) : target;
474
+ if (kind ? kind === "exact" : /\.\w+$/.test(replacement)) entries.push({
475
+ pattern,
476
+ replacement,
477
+ type: "exact"
478
+ });
479
+ else entries.push({
480
+ pattern: pattern.endsWith("/") ? pattern : pattern + "/",
481
+ replacement: replacement.endsWith("/") ? replacement : replacement + "/",
482
+ type: "prefix"
483
+ });
484
+ }
485
+ entries.sort((a, b) => b.pattern.length - a.pattern.length);
486
+ if (entries.length === 0) return null;
487
+ return {
488
+ json: JSON.stringify({ aliases: entries }),
489
+ count: entries.length
490
+ };
491
+ }
492
+ //#endregion
493
+ //#region pipeline/post-process-css.ts
494
+ /**
495
+ * CSS post-processing (spec: css-post-processing) — Lightning CSS
496
+ * autoprefixing, syntax lowering, and optional minification against
497
+ * browserslist targets. The single implementation consumed by both the
498
+ * Vite plugin (virtual-module serve time) and the Next.js plugin
499
+ * (ExtractionSession artifact emission).
500
+ */
501
+ /**
502
+ * Resolve browser targets for Lightning CSS.
503
+ * Priority: explicit config → project browserslist → 'defaults' fallback.
504
+ */
505
+ function resolveLightningTargets(explicitTargets, rootDir) {
506
+ let queries;
507
+ if (explicitTargets) queries = Array.isArray(explicitTargets) ? explicitTargets : [explicitTargets];
508
+ else {
509
+ const detected = browserslist(void 0, { path: rootDir });
510
+ queries = detected.length > 0 ? detected : browserslist("defaults");
511
+ }
512
+ return browserslistToTargets(Array.isArray(queries) && typeof queries[0] === "string" && queries[0].includes(" ") ? browserslist(queries) : queries);
513
+ }
514
+ /**
515
+ * Post-process CSS with Lightning CSS: autoprefixing + optional minification.
516
+ * On failure, returns the original CSS and logs a warning.
517
+ */
518
+ function postProcessCss(css, opts) {
519
+ if (!css) return css;
520
+ try {
521
+ return transform({
522
+ filename: "animus-extracted.css",
523
+ code: Buffer.from(css),
524
+ minify: opts.minify,
525
+ targets: opts.targets
526
+ }).code.toString();
527
+ } catch (e) {
528
+ const msg = e instanceof Error ? e.message : String(e);
529
+ (opts.warnFn ?? console.warn)(`[animus] Lightning CSS post-processing failed: ${msg}`);
530
+ return css;
531
+ }
532
+ }
533
+ //#endregion
534
+ //#region pipeline/manifest-diagnostics.ts
535
+ /**
536
+ * Surface extraction-manifest diagnostics through a plugin's warn channel.
537
+ *
538
+ * Single authoritative copy for both extraction plugins. Surfaces `bail`
539
+ * (component not extracted), `skip` (component skipped), and `warn` kinds;
540
+ * unknown kinds stay silent.
541
+ */
542
+ function surfaceManifestDiagnostics(manifest, warn) {
543
+ for (const diagnostic of manifest.diagnostics ?? []) if (diagnostic.kind === "bail") warn(`⚠ ${diagnostic.component} not extracted: ${diagnostic.message}`);
544
+ else if (diagnostic.kind === "skip") warn(`⚠ ${diagnostic.component}: skipped ${diagnostic.message}`);
545
+ else if (diagnostic.kind === "warn") warn(`⚠ ${diagnostic.file}: ${diagnostic.component}: ${diagnostic.message}`);
546
+ }
547
+ //#endregion
548
+ //#region pipeline/unit-fallback.ts
549
+ /**
550
+ * Append `px` to bare numeric values in CSS declarations for properties
551
+ * that expect length units. Unitless properties are preserved as-is.
552
+ * Numbers inside CSS function calls (cubic-bezier, rgb, calc, etc.) are skipped.
553
+ */
554
+ function applyUnitFallback(css) {
555
+ return css.replace(/([a-z-]+)\s*:\s*([^;{}]+);/g, (match, prop, value) => {
556
+ if (UNITLESS_PROPERTIES.has(prop)) return match;
557
+ let depth = 0;
558
+ let fixed = "";
559
+ let i = 0;
560
+ while (i < value.length) if (value[i] === "(") {
561
+ depth++;
562
+ fixed += value[i];
563
+ i++;
564
+ } else if (value[i] === ")") {
565
+ depth--;
566
+ fixed += value[i];
567
+ i++;
568
+ } else if (depth > 0) {
569
+ fixed += value[i];
570
+ i++;
571
+ } else {
572
+ const numMatch = value.slice(i).match(/^(-?\d+\.?\d*)/);
573
+ if (numMatch) {
574
+ const num = numMatch[1];
575
+ const after = value[i + num.length];
576
+ if (after && /[a-z%]/i.test(after)) fixed += num;
577
+ else fixed += num + "px";
578
+ i += num.length;
579
+ } else {
580
+ fixed += value[i];
581
+ i++;
582
+ }
583
+ }
584
+ return fixed !== value ? `${prop}:${fixed};` : match;
585
+ });
586
+ }
587
+ //#endregion
588
+ //#region pipeline/run-analysis.ts
589
+ /**
590
+ * Build the named `analyzeProject` input set from analysis options. Also
591
+ * the persistence shape for `.animus/analysis-inputs.json` — an isolated
592
+ * process can replay the analysis from exactly this object
593
+ * (spec: next-turbopack-integration).
594
+ */
595
+ function buildAnalysisInputs(opts) {
596
+ return {
597
+ filesJson: JSON.stringify(opts.fileEntries),
598
+ scalesJson: opts.system.scalesJson,
599
+ variableMapJson: opts.system.variableMapJson,
600
+ contextualVarsJson: opts.system.contextualVarsJson,
601
+ propConfigJson: opts.system.propConfigJson,
602
+ groupRegistryJson: opts.system.groupRegistryJson,
603
+ packageResolutionJson: JSON.stringify(opts.packageMap),
604
+ devMode: opts.devMode,
605
+ emitterConfigJson: JSON.stringify({
606
+ runtime_import: opts.emitter.runtimeImport,
607
+ css_module_id: opts.emitter.cssModuleId,
608
+ ...opts.emitter.systemPropsModuleId ? { system_props_module_id: opts.emitter.systemPropsModuleId } : {}
609
+ }),
610
+ selectorAliasesJson: opts.system.selectorAliasesJson,
611
+ globalStyleBlocksJson: opts.system.globalStyleBlocksJson,
612
+ pathAliasesJson: opts.pathAliasesJson,
613
+ keyframesJson: opts.system.keyframesJson,
614
+ staticCssJson: opts.staticCssJson ?? null,
615
+ conditionAliasesJson: opts.system.conditionAliasesJson ?? null
616
+ };
617
+ }
618
+ /**
619
+ * The one analysis invocation both plugins share: build the emitter
620
+ * config, serialize inputs, call the NAPI `analyzeProject`, parse the
621
+ * manifest, surface its diagnostics, and resolve the CSS outputs.
622
+ *
623
+ * Error handling stays at the call site (strict-mode throw vs warn).
624
+ */
625
+ function runProjectAnalysis(engineApi, opts) {
626
+ const { analyzeProject } = engineApi();
627
+ let t = performance.now();
628
+ const inputs = buildAnalysisInputs(opts);
629
+ const serializeMs = Math.round(performance.now() - t);
630
+ t = performance.now();
631
+ const manifestJson = analyzeProject(...buildAnalyzeProjectArgs(inputs));
632
+ const extractMs = Math.round(performance.now() - t);
633
+ t = performance.now();
634
+ const manifest = JSON.parse(manifestJson);
635
+ surfaceManifestDiagnostics(manifest, opts.warn);
636
+ const parseMs = Math.round(performance.now() - t);
637
+ return {
638
+ manifest,
639
+ manifestJson,
640
+ globalCss: manifest?.sheets?.global || "",
641
+ componentCss: applyUnitFallback(manifest?.css || ""),
642
+ inputs,
643
+ timings: {
644
+ serializeMs,
645
+ extractMs,
646
+ parseMs
647
+ }
648
+ };
649
+ }
650
+ /**
651
+ * Clear the engine's per-file analysis cache so stale results from a prior
652
+ * build never bleed into a fresh run. Tolerates engines without the
653
+ * capability (older builds) — the probe is benign.
654
+ */
655
+ function clearEngineCache(engineApi) {
656
+ try {
657
+ const { clearAnalysisCache } = engineApi();
658
+ clearAnalysisCache();
659
+ } catch {}
660
+ }
661
+ //#endregion
662
+ //#region pipeline/static-css.ts
663
+ /** Key-sorted stringify so semantically identical configs serialize
664
+ * identically (stable analysis-inputs hashing). */
665
+ function stableStringify(value) {
666
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
667
+ if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
668
+ return JSON.stringify(value);
669
+ }
670
+ /** Serialize a staticCss option for the engine; null when absent/empty. */
671
+ function serializeStaticCss(config) {
672
+ if (!config) return null;
673
+ const json = stableStringify(config);
674
+ return json === "{}" ? null : json;
675
+ }
676
+ //#endregion
677
+ //#region pipeline/prefix.ts
678
+ /**
679
+ * Apply namespace prefix to a variable map, CSS variable declarations, and theme JSON.
680
+ *
681
+ * Variable map: `{ "colors.ember": "--color-ember" }` -> `{ "colors.ember": "--prefix-color-ember" }`
682
+ * Variable CSS: `--color-ember: #FF2800` -> `--prefix-color-ember: #FF2800`
683
+ * `var(--color-ember)` -> `var(--prefix-color-ember)`
684
+ * Theme JSON: `{ "colors.ember": "var(--color-ember)" }` -> `{ "colors.ember": "var(--prefix-color-ember)" }`
685
+ */
686
+ function applyPrefix(prefix, variableMapJson, variableCss, themeJson, contextualVarsJson) {
687
+ if (!prefix) return {
688
+ variableMapJson,
689
+ variableCss,
690
+ themeJson,
691
+ contextualVarsJson
692
+ };
693
+ const varRefRe = /var\(--([a-zA-Z][\w-]*)\)/g;
694
+ const map = JSON.parse(variableMapJson);
695
+ const prefixed = {};
696
+ for (const [key, varName] of Object.entries(map)) prefixed[key] = varName.startsWith("--") ? `--${prefix}-${varName.slice(2)}` : varName;
697
+ let css = variableCss;
698
+ css = css.replace(/--([a-zA-Z][\w-]*)\s*:/g, `--${prefix}-$1:`);
699
+ css = css.replace(varRefRe, `var(--${prefix}-$1)`);
700
+ const result = {
701
+ variableMapJson: JSON.stringify(prefixed),
702
+ variableCss: css
703
+ };
704
+ if (themeJson) result.themeJson = themeJson.replace(varRefRe, `var(--${prefix}-$1)`);
705
+ if (contextualVarsJson) {
706
+ const ctxVars = JSON.parse(contextualVarsJson);
707
+ const prefixedCtx = {};
708
+ for (const [scale, names] of Object.entries(ctxVars)) prefixedCtx[scale] = names.map((name) => `${prefix}-${name}`);
709
+ result.contextualVarsJson = JSON.stringify(prefixedCtx);
710
+ }
711
+ return result;
712
+ }
713
+ //#endregion
714
+ //#region pipeline/system-config.ts
715
+ /**
716
+ * Load a SystemInstance via the engine's `loadSystemModule` and normalize
717
+ * it into a SystemConfig. When `prefix` is set, CSS variable names in the
718
+ * variable map/css (and theme + contextual vars when affected) are
719
+ * namespaced via the shared `applyPrefix`.
720
+ *
721
+ * Error handling stays at the call site — the Vite plugin warns (or throws
722
+ * in strict mode) and keeps its previous config, the Next plugin lets the
723
+ * failure propagate.
724
+ */
725
+ function loadSystemConfig(engineApi, opts) {
726
+ const { loadSystemModule } = engineApi();
727
+ const config = loadSystemModule(opts.systemPath, opts.rootDir);
728
+ let scalesJson = config.scalesJson;
729
+ let variableMapJson = config.variableMapJson;
730
+ let variableCss = config.variableCss;
731
+ let contextualVarsJson = config.contextualVarsJson || null;
732
+ if (opts.prefix) {
733
+ const prefixed = applyPrefix(opts.prefix, variableMapJson, variableCss, scalesJson, contextualVarsJson || void 0);
734
+ variableMapJson = prefixed.variableMapJson;
735
+ variableCss = prefixed.variableCss;
736
+ if (prefixed.themeJson) scalesJson = prefixed.themeJson;
737
+ if (prefixed.contextualVarsJson) contextualVarsJson = prefixed.contextualVarsJson;
738
+ }
739
+ return {
740
+ propConfigJson: config.propConfig,
741
+ groupRegistryJson: config.groupRegistry,
742
+ scalesJson,
743
+ variableMapJson,
744
+ variableCss,
745
+ contextualVarsJson,
746
+ selectorAliasesJson: config.selectorAliases || null,
747
+ conditionAliasesJson: config.conditionAliases || null,
748
+ globalStyleBlocksJson: config.globalStyleBlocks || null,
749
+ keyframesJson: config.keyframesBlocks || null,
750
+ dependencies: config.dependencies ?? []
751
+ };
752
+ }
753
+ //#endregion
754
+ //#region pipeline/dynamic-prop-config.ts
755
+ function buildDynamicPropConfig(dynamicProps) {
756
+ const configEntries = {};
757
+ for (const [propName, meta] of Object.entries(dynamicProps)) configEntries[propName] = {
758
+ varName: meta.var_name,
759
+ slotClass: meta.slot_class,
760
+ ...meta.transform_name ? { transformName: meta.transform_name } : {},
761
+ ...meta.scale_values && Object.keys(meta.scale_values).length > 0 ? { scaleValues: meta.scale_values } : {}
762
+ };
763
+ return configEntries;
764
+ }
765
+ //#endregion
766
+ //#region pipeline/system-props-module.ts
767
+ /**
768
+ * Build the source of the runtime system-props module — served as
769
+ * `virtual:animus/system-props` by the Vite plugin and written to
770
+ * `.animus/system-props.js` by the Next plugin. One generator so the two
771
+ * runtimes can never drift.
772
+ *
773
+ * Runtime transform functions for dynamic props are not yet supported
774
+ * (transforms are resolved at extraction time via boa_engine in Rust), so
775
+ * `transforms` defaults to an empty object literal.
776
+ */
777
+ function buildSystemPropsModule(opts) {
778
+ const dynamicPropConfig = buildDynamicPropConfig(opts.dynamicProps);
779
+ return `export const systemPropMap = ${opts.systemPropMapJson};\nexport const systemPropGroups = ${opts.groupRegistryJson};\nexport const dynamicPropConfig = ${JSON.stringify(dynamicPropConfig)};\nexport const transforms = ${opts.transformsSource ?? "{}"};\n`;
780
+ }
781
+ //#endregion
782
+ //#region pipeline/timing-waterfall.ts
783
+ /**
784
+ * Rust-side timing phases as reported in `manifest.timing`, in waterfall
785
+ * order, with their display labels. Mirrors the timing struct in the
786
+ * extract-v2 crate — a renamed or added Rust phase is updated here once,
787
+ * not per plugin.
788
+ */
789
+ const RUST_TIMING_PHASES = [
790
+ ["parseAndWalk", "parse+walk"],
791
+ ["importResolution", "imports"],
792
+ ["extensionProvenance", "provenance"],
793
+ ["topologicalSort", "topo-sort"],
794
+ ["chainEvaluation", "chains"],
795
+ ["jsxScanning", "jsx-scan"],
796
+ ["systemPropAggregation", "sys-props"],
797
+ ["usageLedger", "usage"],
798
+ ["reconciliation", "reconcile"],
799
+ ["cssGeneration", "css-gen"],
800
+ ["manifestSerialization", "serialize"]
801
+ ];
802
+ /**
803
+ * Format the Rust timing waterfall as log lines. `indent` and `labelWidth`
804
+ * let each plugin keep its established column layout; the phase table and
805
+ * the `(N files, M cached)` suffix are shared.
806
+ */
807
+ function formatRustTimingWaterfall(timing, opts) {
808
+ const lines = [];
809
+ for (const [key, label] of RUST_TIMING_PHASES) {
810
+ const ms = timing[key] ?? 0;
811
+ const pad = " ".repeat(Math.max(0, opts.labelWidth - label.length));
812
+ const extra = key === "parseAndWalk" ? ` (${timing.fileCount ?? 0} files, ${timing.cacheHits ?? 0} cached)` : "";
813
+ lines.push(`${opts.indent}${label}${pad}${String(ms).padStart(5)}ms${extra}`);
814
+ }
815
+ return lines;
816
+ }
817
+ //#endregion
818
+ //#region pipeline/tsconfig-paths.ts
819
+ /**
820
+ * tsconfig `paths` → alias pairs for the Rust `pathAliasesJson` contract
821
+ * (via `buildPathAliasesJson`). Used by drivers that cannot harvest aliases
822
+ * from a live bundler config — Turbopack exposes none.
823
+ *
824
+ * Semantics (deliberately the TypeScript subset that matters for import
825
+ * provenance): JSONC tolerated (comments + trailing commas); `extends`
826
+ * chains followed (string or array; missing parents and cycles skipped);
827
+ * the NEAREST config declaring `paths` wins wholesale (TS replaces the
828
+ * object, never merges); targets resolve against the nearest `baseUrl`
829
+ * (resolved from its declaring config's directory) or, absent one, the
830
+ * directory of the config declaring `paths`; first target per pattern.
831
+ * `x/*` patterns become prefix pairs, non-wildcard patterns exact pairs,
832
+ * bare `*` catch-alls and mid-pattern wildcards are skipped.
833
+ */
834
+ /** String-aware JSONC → JSON: strips // and block comments + trailing commas. */
835
+ function stripJsonc(text) {
836
+ let out = "";
837
+ let inString = false;
838
+ let inLine = false;
839
+ let inBlock = false;
840
+ for (let i = 0; i < text.length; i++) {
841
+ const ch = text[i];
842
+ const next = text[i + 1];
843
+ if (inLine) {
844
+ if (ch === "\n") {
845
+ inLine = false;
846
+ out += ch;
847
+ }
848
+ continue;
849
+ }
850
+ if (inBlock) {
851
+ if (ch === "*" && next === "/") {
852
+ inBlock = false;
853
+ i++;
854
+ }
855
+ continue;
856
+ }
857
+ if (inString) {
858
+ out += ch;
859
+ if (ch === "\\") {
860
+ out += next ?? "";
861
+ i++;
862
+ } else if (ch === "\"") inString = false;
863
+ continue;
864
+ }
865
+ if (ch === "\"") {
866
+ inString = true;
867
+ out += ch;
868
+ continue;
869
+ }
870
+ if (ch === "/" && next === "/") {
871
+ inLine = true;
872
+ i++;
873
+ continue;
874
+ }
875
+ if (ch === "/" && next === "*") {
876
+ inBlock = true;
877
+ i++;
878
+ continue;
879
+ }
880
+ out += ch;
881
+ }
882
+ return out.replace(/,(\s*[}\]])/g, "$1");
883
+ }
884
+ function readConfig(path) {
885
+ let raw;
886
+ try {
887
+ raw = readFileSync(path, "utf-8");
888
+ } catch {
889
+ return null;
890
+ }
891
+ try {
892
+ const json = JSON.parse(stripJsonc(raw));
893
+ return {
894
+ dir: dirname(path),
895
+ compilerOptions: json.compilerOptions ?? {},
896
+ extends: json.extends
897
+ };
898
+ } catch {
899
+ return null;
900
+ }
901
+ }
902
+ function resolveExtendsTarget(specifier, fromDir) {
903
+ if (specifier.startsWith(".") || isAbsolute(specifier)) {
904
+ const base = resolve(fromDir, specifier);
905
+ for (const candidate of [
906
+ base,
907
+ `${base}.json`,
908
+ join(base, "tsconfig.json")
909
+ ]) if (existsSync(candidate)) return candidate;
910
+ return null;
911
+ }
912
+ try {
913
+ return __require.resolve(specifier.endsWith(".json") ? specifier : `${specifier}/tsconfig.json`, { paths: [fromDir] });
914
+ } catch {
915
+ try {
916
+ return __require.resolve(specifier, { paths: [fromDir] });
917
+ } catch {
918
+ return null;
919
+ }
920
+ }
921
+ }
922
+ /** Child-first flattened extends chain starting at `entryPath`. */
923
+ function loadChain(entryPath) {
924
+ const chain = [];
925
+ const visited = /* @__PURE__ */ new Set();
926
+ const queue = [entryPath];
927
+ while (queue.length > 0) {
928
+ const key = resolve(queue.shift());
929
+ if (visited.has(key)) continue;
930
+ visited.add(key);
931
+ const node = readConfig(key);
932
+ if (!node) continue;
933
+ chain.push(node);
934
+ const parents = Array.isArray(node.extends) ? node.extends : node.extends !== void 0 ? [node.extends] : [];
935
+ for (const parent of parents) {
936
+ if (typeof parent !== "string") continue;
937
+ const resolved = resolveExtendsTarget(parent, node.dir);
938
+ if (resolved) queue.push(resolved);
939
+ }
940
+ }
941
+ return chain;
942
+ }
943
+ /**
944
+ * Read the project's tsconfig path aliases as pairs consumable by
945
+ * `buildPathAliasesJson`. Returns an empty array when no readable tsconfig
946
+ * (or no usable `paths`) exists.
947
+ */
948
+ function readTsconfigAliasPairs(rootDir) {
949
+ const chain = loadChain(join(rootDir, "tsconfig.json"));
950
+ if (chain.length === 0) return [];
951
+ const pathsOwner = chain.find((node) => node.compilerOptions.paths && typeof node.compilerOptions.paths === "object");
952
+ if (!pathsOwner) return [];
953
+ const paths = pathsOwner.compilerOptions.paths;
954
+ const baseOwner = chain.find((node) => typeof node.compilerOptions.baseUrl === "string");
955
+ const base = baseOwner ? resolve(baseOwner.dir, baseOwner.compilerOptions.baseUrl) : pathsOwner.dir;
956
+ const pairs = [];
957
+ for (const [pattern, targets] of Object.entries(paths)) {
958
+ const target = Array.isArray(targets) ? targets[0] : void 0;
959
+ if (typeof target !== "string") continue;
960
+ const patternStars = pattern.split("*").length - 1;
961
+ const targetStars = target.split("*").length - 1;
962
+ if (pattern === "*" || patternStars > 1 || targetStars > 1) continue;
963
+ if (pattern.endsWith("/*")) {
964
+ if (!target.endsWith("/*")) continue;
965
+ pairs.push({
966
+ pattern: pattern.slice(0, -2),
967
+ target: resolve(base, target.slice(0, -2)),
968
+ kind: "prefix"
969
+ });
970
+ continue;
971
+ }
972
+ if (patternStars > 0 || targetStars > 0) continue;
973
+ pairs.push({
974
+ pattern,
975
+ target: resolve(base, target),
976
+ kind: "exact"
977
+ });
978
+ }
979
+ return pairs;
980
+ }
981
+ //#endregion
136
982
  //#region pipeline/mdx-preprocessor.ts
137
983
  /**
138
984
  * MDX source preprocessor for the extraction pipeline.
@@ -190,190 +1036,55 @@ async function preprocessMdx(source, filename) {
190
1036
  }
191
1037
  }
192
1038
  //#endregion
193
- //#region pipeline/prefix.ts
194
- /**
195
- * Apply namespace prefix to a variable map, CSS variable declarations, and theme JSON.
196
- *
197
- * Variable map: `{ "colors.ember": "--color-ember" }` -> `{ "colors.ember": "--prefix-color-ember" }`
198
- * Variable CSS: `--color-ember: #FF2800` -> `--prefix-color-ember: #FF2800`
199
- * `var(--color-ember)` -> `var(--prefix-color-ember)`
200
- * Theme JSON: `{ "colors.ember": "var(--color-ember)" }` -> `{ "colors.ember": "var(--prefix-color-ember)" }`
201
- */
202
- function applyPrefix(prefix, variableMapJson, variableCss, themeJson, contextualVarsJson) {
203
- if (!prefix) return {
204
- variableMapJson,
205
- variableCss,
206
- themeJson,
207
- contextualVarsJson
208
- };
209
- const varRefRe = /var\(--([a-zA-Z][\w-]*)\)/g;
210
- const map = JSON.parse(variableMapJson);
211
- const prefixed = {};
212
- for (const [key, varName] of Object.entries(map)) prefixed[key] = varName.startsWith("--") ? `--${prefix}-${varName.slice(2)}` : varName;
213
- let css = variableCss;
214
- css = css.replace(/--([a-zA-Z][\w-]*)\s*:/g, `--${prefix}-$1:`);
215
- css = css.replace(varRefRe, `var(--${prefix}-$1)`);
216
- const result = {
217
- variableMapJson: JSON.stringify(prefixed),
218
- variableCss: css
219
- };
220
- if (themeJson) result.themeJson = themeJson.replace(varRefRe, `var(--${prefix}-$1)`);
221
- if (contextualVarsJson) {
222
- const ctxVars = JSON.parse(contextualVarsJson);
223
- const prefixedCtx = {};
224
- for (const [scale, names] of Object.entries(ctxVars)) prefixedCtx[scale] = names.map((name) => `${prefix}-${name}`);
225
- result.contextualVarsJson = JSON.stringify(prefixedCtx);
226
- }
227
- return result;
228
- }
229
- //#endregion
230
1039
  //#region pipeline/utils.ts
231
1040
  /** Convert camelCase CSS property names to kebab-case. */
232
1041
  function camelToKebab(str) {
233
1042
  return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
234
1043
  }
235
1044
  //#endregion
236
- //#region pipeline/resolve-global-styles.ts
237
- /**
238
- * Resolve {scale.path} and {scale.path/alpha} token aliases in a CSS value string.
239
- * Mirrors the Rust theme_resolver's resolve_token_aliases logic.
240
- */
241
- function resolveTokenAliases(value, flat, variableMap) {
242
- if (!value.includes("{")) return value;
243
- return value.replace(/\{([^}]+)\}/g, (_match, content) => {
244
- const slashIdx = content.indexOf("/");
245
- const tokenPath = slashIdx >= 0 ? content.slice(0, slashIdx) : content;
246
- const alpha = slashIdx >= 0 ? Number.parseInt(content.slice(slashIdx + 1), 10) : null;
247
- const flatKey = tokenPath;
248
- let resolved;
249
- if (variableMap[flatKey]) resolved = `var(${variableMap[flatKey]})`;
250
- else if (flat[flatKey] != null) resolved = flat[flatKey];
251
- else return `{${content}}`;
252
- if (alpha === 0) return "transparent";
253
- if (alpha != null && alpha !== 100) return `color-mix(in srgb, ${resolved} ${alpha}%, transparent)`;
254
- return resolved;
255
- });
256
- }
257
- /**
258
- * Resolve a single CSS value through prop config: scale lookup, transform, token aliases.
259
- */
260
- function resolveValue(raw, config, flat, variableMap, transforms) {
261
- let resolved = String(raw);
262
- if (config?.scale) {
263
- const key = config.scale + "." + raw;
264
- if (flat[key] != null) resolved = flat[key];
265
- }
266
- if (config?.transform && transforms[config.transform]) {
267
- const fn = transforms[config.transform];
268
- const input = typeof resolved === "string" && !Number.isNaN(Number(resolved)) ? Number(resolved) : resolved;
269
- resolved = String(fn(input));
270
- }
271
- return resolveTokenAliases(resolved, flat, variableMap);
272
- }
273
- /**
274
- * Resolve a block of selectors -> style objects using prop config + theme.
275
- * @keyframes selectors are resolved with full prop config support.
276
- */
277
- function resolveBlock(selectors, propConfig, flat, variableMap, transforms) {
278
- const rules = [];
279
- for (const [selector, styleObj] of Object.entries(selectors)) {
280
- if (selector.startsWith("@keyframes")) {
281
- const frames = [];
282
- for (const [pct, frameStyles] of Object.entries(styleObj)) if (typeof frameStyles === "object" && frameStyles !== null) {
283
- const decls = [];
284
- for (const [prop, raw] of Object.entries(frameStyles)) {
285
- const cfg = propConfig[prop];
286
- const cssProps = cfg?.properties?.length ? cfg.properties : cfg ? [cfg.property] : [prop];
287
- const resolved = resolveValue(raw, cfg, flat, variableMap, transforms);
288
- for (const cssProp of cssProps) decls.push(` ${camelToKebab(cssProp)}: ${resolved};`);
289
- }
290
- frames.push(` ${pct} {\n${decls.join("\n")}\n }`);
291
- }
292
- rules.push(`${selector} {\n${frames.join("\n")}\n}`);
293
- continue;
294
- }
295
- const decls = [];
296
- for (const [prop, raw] of Object.entries(styleObj)) {
297
- const config = propConfig[prop];
298
- const cssProps = config?.properties?.length ? config.properties : config ? [config.property] : [prop];
299
- const resolved = resolveValue(raw, config, flat, variableMap, transforms);
300
- for (const cssProp of cssProps) decls.push(` ${camelToKebab(cssProp)}: ${resolved};`);
301
- }
302
- if (decls.length) rules.push(`${selector} {\n${decls.join("\n")}\n}`);
303
- }
304
- return rules.join("\n\n");
305
- }
1045
+ //#region pipeline/watch-keys.ts
306
1046
  /**
307
- * Resolve global style blocks into CSS strings.
1047
+ * Membership keys for a filesystem path in the system-dependency watch set.
308
1048
  *
309
- * Takes the global style map (block name -> selectors -> props -> values),
310
- * resolves prop shorthand, scale lookups, transforms, and token aliases,
311
- * and returns a map of block name -> resolved CSS string.
312
- */
313
- function resolveGlobalStyles(globalStyles, propConfig, flat, variableMap, transforms) {
314
- const result = {};
315
- for (const [name, block] of Object.entries(globalStyles)) {
316
- const resolved = resolveBlock(block, propConfig, flat, variableMap, transforms);
317
- if (resolved) result[name] = resolved;
318
- }
319
- return result;
1049
+ * The loader reports symlink-resolved canonical paths; watcher events carry
1050
+ * lexical absolute paths that may traverse symlinks (bun workspace links) and
1051
+ * — for deletions — may no longer exist, so `realpathSync` on the event path
1052
+ * alone cannot produce the canonical form. Both insertion and lookup go
1053
+ * through this one normalizer: the lexical form plus a canonical form
1054
+ * reconstructed through the nearest existing ancestor.
1055
+ */
1056
+ function toWatchKeys(input) {
1057
+ const lexical = normalize(input);
1058
+ const canonical = realpathThroughNearestExistingAncestor(lexical);
1059
+ return canonical === lexical ? [lexical] : [lexical, canonical];
320
1060
  }
321
- //#endregion
322
- //#region pipeline/resolve-transforms.ts
323
1061
  /**
324
- * Resolve __TRANSFORM__ placeholders in extracted CSS.
1062
+ * Canonicalize a path that may not exist: walk up to the nearest existing
1063
+ * ancestor, realpath it, and re-append the missing suffix. Handles deleted
1064
+ * files under still-existing (possibly symlinked) directories and atomic
1065
+ * unlink/rename saves. Falls back to the input when no ancestor resolves.
325
1066
  *
326
- * Pattern: `__TRANSFORM__name__rawValue__` -> transform function result.
327
- * The Rust crate emits these placeholders for CSS values that require
328
- * JS transform functions (e.g., size, grid) which can't be evaluated in Rust.
329
- */
330
- function resolveTransformPlaceholders(css, transforms) {
331
- return css.replace(/__TRANSFORM__(\w+)__(.+?)__/g, (_, name, rawValue) => {
332
- const fn = transforms[name];
333
- if (!fn) return rawValue;
334
- const result = fn(rawValue !== "" && !Number.isNaN(Number(rawValue)) ? Number(rawValue) : rawValue);
335
- return typeof result === "object" ? JSON.stringify(result) : String(result);
336
- });
337
- }
338
- //#endregion
339
- //#region pipeline/unit-fallback.ts
340
- /**
341
- * Append `px` to bare numeric values in CSS declarations for properties
342
- * that expect length units. Unitless properties are preserved as-is.
343
- * Numbers inside CSS function calls (cubic-bezier, rgb, calc, etc.) are skipped.
1067
+ * Known limit: a symlink retargeted AFTER the system load canonicalizes to
1068
+ * the new target, which cannot match the recorded (old-target) dependency —
1069
+ * the membership test then fails open (no reset), identical to not having
1070
+ * this reconstruction at all. Retarget sensitivity is a resolution-input
1071
+ * concern, deliberately out of scope for the watch set.
344
1072
  */
345
- function applyUnitFallback(css) {
346
- return css.replace(/([a-z-]+)\s*:\s*([^;{}]+);/g, (match, prop, value) => {
347
- if (UNITLESS_PROPERTIES.has(prop)) return match;
348
- let depth = 0;
349
- let fixed = "";
350
- let i = 0;
351
- while (i < value.length) if (value[i] === "(") {
352
- depth++;
353
- fixed += value[i];
354
- i++;
355
- } else if (value[i] === ")") {
356
- depth--;
357
- fixed += value[i];
358
- i++;
359
- } else if (depth > 0) {
360
- fixed += value[i];
361
- i++;
362
- } else {
363
- const numMatch = value.slice(i).match(/^(-?\d+\.?\d*)/);
364
- if (numMatch) {
365
- const num = numMatch[1];
366
- const after = value[i + num.length];
367
- if (after && /[a-z%]/i.test(after)) fixed += num;
368
- else fixed += num + "px";
369
- i += num.length;
370
- } else {
371
- fixed += value[i];
372
- i++;
373
- }
374
- }
375
- return fixed !== value ? `${prop}:${fixed};` : match;
376
- });
1073
+ function realpathThroughNearestExistingAncestor(path) {
1074
+ try {
1075
+ return realpathSync(path);
1076
+ } catch {}
1077
+ const suffix = [];
1078
+ let current = path;
1079
+ for (;;) {
1080
+ const parent = dirname$1(current);
1081
+ if (parent === current) return path;
1082
+ suffix.unshift(basename(current));
1083
+ current = parent;
1084
+ try {
1085
+ return join$1(realpathSync(current), ...suffix);
1086
+ } catch {}
1087
+ }
377
1088
  }
378
1089
  //#endregion
379
- export { ANIMUS_LAYERS, DEFAULT_EXTENSIONS, applyPrefix, applyUnitFallback, assembleStylesheet, camelToKebab, extractSystemFilePackages, preprocessMdx, resolveGlobalStyles, resolveTokenAliases, resolveTransformPlaceholders, resolveValue, stripLeadingLayerDeclaration, validateLayerOrder };
1090
+ export { ANIMUS_LAYERS, DEFAULT_EXTENSIONS, RETIRED_ENGINE_MESSAGE, applyPrefix, applyUnitFallback, assembleStylesheet, assertNoRetiredEngineSelection, buildAnalysisInputs, buildAnalyzeProjectArgs, buildDynamicPropConfig, buildPathAliasesJson, buildSystemPropsModule, camelToKebab, clearEngineCache, collectExternalPackageSources, contentHash, createV2EngineApi, discoverFiles, extractSystemFilePackages, findPackageRoot, formatRustTimingWaterfall, loadSystemConfig, postProcessCss, preprocessMdx, readTsconfigAliasPairs, resolveLightningTargets, runProjectAnalysis, serializeStaticCss, stripLeadingLayerDeclaration, surfaceManifestDiagnostics, toWatchKeys, validateLayerOrder };