@animus-ui/extract 0.1.7 → 0.1.9

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
@@ -24,13 +24,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  let crypto = require("crypto");
25
25
  let fs = require("fs");
26
26
  let path = require("path");
27
+ let node_fs = require("node:fs");
28
+ let node_path = require("node:path");
29
+ node_path = __toESM(node_path);
27
30
  let module$1 = require("module");
28
31
  let browserslist = require("browserslist");
29
32
  browserslist = __toESM(browserslist);
30
33
  let lightningcss = require("lightningcss");
31
34
  let _animus_ui_properties = require("@animus-ui/properties");
32
- let node_fs = require("node:fs");
33
- let node_path = require("node:path");
34
35
  //#region pipeline/assemble-stylesheet.ts
35
36
  /**
36
37
  * The 7 Animus cascade layers. Always prefixed with `anm-` to avoid
@@ -123,7 +124,8 @@ function buildAnalyzeProjectArgs(inputs) {
123
124
  inputs.keyframesJson,
124
125
  inputs.staticCssJson,
125
126
  inputs.conditionAliasesJson,
126
- inputs.externalDirsJson
127
+ inputs.externalDirsJson,
128
+ inputs.transformSourcesJson
127
129
  ];
128
130
  }
129
131
  //#endregion
@@ -142,7 +144,7 @@ function createV2EngineApi(deps) {
142
144
  return {
143
145
  loadSystemModule: (...args) => native.loadSystemModule(...args),
144
146
  scanKeyframesExports: (entryPath, rootDir) => native.scanKeyframesExports(entryPath, rootDir) ?? null,
145
- analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson) => {
147
+ analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson, transformSourcesJson) => {
146
148
  const filesJson = deps.rehydrateFilesJson ? deps.rehydrateFilesJson(filesJsonRaw) : filesJsonRaw;
147
149
  const sent = /* @__PURE__ */ new Map();
148
150
  for (const entry of JSON.parse(filesJson)) sent.set(entry.path, entry.source);
@@ -166,6 +168,7 @@ function createV2EngineApi(deps) {
166
168
  pathAliasesJson: pathAliasesJson ?? void 0,
167
169
  staticCssJson: staticCssJson ?? void 0,
168
170
  externalDirsJson: externalDirsJson ?? void 0,
171
+ transformSourcesJson: transformSourcesJson ?? void 0,
169
172
  devMode
170
173
  };
171
174
  const engine = new native.ExtractEngine(config);
@@ -337,6 +340,126 @@ function discoverFiles(dir, rootDir, excludePatterns, extensionsSet) {
337
340
  return results;
338
341
  }
339
342
  //#endregion
343
+ //#region pipeline/source-identity.ts
344
+ /**
345
+ * SourceId derivation authority + allowlist membership (openspec:
346
+ * external-source-watch-ingestion, design D1/D2/D5).
347
+ *
348
+ * One shared derivation produces the canonical source identity consumed by
349
+ * discovery, watch ingestion, ownership, deletion pruning, and diagnostics
350
+ * — raw event paths are lookup INPUTS only, so event spelling (symlink
351
+ * alias vs canonical, lexical vs realpath) can never fork identity.
352
+ *
353
+ * The containment and volume helpers are pure and path-API-injectable so
354
+ * Windows semantics are unit-testable via `path.win32` on any host; runtime
355
+ * callers use the ambient platform implementation.
356
+ */
357
+ /**
358
+ * Structural containment: `target` is the root itself or a descendant of it,
359
+ * decided by `relative()` shape — never by string prefixing, so `/ui` can
360
+ * never claim `/ui-old`, and a cross-drive win32 target (whose relative()
361
+ * result is absolute) is never contained.
362
+ */
363
+ function isPathWithinRoot(root, target, pathApi = node_path.default) {
364
+ const rel = pathApi.relative(root, target);
365
+ return rel === "" || !pathApi.isAbsolute(rel) && rel !== ".." && !rel.startsWith(".." + pathApi.sep);
366
+ }
367
+ /**
368
+ * Cross-volume gate (design D5): two paths share a platform path root
369
+ * (win32 drive letter or UNC share; always true on posix). Compared
370
+ * case-insensitively — win32 drive letters are case-insensitive.
371
+ */
372
+ function sharesVolumeRoot(a, b, pathApi = node_path.default) {
373
+ const volumeOf = (p) => pathApi.parse(pathApi.resolve(p)).root.toLowerCase();
374
+ return volumeOf(a) === volumeOf(b);
375
+ }
376
+ function canonicalize(p) {
377
+ try {
378
+ return (0, node_fs.realpathSync)(p);
379
+ } catch {
380
+ return null;
381
+ }
382
+ }
383
+ function createSourceIdentity(rootDir) {
384
+ const normalizedRootDir = node_path.default.normalize(rootDir);
385
+ const canonicalRootDir = canonicalize(normalizedRootDir) ?? normalizedRootDir;
386
+ const roots = [];
387
+ const aliases = /* @__PURE__ */ new Map();
388
+ const owningRecord = (canonicalPath) => {
389
+ let owner = null;
390
+ for (const record of roots) {
391
+ if (!isPathWithinRoot(record.canonical, canonicalPath)) continue;
392
+ if (!owner || record.canonical.length > owner.canonical.length) owner = record;
393
+ }
394
+ return owner;
395
+ };
396
+ return {
397
+ rootDir: normalizedRootDir,
398
+ canonicalRootDir,
399
+ registerExternalRoot(root) {
400
+ const asRegistered = node_path.default.normalize(root);
401
+ const canonical = canonicalize(asRegistered) ?? asRegistered;
402
+ const existing = roots.find((record) => record.canonical === canonical);
403
+ if (existing) return existing.canonical;
404
+ roots.push({
405
+ asRegistered,
406
+ canonical
407
+ });
408
+ return canonical;
409
+ },
410
+ externalRoots() {
411
+ return roots.map((record) => record.canonical);
412
+ },
413
+ resolveSourceId(inputPath) {
414
+ const lexical = node_path.default.normalize(inputPath);
415
+ const canonical = canonicalize(lexical);
416
+ if (canonical === null) return null;
417
+ const owner = owningRecord(canonical);
418
+ const ownedBy = (record) => {
419
+ const pathInRoot = node_path.default.relative(record.canonical, canonical);
420
+ return {
421
+ sourceKey: node_path.default.relative(normalizedRootDir, node_path.default.join(record.asRegistered, pathInRoot)),
422
+ owningRoot: record.canonical,
423
+ pathInRoot
424
+ };
425
+ };
426
+ const ownerIsMoreSpecific = owner !== null && owner.canonical.length > canonicalRootDir.length;
427
+ let resolved;
428
+ if (!ownerIsMoreSpecific && isPathWithinRoot(canonicalRootDir, canonical)) {
429
+ const key = node_path.default.relative(canonicalRootDir, canonical);
430
+ resolved = {
431
+ sourceKey: key,
432
+ owningRoot: null,
433
+ pathInRoot: key
434
+ };
435
+ } else if (owner) resolved = ownedBy(owner);
436
+ else return null;
437
+ aliases.set(lexical, resolved);
438
+ aliases.set(canonical, resolved);
439
+ return resolved;
440
+ },
441
+ resolveDeletedSourceId(inputPath) {
442
+ return aliases.get(node_path.default.normalize(inputPath)) ?? null;
443
+ },
444
+ containingExternalRoot(inputPath) {
445
+ const lexical = node_path.default.normalize(inputPath);
446
+ let owner = null;
447
+ for (const record of roots) {
448
+ if (!(isPathWithinRoot(record.canonical, lexical) || isPathWithinRoot(record.asRegistered, lexical))) continue;
449
+ if (!owner || record.canonical.length > owner.canonical.length) owner = record;
450
+ }
451
+ if (owner) return owner.canonical;
452
+ const canonical = canonicalize(lexical);
453
+ if (canonical === null) return null;
454
+ for (const record of roots) {
455
+ if (!isPathWithinRoot(record.canonical, canonical)) continue;
456
+ if (!owner || record.canonical.length > owner.canonical.length) owner = record;
457
+ }
458
+ return owner?.canonical ?? null;
459
+ }
460
+ };
461
+ }
462
+ //#endregion
340
463
  //#region pipeline/discover-packages.ts
341
464
  /**
342
465
  * Walk up from a resolved package entry file to the nearest directory
@@ -361,6 +484,20 @@ const PACKAGE_SRC_EXCLUDES = [
361
484
  ".test.",
362
485
  ".spec."
363
486
  ];
487
+ /** True when a PACKAGE-RELATIVE path matches a package-source exclusion
488
+ * fragment (the "within the package" filter — see PACKAGE_SRC_EXCLUDES). */
489
+ function isExcludedPackageRelativePath(rel) {
490
+ return PACKAGE_SRC_EXCLUDES.some((pattern) => rel.includes(pattern));
491
+ }
492
+ /**
493
+ * THE package-source walk (guardrail G1 — one policy): shared discovery with
494
+ * no consumer patterns, then exclusion by package-relative path so fragments
495
+ * in the package's own location can't blank out its sources. Used by the
496
+ * collector, hosts' dirty-root rewalks, and watch-path classification alike.
497
+ */
498
+ function walkPackageSources(packageDir, extensionsSet) {
499
+ return discoverFiles(packageDir, packageDir, [], extensionsSet).filter((absPath) => !isExcludedPackageRelativePath((0, path.relative)(packageDir, absPath)));
500
+ }
364
501
  /** Exclusions for the compiled-output fallback when a package does not ship src/. */
365
502
  const PACKAGE_OUTPUT_EXCLUDES = [
366
503
  "node_modules",
@@ -369,9 +506,9 @@ const PACKAGE_OUTPUT_EXCLUDES = [
369
506
  ".d.ts",
370
507
  ".map"
371
508
  ];
372
- const isFile = (path$2) => {
509
+ const isFile = (path$3) => {
373
510
  try {
374
- return (0, fs.statSync)(path$2).isFile();
511
+ return (0, fs.statSync)(path$3).isFile();
375
512
  } catch {
376
513
  return false;
377
514
  }
@@ -399,7 +536,7 @@ function sourceEntryForSpecifier(specifier, srcDir, extensionsSet) {
399
536
  if ((0, path.isAbsolute)(specifier)) {
400
537
  const resolved = resolveAbsolutePathSpecifier(specifier, extensionsSet);
401
538
  if (!resolved) return null;
402
- return !(0, path.relative)(srcDir, resolved).startsWith("..") ? resolved : null;
539
+ return isPathWithinRoot(srcDir, resolved) ? resolved : null;
403
540
  }
404
541
  const packageName = bareSpecifierPackageName(specifier);
405
542
  return resolveAbsolutePathSpecifier((0, path.join)(srcDir, specifier.slice(packageName.length + 1) || "index"), extensionsSet);
@@ -413,7 +550,7 @@ function sourceEntryForSpecifier(specifier, srcDir, extensionsSet) {
413
550
  * file's mtime is readable. Detection only — reporting is the caller's policy.
414
551
  */
415
552
  function distEntryIsStale(absEntry, srcDir, srcFiles) {
416
- if (!(0, path.relative)(srcDir, absEntry).startsWith("..")) return false;
553
+ if (isPathWithinRoot(srcDir, absEntry)) return false;
417
554
  let distMtime;
418
555
  try {
419
556
  distMtime = (0, fs.statSync)(absEntry).mtimeMs;
@@ -445,15 +582,20 @@ function distEntryIsStale(absEntry, srcDir, srcFiles) {
445
582
  * them — any warning or gate is the caller's policy.
446
583
  */
447
584
  async function collectExternalPackageSources(opts) {
448
- const { specifiers, resolveSpecifier, rootDir, extensionsSet, hasEntry, preprocessFile, onUnreadable } = opts;
585
+ const { specifiers, resolveSpecifier, rootDir, extensionsSet, hasEntry, preprocessFile, onUnreadable, onPackageResolved } = opts;
449
586
  const entries = [];
450
587
  const pushed = /* @__PURE__ */ new Set();
451
588
  const packageMap = {};
452
589
  const sourceEntries = /* @__PURE__ */ new Map();
590
+ const keyframesScanEntries = /* @__PURE__ */ new Map();
453
591
  const packageDirs = [];
454
- const dirOwners = {};
592
+ const dirOwnerSets = {};
593
+ const dirExtensions = {};
455
594
  const fileOwners = {};
456
595
  const outcomes = [];
596
+ const claimDir = (dir, specifier) => {
597
+ (dirOwnerSets[dir] ??= []).push(specifier);
598
+ };
457
599
  const alreadyIngested = (relPath) => hasEntry(relPath) || pushed.has(relPath);
458
600
  for (const specifier of specifiers) {
459
601
  let absEntry;
@@ -476,12 +618,15 @@ async function collectExternalPackageSources(opts) {
476
618
  let staleDist = false;
477
619
  if ((0, fs.existsSync)(srcDir)) {
478
620
  packageDirs.push(srcDir);
479
- dirOwners[srcDir] ??= specifier;
621
+ claimDir(srcDir, specifier);
622
+ dirExtensions[srcDir] = [...extensionsSet];
623
+ onPackageResolved?.(specifier, srcDir);
480
624
  const srcEntry = sourceEntryForSpecifier(specifier, srcDir, extensionsSet);
481
625
  if (srcEntry) {
482
626
  packageMap[specifier] = (0, path.relative)(rootDir, srcEntry);
483
627
  sourceEntries.set(specifier, srcEntry);
484
628
  } else packageMap[specifier] = (0, path.relative)(rootDir, absEntry);
629
+ keyframesScanEntries.set(specifier, srcEntry ?? absEntry);
485
630
  if (!(0, path.isAbsolute)(specifier)) {
486
631
  const packageName = bareSpecifierPackageName(specifier);
487
632
  if (packageName !== specifier && !(packageName in packageMap)) {
@@ -489,13 +634,11 @@ async function collectExternalPackageSources(opts) {
489
634
  if (rootEntry) {
490
635
  packageMap[packageName] = (0, path.relative)(rootDir, rootEntry);
491
636
  sourceEntries.set(packageName, rootEntry);
637
+ keyframesScanEntries.set(packageName, rootEntry);
492
638
  }
493
639
  }
494
640
  }
495
- const pkgFiles = discoverFiles(srcDir, srcDir, [], extensionsSet).filter((absPath) => {
496
- const inPkg = (0, path.relative)(srcDir, absPath);
497
- return !PACKAGE_SRC_EXCLUDES.some((pattern) => inPkg.includes(pattern));
498
- });
641
+ const pkgFiles = walkPackageSources(srcDir, extensionsSet);
499
642
  staleDist = distEntryIsStale(absEntry, srcDir, pkgFiles);
500
643
  for (const pkgFile of pkgFiles) {
501
644
  const relPath = (0, path.relative)(rootDir, pkgFile);
@@ -523,10 +666,13 @@ async function collectExternalPackageSources(opts) {
523
666
  } else {
524
667
  const outputDir = (0, path.dirname)(absEntry);
525
668
  packageDirs.push(outputDir);
526
- dirOwners[outputDir] ??= specifier;
669
+ claimDir(outputDir, specifier);
670
+ onPackageResolved?.(specifier, outputDir);
527
671
  packageMap[specifier] = (0, path.relative)(rootDir, absEntry);
672
+ keyframesScanEntries.set(specifier, absEntry);
528
673
  const outputExtensions = new Set(extensionsSet);
529
674
  outputExtensions.add((0, path.extname)(absEntry));
675
+ dirExtensions[outputDir] = [...outputExtensions];
530
676
  const outputFiles = discoverFiles(outputDir, outputDir, [], outputExtensions).filter((file) => {
531
677
  const relToOutput = (0, path.relative)(outputDir, file);
532
678
  return !PACKAGE_OUTPUT_EXCLUDES.some((pattern) => relToOutput.includes(pattern));
@@ -566,12 +712,78 @@ async function collectExternalPackageSources(opts) {
566
712
  entries,
567
713
  packageMap,
568
714
  sourceEntries,
715
+ keyframesScanEntries,
569
716
  packageDirs,
570
- dirOwners,
717
+ dirOwnerSets,
718
+ dirExtensions,
571
719
  fileOwners,
572
720
  outcomes
573
721
  };
574
722
  }
723
+ /** First-declared specifier per package dir — the single-value ownership
724
+ * view correlation consumers key on, derived from `dirOwnerSets`. */
725
+ function firstOwners(dirOwnerSets) {
726
+ const owners = {};
727
+ for (const [dir, specifiers] of Object.entries(dirOwnerSets)) if (specifiers.length > 0) owners[dir] = specifiers[0];
728
+ return owners;
729
+ }
730
+ /**
731
+ * Excise a set of rejected specifiers from a collection result ATOMICALLY
732
+ * (openspec: external-source-watch-ingestion, design D5): the returned copy
733
+ * carries no entries, package-map targets, source entries, package dirs, or
734
+ * ownership for the rejected packages — a package dir is removed only when
735
+ * EVERY specifier that claimed it is rejected (a dir-level rejection, such
736
+ * as the cross-volume gate, always rejects them together). Outcome records
737
+ * are reporting inputs, not membership, and pass through untouched.
738
+ */
739
+ function excludeCollectedPackages(collected, rejectedSpecifiers, rootDir) {
740
+ if (rejectedSpecifiers.size === 0) return collected;
741
+ const rejectedDirs = Object.entries(collected.dirOwnerSets).filter(([, specs]) => specs.every((s) => rejectedSpecifiers.has(s))).map(([dir]) => dir);
742
+ const underRejectedDir = (absPath) => rejectedDirs.some((dir) => isPathWithinRoot(dir, absPath));
743
+ const targetRejected = (specifier, absTarget) => rejectedSpecifiers.has(specifier) || underRejectedDir(absTarget);
744
+ const packageMap = {};
745
+ for (const [specifier, relTarget] of Object.entries(collected.packageMap)) {
746
+ if (targetRejected(specifier, (0, path.resolve)(rootDir, relTarget))) continue;
747
+ packageMap[specifier] = relTarget;
748
+ }
749
+ const sourceEntries = /* @__PURE__ */ new Map();
750
+ for (const [specifier, absEntry] of collected.sourceEntries) {
751
+ if (targetRejected(specifier, absEntry)) continue;
752
+ sourceEntries.set(specifier, absEntry);
753
+ }
754
+ const keyframesScanEntries = /* @__PURE__ */ new Map();
755
+ for (const [specifier, absEntry] of collected.keyframesScanEntries) {
756
+ if (targetRejected(specifier, absEntry)) continue;
757
+ keyframesScanEntries.set(specifier, absEntry);
758
+ }
759
+ const dirOwnerSets = {};
760
+ for (const [dir, specs] of Object.entries(collected.dirOwnerSets)) {
761
+ const kept = specs.filter((s) => !rejectedSpecifiers.has(s));
762
+ if (kept.length === 0) continue;
763
+ dirOwnerSets[dir] = kept;
764
+ }
765
+ const dirExtensions = {};
766
+ for (const [dir, exts] of Object.entries(collected.dirExtensions)) {
767
+ if (rejectedDirs.includes(dir)) continue;
768
+ dirExtensions[dir] = exts;
769
+ }
770
+ const fileOwners = {};
771
+ for (const [relPath, owner] of Object.entries(collected.fileOwners)) {
772
+ if (rejectedSpecifiers.has(owner)) continue;
773
+ fileOwners[relPath] = owner;
774
+ }
775
+ return {
776
+ entries: collected.entries.filter((entry) => !rejectedSpecifiers.has(collected.fileOwners[entry.path])),
777
+ packageMap,
778
+ sourceEntries,
779
+ keyframesScanEntries,
780
+ packageDirs: collected.packageDirs.filter((dir) => !rejectedDirs.includes(dir)),
781
+ dirOwnerSets,
782
+ dirExtensions,
783
+ fileOwners,
784
+ outcomes: collected.outcomes
785
+ };
786
+ }
575
787
  /**
576
788
  * Extract external DS package names from inheritance declarations in the
577
789
  * system file.
@@ -941,7 +1153,7 @@ function buildSourceTokenIndex(opts) {
941
1153
  if ((0, fs.existsSync)((0, path.join)(packageRoot, "package.json"))) addOwnerDir(packageRoot, specifier);
942
1154
  }
943
1155
  for (const [modulePath, exports] of Object.entries(manifests)) {
944
- const owner = realDirOwners.find(({ dir }) => modulePath === dir || modulePath.startsWith(dir + path.sep));
1156
+ const owner = realDirOwners.find(({ dir }) => isPathWithinRoot(dir, modulePath));
945
1157
  if (!owner) continue;
946
1158
  let tokens = index.get(owner.specifier);
947
1159
  if (!tokens) {
@@ -1082,16 +1294,42 @@ function postProcessCss(css, opts) {
1082
1294
  /** Stable code for selector forms with no substitutable subject.
1083
1295
  * Mirrors the Rust constant in `extract-v2/src/eval.rs`. */
1084
1296
  const SELECTOR_UNSUPPORTED_SUBJECT = "animus.selector.unsupported-subject";
1297
+ /** Unresolved-parent chain-drop message (mirrors the Rust diagnostic in
1298
+ * `extract-v2` — the ONE encoding of this message shape; consumers match
1299
+ * through the helpers below, never their own regex copies). */
1300
+ const UNRESOLVED_PARENT_RE = /chain dropped: could not resolve parent component '([^']+)'/;
1301
+ /** True when the diagnostic reports a chain dropped for an unresolved
1302
+ * parent component. */
1303
+ function isUnresolvedParentDrop(diagnostic) {
1304
+ return typeof diagnostic?.message === "string" && UNRESOLVED_PARENT_RE.test(diagnostic.message);
1305
+ }
1306
+ /** The parent binding named by an unresolved-parent drop, or null when the
1307
+ * diagnostic is not one. */
1308
+ function unresolvedParentName(diagnostic) {
1309
+ if (typeof diagnostic?.message !== "string") return null;
1310
+ return UNRESOLVED_PARENT_RE.exec(diagnostic.message)?.[1] ?? null;
1311
+ }
1085
1312
  /** True when the selector string carries at least one substitutable `&`
1086
1313
  * subject outside quoted text (mirrors the Rust `selector_subject` walk —
1087
1314
  * ancestor, leading, and repeated subjects all count; a `&` inside a
1088
1315
  * quoted attribute value does not). */
1089
1316
  function hasSelectorSubject(value) {
1090
1317
  let quote = null;
1091
- for (const c of value) if (quote !== null) {
1092
- if (c === quote) quote = null;
1093
- } else if (c === "\"" || c === "'") quote = c;
1094
- else if (c === "&") return true;
1318
+ let escaped = false;
1319
+ for (const c of value) {
1320
+ if (escaped) {
1321
+ escaped = false;
1322
+ continue;
1323
+ }
1324
+ if (c === "\\") {
1325
+ escaped = true;
1326
+ continue;
1327
+ }
1328
+ if (quote !== null) {
1329
+ if (c === quote) quote = null;
1330
+ } else if (c === "\"" || c === "'") quote = c;
1331
+ else if (c === "&") return true;
1332
+ }
1095
1333
  return false;
1096
1334
  }
1097
1335
  /**
@@ -1221,6 +1459,7 @@ function buildAnalysisInputs(opts) {
1221
1459
  keyframesJson: opts.system.keyframesJson,
1222
1460
  staticCssJson: opts.staticCssJson ?? null,
1223
1461
  conditionAliasesJson: opts.system.conditionAliasesJson ?? null,
1462
+ transformSourcesJson: opts.system.transformSourcesJson ?? null,
1224
1463
  externalDirsJson: opts.externalDirs?.length && hasSourceThemeManifests(opts.system) ? JSON.stringify(opts.externalDirs) : null
1225
1464
  };
1226
1465
  }
@@ -1362,6 +1601,7 @@ function loadSystemConfig(engineApi, opts) {
1362
1601
  contextualVarsJson,
1363
1602
  selectorAliasesJson: config.selectorAliases || null,
1364
1603
  conditionAliasesJson: config.conditionAliases || null,
1604
+ transformSourcesJson: config.transformSources || null,
1365
1605
  globalStyleBlocksJson: config.globalStyleBlocks || null,
1366
1606
  keyframesJson: config.keyframesBlocks || null,
1367
1607
  dependencies: config.dependencies ?? [],
@@ -1509,17 +1749,17 @@ function stripJsonc(text) {
1509
1749
  }
1510
1750
  return out.replace(/,(\s*[}\]])/g, "$1");
1511
1751
  }
1512
- function readConfig(path$1) {
1752
+ function readConfig(path$2) {
1513
1753
  let raw;
1514
1754
  try {
1515
- raw = (0, fs.readFileSync)(path$1, "utf-8");
1755
+ raw = (0, fs.readFileSync)(path$2, "utf-8");
1516
1756
  } catch {
1517
1757
  return null;
1518
1758
  }
1519
1759
  try {
1520
1760
  const json = JSON.parse(stripJsonc(raw));
1521
1761
  return {
1522
- dir: (0, path.dirname)(path$1),
1762
+ dir: (0, path.dirname)(path$2),
1523
1763
  compilerOptions: json.compilerOptions ?? {},
1524
1764
  extends: json.extends
1525
1765
  };
@@ -1607,6 +1847,22 @@ function readTsconfigAliasPairs(rootDir) {
1607
1847
  return pairs;
1608
1848
  }
1609
1849
  //#endregion
1850
+ //#region pipeline/error-diagnostics.ts
1851
+ /**
1852
+ * Throw when the diagnostics carry any `kind: "error"` entry — one
1853
+ * aggregated `Error` naming EVERY offender (component, file, message — one
1854
+ * line per entry, each prefixed `[animus]`). Warning-only diagnostics
1855
+ * (`bail`/`skip`/`warn`, any severity) never trip this gate; their routing
1856
+ * stays with `surfaceManifestDiagnostics`. Callers accept the manifest only
1857
+ * past this point, so no stylesheet from a failing analysis is published.
1858
+ */
1859
+ function assertNoErrorDiagnostics(diagnostics) {
1860
+ const errors = (diagnostics ?? []).filter((diagnostic) => diagnostic.kind === "error");
1861
+ if (errors.length === 0) return;
1862
+ const lines = [...new Set(errors.map((diagnostic) => `[animus] ${diagnostic.component || "<unknown component>"} (${diagnostic.file || "<unknown file>"}): ${diagnostic.message || "<no message>"}`))];
1863
+ throw new Error(lines.join("\n"));
1864
+ }
1865
+ //#endregion
1610
1866
  //#region pipeline/mdx-preprocessor.ts
1611
1867
  /**
1612
1868
  * MDX source preprocessor for the extraction pipeline.
@@ -1664,6 +1920,79 @@ async function preprocessMdx(source, filename) {
1664
1920
  }
1665
1921
  }
1666
1922
  //#endregion
1923
+ //#region pipeline/replacement-plans.ts
1924
+ const snapshotByManifest = /* @__PURE__ */ new WeakMap();
1925
+ function snapshotFilePlans(manifest) {
1926
+ const memoKey = manifest !== null && typeof manifest === "object" ? manifest : null;
1927
+ if (memoKey) {
1928
+ const cached = snapshotByManifest.get(memoKey);
1929
+ if (cached) return cached;
1930
+ }
1931
+ const entriesByFile = /* @__PURE__ */ new Map();
1932
+ for (const [id, desc] of Object.entries(manifest?.components ?? {})) {
1933
+ const file = desc.file;
1934
+ if (!file) continue;
1935
+ const list = entriesByFile.get(file) ?? [];
1936
+ list.push([id, desc.replacement ?? ""]);
1937
+ entriesByFile.set(file, list);
1938
+ }
1939
+ const snapshot = /* @__PURE__ */ new Map();
1940
+ for (const [file, entries] of entriesByFile) {
1941
+ entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1942
+ snapshot.set(file, JSON.stringify(entries));
1943
+ }
1944
+ if (memoKey) snapshotByManifest.set(memoKey, snapshot);
1945
+ return snapshot;
1946
+ }
1947
+ /**
1948
+ * Rel paths whose plan changed between two snapshots: absent→present,
1949
+ * present→absent, and any change to membership or replacement content.
1950
+ * `exclude` drops one path (the changed file itself on the edit path — its
1951
+ * own modules are already in the update set).
1952
+ */
1953
+ function diffFilePlans(prev, next, opts) {
1954
+ const changed = /* @__PURE__ */ new Set();
1955
+ for (const [file, plan] of next) if (prev.get(file) !== plan) changed.add(file);
1956
+ for (const file of prev.keys()) if (!next.has(file)) changed.add(file);
1957
+ if (opts?.exclude) changed.delete(opts.exclude);
1958
+ return [...changed].sort();
1959
+ }
1960
+ /** Domain prefix keeping the epoch hash disjoint from every other
1961
+ * contentHash use in the pipeline (design D5). */
1962
+ const REPLACEMENT_PLANS_DOMAIN = "animus-replacement-plans-v1\0";
1963
+ /** Section marker for the served-dependency witness. Leads with NUL: every
1964
+ * plan entry ends `…\0` and neither file paths nor plan JSON contain NUL,
1965
+ * so `\0\0` is unreachable from entry serialization alone — a snapshot can
1966
+ * never forge the witness section. */
1967
+ const SERVED_DEPENDENCY_DOMAIN = "\0animus-served-dependency-v1\0";
1968
+ /**
1969
+ * Canonical replacement epoch: contentHash over the domain-prefixed, sorted
1970
+ * canonical serialization of a file-plan snapshot — each entry contributes
1971
+ * `file + '\0' + plan + '\0'` (unambiguous: neither file paths nor the
1972
+ * JSON plan strings contain NUL). Byte-identical plans → identical epoch;
1973
+ * any membership/replacement/absent↔present change moves it.
1974
+ *
1975
+ * `servedDependencyWitness` extends the epoch to content the plans cannot
1976
+ * see but restored modules depend on: transformed modules import the
1977
+ * session's served system-props artifact, so a consumer passes that
1978
+ * artifact's content here and an offline change to it moves the epoch —
1979
+ * invalidating persistent-cache snapshots that reference the old epoch
1980
+ * artifact. Absence and empty-string are distinct values.
1981
+ *
1982
+ * This is THE epoch definition — integrations consume this hash (or the
1983
+ * snapshot diff) and never define their own. Epoch values compare only for
1984
+ * equality (a moved value invalidates), so the serialization is free to
1985
+ * evolve with a domain bump: sessions running different serializations
1986
+ * simply disagree once and rebuild.
1987
+ */
1988
+ function hashReplacementPlans(snapshot, servedDependencyWitness) {
1989
+ const entries = [...snapshot.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1990
+ let serialized = REPLACEMENT_PLANS_DOMAIN;
1991
+ for (const [file, plan] of entries) serialized += file + "\0" + plan + "\0";
1992
+ if (servedDependencyWitness !== void 0) serialized += SERVED_DEPENDENCY_DOMAIN + servedDependencyWitness;
1993
+ return contentHash(serialized);
1994
+ }
1995
+ //#endregion
1667
1996
  //#region pipeline/utils.ts
1668
1997
  /** Convert camelCase CSS property names to kebab-case. */
1669
1998
  function camelToKebab(str) {
@@ -1723,6 +2052,7 @@ exports.RETIRED_ENGINE_MESSAGE = RETIRED_ENGINE_MESSAGE;
1723
2052
  exports.applyPrefix = applyPrefix;
1724
2053
  exports.applyUnitFallback = applyUnitFallback;
1725
2054
  exports.assembleStylesheet = assembleStylesheet;
2055
+ exports.assertNoErrorDiagnostics = assertNoErrorDiagnostics;
1726
2056
  exports.assertNoRetiredEngineSelection = assertNoRetiredEngineSelection;
1727
2057
  exports.buildAnalysisInputs = buildAnalysisInputs;
1728
2058
  exports.buildAnalyzeProjectArgs = buildAnalyzeProjectArgs;
@@ -1733,27 +2063,40 @@ exports.camelToKebab = camelToKebab;
1733
2063
  exports.clearEngineCache = clearEngineCache;
1734
2064
  exports.collectExternalPackageSources = collectExternalPackageSources;
1735
2065
  exports.contentHash = contentHash;
2066
+ exports.createSourceIdentity = createSourceIdentity;
1736
2067
  exports.createV2EngineApi = createV2EngineApi;
2068
+ exports.diffFilePlans = diffFilePlans;
1737
2069
  exports.discoverFiles = discoverFiles;
1738
2070
  exports.enforceExternalTokenContracts = enforceExternalTokenContracts;
2071
+ exports.excludeCollectedPackages = excludeCollectedPackages;
1739
2072
  exports.extractSystemFilePackages = extractSystemFilePackages;
1740
2073
  exports.findAssetSpecifiers = findAssetSpecifiers;
1741
2074
  exports.findPackageRoot = findPackageRoot;
2075
+ exports.firstOwners = firstOwners;
1742
2076
  exports.formatRustTimingWaterfall = formatRustTimingWaterfall;
2077
+ exports.hashReplacementPlans = hashReplacementPlans;
2078
+ exports.isExcludedPackageRelativePath = isExcludedPackageRelativePath;
2079
+ exports.isPathWithinRoot = isPathWithinRoot;
2080
+ exports.isUnresolvedParentDrop = isUnresolvedParentDrop;
1743
2081
  exports.loadSystemConfig = loadSystemConfig;
1744
2082
  exports.mergeExternalKeyframes = mergeExternalKeyframes;
1745
2083
  exports.postProcessCss = postProcessCss;
1746
2084
  exports.preprocessMdx = preprocessMdx;
1747
2085
  exports.readTsconfigAliasPairs = readTsconfigAliasPairs;
2086
+ exports.resolveAbsolutePathSpecifier = resolveAbsolutePathSpecifier;
1748
2087
  exports.resolveAssetFile = resolveAssetFile;
1749
2088
  exports.resolveLightningTargets = resolveLightningTargets;
1750
2089
  exports.resolveThroughPathAliases = resolveThroughPathAliases;
1751
2090
  exports.runProjectAnalysis = runProjectAnalysis;
1752
2091
  exports.serializeStaticCss = serializeStaticCss;
2092
+ exports.sharesVolumeRoot = sharesVolumeRoot;
2093
+ exports.snapshotFilePlans = snapshotFilePlans;
1753
2094
  exports.staleDistIncludesMessage = staleDistIncludesMessage;
1754
2095
  exports.stripLeadingLayerDeclaration = stripLeadingLayerDeclaration;
1755
2096
  exports.substituteAssetPlaceholders = substituteAssetPlaceholders;
1756
2097
  exports.surfaceManifestDiagnostics = surfaceManifestDiagnostics;
1757
2098
  exports.toWatchKeys = toWatchKeys;
1758
2099
  exports.unresolvableIncludesMessage = unresolvableIncludesMessage;
2100
+ exports.unresolvedParentName = unresolvedParentName;
1759
2101
  exports.validateLayerOrder = validateLayerOrder;
2102
+ exports.walkPackageSources = walkPackageSources;
package/dist/index.d.ts CHANGED
@@ -10,7 +10,9 @@ export { KEYFRAMES_EXPORT_COLLISION, KEYFRAMES_EXTERNAL_ENTRY_FAILED, mergeExter
10
10
  export type { ExternalKeyframesMerge } from './external-keyframes';
11
11
  export { discoverFiles } from './discover-files';
12
12
  export type { CollectedExternalPackages, ExternalPackageOutcome, } from './discover-packages';
13
- export { collectExternalPackageSources, extractSystemFilePackages, findPackageRoot, staleDistIncludesMessage, unresolvableIncludesMessage, } from './discover-packages';
13
+ export { collectExternalPackageSources, excludeCollectedPackages, extractSystemFilePackages, findPackageRoot, firstOwners, isExcludedPackageRelativePath, resolveAbsolutePathSpecifier, staleDistIncludesMessage, unresolvableIncludesMessage, walkPackageSources, } from './discover-packages';
14
+ export type { ResolvedSourceId, SourceIdentity } from './source-identity';
15
+ export { createSourceIdentity, isPathWithinRoot, sharesVolumeRoot, } from './source-identity';
14
16
  export { findAssetSpecifiers, substituteAssetPlaceholders, } from './asset-placeholders';
15
17
  export { resolveAssetFile, resolveThroughPathAliases } from './resolve-asset';
16
18
  export { enforceExternalTokenContracts } from './correlate-external-tokens';
@@ -28,11 +30,15 @@ export { formatRustTimingWaterfall } from './timing-waterfall';
28
30
  export { readTsconfigAliasPairs } from './tsconfig-paths';
29
31
  export type { DynamicPropConfigEntry, DynamicPropMeta, } from './dynamic-prop-config';
30
32
  export { buildDynamicPropConfig } from './dynamic-prop-config';
33
+ export type { CssDiagnosticLike } from './error-diagnostics';
34
+ export { assertNoErrorDiagnostics } from './error-diagnostics';
31
35
  export type { ManifestDiagnostic } from './manifest-diagnostics';
32
- export { surfaceManifestDiagnostics } from './manifest-diagnostics';
36
+ export { isUnresolvedParentDrop, surfaceManifestDiagnostics, unresolvedParentName, } from './manifest-diagnostics';
33
37
  export type { DefaultExtension, PreprocessMdxResult } from './mdx-preprocessor';
34
38
  export { DEFAULT_EXTENSIONS, preprocessMdx } from './mdx-preprocessor';
35
39
  export { applyPrefix } from './prefix';
40
+ export type { FilePlanSnapshot } from './replacement-plans';
41
+ export { diffFilePlans, hashReplacementPlans, snapshotFilePlans, } from './replacement-plans';
36
42
  export { applyUnitFallback } from './unit-fallback';
37
43
  export { camelToKebab } from './utils';
38
44
  export { toWatchKeys } from './watch-keys';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../pipeline/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../pipeline/index.ts"],"names":[],"mappings":"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"}