@animus-ui/extract 0.1.3 → 0.1.4

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.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { createHash } from "crypto";
3
- import { existsSync, readFileSync, readdirSync, statSync } from "fs";
4
- import { dirname, extname, isAbsolute, join, relative, resolve } from "path";
3
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "fs";
4
+ import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "path";
5
+ import { createRequire as createRequire$1 } from "module";
5
6
  import browserslist from "browserslist";
6
7
  import { browserslistToTargets, transform } from "lightningcss";
7
8
  import { UNITLESS_PROPERTIES } from "@animus-ui/properties";
8
- import { realpathSync } from "node:fs";
9
+ import { realpathSync as realpathSync$1 } from "node:fs";
9
10
  import { basename, dirname as dirname$1, join as join$1, normalize } from "node:path";
10
11
  //#region \0rolldown/runtime.js
11
12
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
@@ -101,7 +102,8 @@ function buildAnalyzeProjectArgs(inputs) {
101
102
  inputs.pathAliasesJson,
102
103
  inputs.keyframesJson,
103
104
  inputs.staticCssJson,
104
- inputs.conditionAliasesJson
105
+ inputs.conditionAliasesJson,
106
+ inputs.externalDirsJson
105
107
  ];
106
108
  }
107
109
  //#endregion
@@ -119,7 +121,7 @@ function createV2EngineApi(deps) {
119
121
  const native = loadNativeEngine();
120
122
  return {
121
123
  loadSystemModule: (...args) => native.loadSystemModule(...args),
122
- analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson) => {
124
+ analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson) => {
123
125
  const filesJson = deps.rehydrateFilesJson ? deps.rehydrateFilesJson(filesJsonRaw) : filesJsonRaw;
124
126
  const sent = /* @__PURE__ */ new Map();
125
127
  for (const entry of JSON.parse(filesJson)) sent.set(entry.path, entry.source);
@@ -142,6 +144,7 @@ function createV2EngineApi(deps) {
142
144
  packageResolutionJson: packageResolutionJson ?? void 0,
143
145
  pathAliasesJson: pathAliasesJson ?? void 0,
144
146
  staticCssJson: staticCssJson ?? void 0,
147
+ externalDirsJson: externalDirsJson ?? void 0,
145
148
  devMode
146
149
  };
147
150
  const engine = new native.ExtractEngine(config);
@@ -200,8 +203,10 @@ function assertNoRetiredEngineSelection(engineOption) {
200
203
  //#region pipeline/content-hash.ts
201
204
  /**
202
205
  * 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.
206
+ * extraction plugins, and for content-addressing copied asset bytes. The
207
+ * algorithm/encoding is a cross-plugin contract: cache keys written by one
208
+ * build path must compare equal in the next. Accepts a Buffer directly so
209
+ * binary sources hash without an intermediate string copy.
205
210
  */
206
211
  function contentHash(source) {
207
212
  return createHash("md5").update(source).digest("hex");
@@ -264,6 +269,14 @@ const PACKAGE_SRC_EXCLUDES = [
264
269
  ".test.",
265
270
  ".spec."
266
271
  ];
272
+ /** Exclusions for the compiled-output fallback when a package does not ship src/. */
273
+ const PACKAGE_OUTPUT_EXCLUDES = [
274
+ "node_modules",
275
+ ".test.",
276
+ ".spec.",
277
+ ".d.ts",
278
+ ".map"
279
+ ];
267
280
  const isFile = (path) => {
268
281
  try {
269
282
  return statSync(path).isFile();
@@ -285,6 +298,42 @@ function resolveAbsolutePathSpecifier(absSpecifier, extensionsSet) {
285
298
  ...Array.from(extensionsSet, (ext) => join(absSpecifier, `index${ext}`))
286
299
  ].find(isFile) ?? null;
287
300
  }
301
+ /** The package-name half of a bare specifier (`@scope/name/sub` → `@scope/name`). */
302
+ function bareSpecifierPackageName(specifier) {
303
+ const segments = specifier.split("/");
304
+ return specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
305
+ }
306
+ function sourceEntryForSpecifier(specifier, srcDir, extensionsSet) {
307
+ if (isAbsolute(specifier)) {
308
+ const resolved = resolveAbsolutePathSpecifier(specifier, extensionsSet);
309
+ if (!resolved) return null;
310
+ return !relative(srcDir, resolved).startsWith("..") ? resolved : null;
311
+ }
312
+ const packageName = bareSpecifierPackageName(specifier);
313
+ return resolveAbsolutePathSpecifier(join(srcDir, specifier.slice(packageName.length + 1) || "index"), extensionsSet);
314
+ }
315
+ /**
316
+ * The D13 dist-freshness check: applicable only when a specifier has BOTH a
317
+ * walked src/ tree and a resolved dist entry that exists on disk outside that
318
+ * tree; stale when the dist entry's mtime is older than the newest walked
319
+ * source file's. Not applicable (never stale) when the entry resolves inside
320
+ * src/ (no dist to skew), when the entry file is missing, or when no source
321
+ * file's mtime is readable. Detection only — reporting is the caller's policy.
322
+ */
323
+ function distEntryIsStale(absEntry, srcDir, srcFiles) {
324
+ if (!relative(srcDir, absEntry).startsWith("..")) return false;
325
+ let distMtime;
326
+ try {
327
+ distMtime = statSync(absEntry).mtimeMs;
328
+ } catch {
329
+ return false;
330
+ }
331
+ let newestSrcMtime = -Infinity;
332
+ for (const srcFile of srcFiles) try {
333
+ newestSrcMtime = Math.max(newestSrcMtime, statSync(srcFile).mtimeMs);
334
+ } catch {}
335
+ return distMtime < newestSrcMtime;
336
+ }
288
337
  /**
289
338
  * Shared external-package source collection (spec:
290
339
  * external-package-file-discovery), consumed by both extraction plugins.
@@ -310,6 +359,8 @@ async function collectExternalPackageSources(opts) {
310
359
  const packageMap = {};
311
360
  const sourceEntries = /* @__PURE__ */ new Map();
312
361
  const packageDirs = [];
362
+ const dirOwners = {};
363
+ const fileOwners = {};
313
364
  const outcomes = [];
314
365
  const alreadyIngested = (relPath) => hasEntry(relPath) || pushed.has(relPath);
315
366
  for (const specifier of specifiers) {
@@ -330,17 +381,30 @@ async function collectExternalPackageSources(opts) {
330
381
  }
331
382
  const srcDir = join(findPackageRoot(absEntry), "src");
332
383
  let fileCount = 0;
384
+ let staleDist = false;
333
385
  if (existsSync(srcDir)) {
334
386
  packageDirs.push(srcDir);
335
- const srcEntry = join(srcDir, "index.ts");
336
- if (existsSync(srcEntry)) {
387
+ dirOwners[srcDir] ??= specifier;
388
+ const srcEntry = sourceEntryForSpecifier(specifier, srcDir, extensionsSet);
389
+ if (srcEntry) {
337
390
  packageMap[specifier] = relative(rootDir, srcEntry);
338
391
  sourceEntries.set(specifier, srcEntry);
339
392
  } else packageMap[specifier] = relative(rootDir, absEntry);
393
+ if (!isAbsolute(specifier)) {
394
+ const packageName = bareSpecifierPackageName(specifier);
395
+ if (packageName !== specifier && !(packageName in packageMap)) {
396
+ const rootEntry = sourceEntryForSpecifier(packageName, srcDir, extensionsSet);
397
+ if (rootEntry) {
398
+ packageMap[packageName] = relative(rootDir, rootEntry);
399
+ sourceEntries.set(packageName, rootEntry);
400
+ }
401
+ }
402
+ }
340
403
  const pkgFiles = discoverFiles(srcDir, srcDir, [], extensionsSet).filter((absPath) => {
341
404
  const inPkg = relative(srcDir, absPath);
342
405
  return !PACKAGE_SRC_EXCLUDES.some((pattern) => inPkg.includes(pattern));
343
406
  });
407
+ staleDist = distEntryIsStale(absEntry, srcDir, pkgFiles);
344
408
  for (const pkgFile of pkgFiles) {
345
409
  const relPath = relative(rootDir, pkgFile);
346
410
  if (alreadyIngested(relPath)) {
@@ -361,28 +425,48 @@ async function collectExternalPackageSources(opts) {
361
425
  source: processed.source
362
426
  });
363
427
  pushed.add(processed.relPath);
428
+ fileOwners[processed.relPath] ??= specifier;
364
429
  fileCount++;
365
430
  }
366
431
  } 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");
432
+ const outputDir = dirname(absEntry);
433
+ packageDirs.push(outputDir);
434
+ dirOwners[outputDir] ??= specifier;
435
+ packageMap[specifier] = relative(rootDir, absEntry);
436
+ const outputExtensions = new Set(extensionsSet);
437
+ outputExtensions.add(extname(absEntry));
438
+ const outputFiles = discoverFiles(outputDir, outputDir, [], outputExtensions).filter((file) => {
439
+ const relToOutput = relative(outputDir, file);
440
+ return !PACKAGE_OUTPUT_EXCLUDES.some((pattern) => relToOutput.includes(pattern));
441
+ });
442
+ if (!outputFiles.includes(absEntry)) outputFiles.unshift(absEntry);
443
+ for (const outputFile of outputFiles) {
444
+ const outputRelPath = relative(rootDir, outputFile);
445
+ if (alreadyIngested(outputRelPath)) {
446
+ fileCount++;
447
+ continue;
448
+ }
449
+ let source;
450
+ try {
451
+ source = readFileSync(outputFile, "utf-8");
452
+ } catch (err) {
453
+ onUnreadable(outputRelPath, err);
454
+ continue;
455
+ }
456
+ const processed = await preprocessFile(source, outputRelPath, outputFile);
457
+ if (!processed) continue;
373
458
  entries.push({
374
- path: relPath,
375
- source
459
+ path: processed.relPath,
460
+ source: processed.source
376
461
  });
377
- pushed.add(relPath);
462
+ pushed.add(processed.relPath);
463
+ fileOwners[processed.relPath] ??= specifier;
378
464
  fileCount++;
379
- } catch (err) {
380
- onUnreadable(relPath, err);
381
465
  }
382
466
  }
383
467
  outcomes.push({
384
468
  specifier,
385
- outcome: fileCount > 0 ? "resolved" : "empty",
469
+ outcome: staleDist ? "stale-dist" : fileCount > 0 ? "resolved" : "empty",
386
470
  fileCount
387
471
  });
388
472
  }
@@ -391,24 +475,33 @@ async function collectExternalPackageSources(opts) {
391
475
  packageMap,
392
476
  sourceEntries,
393
477
  packageDirs,
478
+ dirOwners,
479
+ fileOwners,
394
480
  outcomes
395
481
  };
396
482
  }
397
483
  /**
398
- * Extract external DS package names from `includes` declarations in the system file.
484
+ * Extract external DS package names from inheritance declarations in the
485
+ * system file.
399
486
  *
400
- * Supports two forms:
401
- * - Primary (1.0+): `createSystem({ includes: [identifier, ...] })` constructor arg
402
- * - Legacy: `.includes([identifier, ...])` chain method (RC migration fallback)
487
+ * Supports four forms:
488
+ * - Primary: `createSystem(...).extend(identifier)` chain calls
489
+ * (repeatable, mixable with `.from()` links; each call
490
+ * contributes one source; a library-bundle identifier
491
+ * traces its base import)
492
+ * - Deprecated chain: `createSystem(...).from(identifier)` chain calls
493
+ * - Deprecated alias: `createSystem({ includes: [identifier, ...] })`
494
+ * - Legacy: `.includes([identifier, ...])` chain method (RC
495
+ * migration fallback)
403
496
  *
404
497
  * For each identifier found, traces back to its import declaration and returns
405
498
  * the import specifier: a bare specifier normalized to its package name, a
406
499
  * relative specifier resolved against the system file's directory into an
407
500
  * 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.
501
+ * too). Only packages explicitly declared through one of these forms are
502
+ * treated as external DS dependencies.
410
503
  *
411
- * Falls back to empty array if no `includes` declaration is found.
504
+ * Falls back to empty array if no declaration is found.
412
505
  */
413
506
  /**
414
507
  * The message for the strict/warn gate over unresolvable includes, or null
@@ -421,6 +514,18 @@ function unresolvableIncludesMessage(outcomes) {
421
514
  if (unresolvable.length === 0) return null;
422
515
  return `[animus-extract] unresolvable include specifier(s): ${unresolvable.join(", ")}`;
423
516
  }
517
+ /**
518
+ * The message for the strict/warn gate over stale dist entries
519
+ * (first-class-extension D13), or null when no declared specifier is stale.
520
+ * A stale dist silently skews merged registry content while discovery
521
+ * compiles the fresh sources, so it surfaces like an unresolvable specifier:
522
+ * non-strict consumers warn with this line, strict consumers throw it.
523
+ */
524
+ function staleDistIncludesMessage(outcomes) {
525
+ const stale = outcomes.filter((record) => record.outcome === "stale-dist").map((record) => record.specifier);
526
+ if (stale.length === 0) return null;
527
+ return `[animus-extract] stale dist for include specifier(s): ${stale.join(", ")} — dist entry is older than the newest src/ file; rebuild the package(s) before extracting`;
528
+ }
424
529
  function extractSystemFilePackages(systemFilePath) {
425
530
  let source;
426
531
  try {
@@ -443,6 +548,94 @@ function extractSystemFilePackages(systemFilePath) {
443
548
  };
444
549
  collectIdentifiers(constructorRegex);
445
550
  collectIdentifiers(chainRegex);
551
+ const IDENT_START_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*/;
552
+ /** Position after any run of whitespace, line comments, block comments. */
553
+ const skipTrivia = (from) => {
554
+ let pos = from;
555
+ for (;;) {
556
+ while (pos < source.length && /\s/.test(source[pos])) pos++;
557
+ if (source.startsWith("//", pos)) {
558
+ const newline = source.indexOf("\n", pos);
559
+ pos = newline === -1 ? source.length : newline + 1;
560
+ continue;
561
+ }
562
+ if (source.startsWith("/*", pos)) {
563
+ const close = source.indexOf("*/", pos + 2);
564
+ pos = close === -1 ? source.length : close + 2;
565
+ continue;
566
+ }
567
+ return pos;
568
+ }
569
+ };
570
+ /**
571
+ * Consume consecutive extend/from links starting at `from`, collecting
572
+ * each link's base identifier; tolerates trivia between tokens and a
573
+ * trailing comma in the argument list. Returns the position after the
574
+ * last consumed link (`from` itself when none matched).
575
+ */
576
+ const consumeChainLinks = (from) => {
577
+ let pos = from;
578
+ for (;;) {
579
+ let cursor = skipTrivia(pos);
580
+ if (source[cursor] !== ".") return pos;
581
+ cursor = skipTrivia(cursor + 1);
582
+ const method = IDENT_START_RE.exec(source.slice(cursor))?.[0];
583
+ if (method !== "extend" && method !== "from") return pos;
584
+ cursor = skipTrivia(cursor + method.length);
585
+ if (source[cursor] !== "(") return pos;
586
+ cursor = skipTrivia(cursor + 1);
587
+ const base = IDENT_START_RE.exec(source.slice(cursor))?.[0];
588
+ if (!base) return pos;
589
+ cursor = skipTrivia(cursor + base.length);
590
+ while (source[cursor] === ".") {
591
+ const afterDot = skipTrivia(cursor + 1);
592
+ const segment = IDENT_START_RE.exec(source.slice(afterDot))?.[0];
593
+ if (!segment) break;
594
+ cursor = skipTrivia(afterDot + segment.length);
595
+ }
596
+ if (source[cursor] === ",") cursor = skipTrivia(cursor + 1);
597
+ if (source[cursor] !== ")") return pos;
598
+ identifiers.add(base);
599
+ pos = cursor + 1;
600
+ }
601
+ };
602
+ /** Identifier assigned directly before `index` (`const x = <here>`), if any. */
603
+ const boundIdentifierBefore = (index) => {
604
+ const match = /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*$/.exec(source.slice(0, index));
605
+ return match ? match[1] : null;
606
+ };
607
+ const chainRootIdentifiers = /* @__PURE__ */ new Set();
608
+ const createSystemAnchor = /createSystem\s*\(/g;
609
+ let anchorMatch;
610
+ while ((anchorMatch = createSystemAnchor.exec(source)) !== null) {
611
+ let pos = anchorMatch.index + anchorMatch[0].length;
612
+ let depth = 1;
613
+ while (pos < source.length && depth > 0) {
614
+ const ch = source[pos];
615
+ if (ch === "(") depth++;
616
+ else if (ch === ")") depth--;
617
+ pos++;
618
+ }
619
+ consumeChainLinks(pos);
620
+ const bound = boundIdentifierBefore(anchorMatch.index);
621
+ if (bound) chainRootIdentifiers.add(bound);
622
+ }
623
+ const scannedRoots = /* @__PURE__ */ new Set();
624
+ for (;;) {
625
+ const pendingRoots = [...chainRootIdentifiers].filter((root) => !scannedRoots.has(root));
626
+ if (pendingRoots.length === 0) break;
627
+ for (const root of pendingRoots) {
628
+ scannedRoots.add(root);
629
+ const rootUse = new RegExp(`(?<![a-zA-Z0-9_$])${root.replace(/\$/g, "\\$")}(?![a-zA-Z0-9_$])`, "g");
630
+ let useMatch;
631
+ while ((useMatch = rootUse.exec(source)) !== null) {
632
+ const afterIdentifier = useMatch.index + useMatch[0].length;
633
+ if (consumeChainLinks(afterIdentifier) === afterIdentifier) continue;
634
+ const bound = boundIdentifierBefore(useMatch.index);
635
+ if (bound) chainRootIdentifiers.add(bound);
636
+ }
637
+ }
638
+ }
446
639
  if (identifiers.size === 0) return [];
447
640
  const importMap = /* @__PURE__ */ new Map();
448
641
  const importRegex = /^\s*import\s+(?:([a-zA-Z_$][a-zA-Z0-9_$]*)\s*,\s*)?(?:\{([^}]*)\}|([a-zA-Z_$][a-zA-Z0-9_$]*))\s+from\s+['"]([^'"]+)['"]/gm;
@@ -463,14 +656,265 @@ function extractSystemFilePackages(systemFilePath) {
463
656
  const specifier = importMap.get(id);
464
657
  if (!specifier) continue;
465
658
  if (specifier.startsWith(".")) packages.add(resolve(systemFileDir, specifier));
466
- else {
467
- const pkgName = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
468
- packages.add(pkgName);
469
- }
659
+ else packages.add(specifier);
470
660
  }
471
661
  return Array.from(packages);
472
662
  }
473
663
  //#endregion
664
+ //#region pipeline/asset-placeholders.ts
665
+ /**
666
+ * Host-side handling of `asset()` placeholders (global-styles-system):
667
+ * `animus-asset:<specifier>` markers ride through the sandbox and the
668
+ * emitter byte-exact; each HOST plugin substitutes them with its bundler's
669
+ * resolved asset URL after extraction. This module owns the shared scanning
670
+ * and replacement mechanics — resolution and strict gating stay at the
671
+ * plugin call sites.
672
+ */
673
+ /** Mirrors the producer-side constant in `@animus-ui/system`'s `asset.ts`
674
+ * (extract must not take a runtime dependency on system); the pairing is
675
+ * pinned by a contract test in `tests/asset-placeholders.test.ts`. */
676
+ const ASSET_PLACEHOLDER_PREFIX = "animus-asset:";
677
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
678
+ const PREFIX_RE = escapeRegExp(ASSET_PLACEHOLDER_PREFIX);
679
+ /**
680
+ * Quoted `url('animus-asset:<specifier>')` form — the emitter always quotes
681
+ * `url()` values, so a quoted match carries the FULL specifier, including
682
+ * whitespace and parentheses (D5 pins the placeholder as specifier-verbatim,
683
+ * so scanning fidelity is the only place such characters can be honored).
684
+ */
685
+ const QUOTED_PLACEHOLDER_RE = new RegExp(`(['"])${PREFIX_RE}([^'"]*?)\\1`, "g");
686
+ /** Bare-form fallback: a placeholder up to CSS url()/quote/whitespace. */
687
+ const BARE_PLACEHOLDER_RE = new RegExp(`${PREFIX_RE}([^'")\\s]+)`, "g");
688
+ /** Unique asset specifiers referenced by placeholders, in appearance order. */
689
+ function findAssetSpecifiers(css) {
690
+ if (!css.includes("animus-asset:")) return [];
691
+ const seen = /* @__PURE__ */ new Set();
692
+ const remainder = css.replace(QUOTED_PLACEHOLDER_RE, (_match, _quote, specifier) => {
693
+ seen.add(specifier);
694
+ return "";
695
+ });
696
+ for (const match of remainder.matchAll(BARE_PLACEHOLDER_RE)) seen.add(match[1]);
697
+ return [...seen];
698
+ }
699
+ /**
700
+ * Replace each specifier's placeholder with its substitution. Specifiers
701
+ * absent from the map keep their placeholder — the caller decides whether
702
+ * that is a strict failure or a warn-and-emit-literally. A substitution may
703
+ * itself be a bundler marker (e.g. Vite's `__VITE_ASSET__<ref>__`) that the
704
+ * host's own asset pipeline resolves to a hashed file name later.
705
+ *
706
+ * Each replacement is anchored to a following delimiter (quote, `)`,
707
+ * whitespace, or end of input) so a specifier that textually prefixes a
708
+ * longer one can never clobber the longer placeholder; longest-first
709
+ * ordering additionally lets the longer of two mapped specifiers claim its
710
+ * occurrences before the shorter runs.
711
+ */
712
+ function substituteAssetPlaceholders(css, urlBySpecifier) {
713
+ if (urlBySpecifier.size === 0) return css;
714
+ if (!css.includes("animus-asset:")) return css;
715
+ const specifiers = [...urlBySpecifier.keys()].sort((a, b) => b.length - a.length);
716
+ let out = css;
717
+ for (const specifier of specifiers) {
718
+ const placeholder = new RegExp(escapeRegExp(ASSET_PLACEHOLDER_PREFIX + specifier) + String.raw`(?=['")\s]|$)`, "g");
719
+ out = out.replace(placeholder, () => urlBySpecifier.get(specifier));
720
+ }
721
+ return out;
722
+ }
723
+ //#endregion
724
+ //#region pipeline/resolve-asset.ts
725
+ /**
726
+ * Node-side asset specifier resolution shared by the host plugins (the
727
+ * asset() contract, global-styles-system): host path aliases apply FIRST —
728
+ * an alias such as `@fonts` works in application modules, so
729
+ * `asset('@fonts/inter.woff2')` must resolve identically — then direct Node
730
+ * resolution, then the package-root fallback (exports maps rarely list
731
+ * asset subpaths). Bundler-native resolution (Vite's `this.resolve`) is
732
+ * still preferred where available; this resolver covers the paths that have
733
+ * no bundler hook (Next's session, Vite's dev re-analysis).
734
+ */
735
+ const requireByRoot = /* @__PURE__ */ new Map();
736
+ function requireAnchoredAt(rootDir) {
737
+ let req = requireByRoot.get(rootDir);
738
+ if (!req) {
739
+ req = createRequire$1(join(rootDir, "package.json"));
740
+ requireByRoot.set(rootDir, req);
741
+ }
742
+ return req;
743
+ }
744
+ function packageRootFromEntry(entry) {
745
+ let current = dirname(entry);
746
+ while (true) {
747
+ if (existsSync(join(current, "package.json"))) return current;
748
+ const parent = dirname(current);
749
+ if (parent === current) return null;
750
+ current = parent;
751
+ }
752
+ }
753
+ const aliasTableCache = /* @__PURE__ */ new Map();
754
+ function parseAliasTable(pathAliasesJson) {
755
+ const cached = aliasTableCache.get(pathAliasesJson);
756
+ if (cached) return cached;
757
+ let aliases;
758
+ try {
759
+ aliases = JSON.parse(pathAliasesJson).aliases ?? [];
760
+ } catch {
761
+ aliases = [];
762
+ }
763
+ aliasTableCache.set(pathAliasesJson, aliases);
764
+ return aliases;
765
+ }
766
+ /**
767
+ * Map a specifier through the harvested host alias table (the same
768
+ * `pathAliasesJson` wire the engine consumes — entries pre-sorted longest
769
+ * pattern first by `buildPathAliasesJson`). Returns an absolute path to an
770
+ * EXISTING file, or null when no alias claims the specifier.
771
+ */
772
+ function resolveThroughPathAliases(specifier, rootDir, pathAliasesJson) {
773
+ if (!pathAliasesJson) return null;
774
+ const aliases = parseAliasTable(pathAliasesJson);
775
+ for (const alias of aliases) {
776
+ let mapped = null;
777
+ if (alias.type === "exact") {
778
+ if (alias.pattern === specifier) mapped = alias.replacement;
779
+ } else if (specifier.startsWith(alias.pattern)) mapped = alias.replacement + specifier.slice(alias.pattern.length);
780
+ if (mapped === null) continue;
781
+ const absolute = isAbsolute(mapped) ? mapped : join(rootDir, mapped);
782
+ if (existsSync(absolute)) return absolute;
783
+ }
784
+ return null;
785
+ }
786
+ /**
787
+ * Resolve an asset specifier to an absolute file: host aliases, then direct
788
+ * Node resolution anchored at `rootDir`, then the package-root fallback.
789
+ * Returns null when nothing matches — strict gating stays at the caller.
790
+ */
791
+ function resolveAssetFile(specifier, rootDir, pathAliasesJson) {
792
+ const aliased = resolveThroughPathAliases(specifier, rootDir, pathAliasesJson);
793
+ if (aliased) return aliased;
794
+ const requireFromRoot = requireAnchoredAt(rootDir);
795
+ try {
796
+ return requireFromRoot.resolve(specifier);
797
+ } catch {}
798
+ const segments = specifier.split("/");
799
+ const packageName = specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
800
+ const subpath = specifier.slice(packageName.length + 1);
801
+ if (!subpath) return null;
802
+ for (const modulesDir of requireFromRoot.resolve.paths(packageName) ?? []) {
803
+ const packageRoot = join(modulesDir, packageName);
804
+ if (!existsSync(join(packageRoot, "package.json"))) continue;
805
+ const candidate = join(packageRoot, subpath);
806
+ if (existsSync(candidate)) return candidate;
807
+ }
808
+ try {
809
+ const packageRoot = packageRootFromEntry(requireFromRoot.resolve(packageName));
810
+ if (!packageRoot) return null;
811
+ const candidate = join(packageRoot, subpath);
812
+ return existsSync(candidate) ? candidate : null;
813
+ } catch {
814
+ return null;
815
+ }
816
+ }
817
+ //#endregion
818
+ //#region pipeline/correlate-external-tokens.ts
819
+ /**
820
+ * Index the loader-captured theme manifests by owning specifier. Module paths
821
+ * are canonical absolute paths (symlinks resolved), so each package dir is
822
+ * realpath'd before prefix-matching; a module outside every known package dir
823
+ * (typically the consumer's own theme) contributes nothing.
824
+ */
825
+ function buildSourceTokenIndex(opts) {
826
+ const index = /* @__PURE__ */ new Map();
827
+ if (!opts.sourceThemeManifestsJson) return index;
828
+ let manifests;
829
+ try {
830
+ manifests = JSON.parse(opts.sourceThemeManifestsJson);
831
+ } catch {
832
+ return index;
833
+ }
834
+ const realDirOwners = [];
835
+ const addOwnerDir = (dir, specifier) => {
836
+ if (!realDirOwners.some((entry) => entry.dir === dir)) realDirOwners.push({
837
+ dir,
838
+ specifier
839
+ });
840
+ };
841
+ for (const [dir, specifier] of Object.entries(opts.dirOwners)) {
842
+ let real = dir;
843
+ try {
844
+ real = realpathSync(dir);
845
+ } catch {}
846
+ addOwnerDir(real, specifier);
847
+ let packageRoot = real;
848
+ while (packageRoot !== dirname(packageRoot) && !existsSync(join(packageRoot, "package.json"))) packageRoot = dirname(packageRoot);
849
+ if (existsSync(join(packageRoot, "package.json"))) addOwnerDir(packageRoot, specifier);
850
+ }
851
+ for (const [modulePath, exports] of Object.entries(manifests)) {
852
+ const owner = realDirOwners.find(({ dir }) => modulePath === dir || modulePath.startsWith(dir + sep));
853
+ if (!owner) continue;
854
+ let tokens = index.get(owner.specifier);
855
+ if (!tokens) {
856
+ tokens = /* @__PURE__ */ new Set();
857
+ index.set(owner.specifier, tokens);
858
+ }
859
+ for (const paths of Object.values(exports)) for (const token of paths) tokens.add(token);
860
+ }
861
+ return index;
862
+ }
863
+ /**
864
+ * The correlation join. Returns one teaching-error message per distinct
865
+ * (component, token, specifier) whose file belongs to a discovered source AND
866
+ * whose token that source's manifest defines. The messages follow the
867
+ * standard severity routing at the CALLER (throw under `strict`, warn
868
+ * otherwise) — this join never reports on its own.
869
+ */
870
+ function correlateExternalTokenDiagnostics(opts) {
871
+ const messages = [];
872
+ const seen = /* @__PURE__ */ new Set();
873
+ for (const diagnostic of opts.diagnostics ?? []) {
874
+ if (diagnostic.kind !== "external-token-candidate" || !diagnostic.token) continue;
875
+ const specifier = opts.fileOwners[diagnostic.file];
876
+ if (!specifier) continue;
877
+ if (!opts.sourceTokens.get(specifier)?.has(diagnostic.token)) continue;
878
+ const key = `${diagnostic.component}\u0000${diagnostic.token}\u0000${specifier}`;
879
+ if (seen.has(key)) continue;
880
+ seen.add(key);
881
+ messages.push(`${diagnostic.component} (from '${specifier}') references token '${diagnostic.token}', which the consumer theme does not define — inherit the source's tokens with createTheme().extend(...) using the tokens (or bundle) export of '${specifier}'`);
882
+ }
883
+ return messages;
884
+ }
885
+ const indexCache = /* @__PURE__ */ new WeakMap();
886
+ /**
887
+ * The complete cross-source token-contract gate both plugins share
888
+ * (extraction-diagnostics): index the loader-captured source manifests,
889
+ * join them against the engine's candidates, and route the resulting
890
+ * teaching errors — throw under `strict`, warn otherwise. One entry point
891
+ * so the two hosts cannot drift on wiring or severity semantics.
892
+ */
893
+ function enforceExternalTokenContracts(opts) {
894
+ const manifestsJson = opts.sourceThemeManifestsJson ?? "";
895
+ const cached = indexCache.get(opts.dirOwners);
896
+ let sourceTokens;
897
+ if (cached && cached.manifestsJson === manifestsJson) sourceTokens = cached.index;
898
+ else {
899
+ sourceTokens = buildSourceTokenIndex({
900
+ sourceThemeManifestsJson: opts.sourceThemeManifestsJson,
901
+ dirOwners: opts.dirOwners
902
+ });
903
+ indexCache.set(opts.dirOwners, {
904
+ manifestsJson,
905
+ index: sourceTokens
906
+ });
907
+ }
908
+ const messages = correlateExternalTokenDiagnostics({
909
+ diagnostics: opts.diagnostics,
910
+ fileOwners: opts.fileOwners,
911
+ sourceTokens
912
+ });
913
+ if (messages.length === 0) return;
914
+ if (opts.strict) throw new Error(`${opts.prefix} ${messages.join("\n")}`);
915
+ for (const message of messages) opts.warn(message);
916
+ }
917
+ //#endregion
474
918
  //#region pipeline/path-aliases.ts
475
919
  /**
476
920
  * Normalize alias pairs: strip a `rootDir` prefix from targets, classify
@@ -623,9 +1067,15 @@ function buildAnalysisInputs(opts) {
623
1067
  pathAliasesJson: opts.pathAliasesJson,
624
1068
  keyframesJson: opts.system.keyframesJson,
625
1069
  staticCssJson: opts.staticCssJson ?? null,
626
- conditionAliasesJson: opts.system.conditionAliasesJson ?? null
1070
+ conditionAliasesJson: opts.system.conditionAliasesJson ?? null,
1071
+ externalDirsJson: opts.externalDirs?.length && hasSourceThemeManifests(opts.system) ? JSON.stringify(opts.externalDirs) : null
627
1072
  };
628
1073
  }
1074
+ /** Whether the loader captured at least one source built-theme manifest. */
1075
+ function hasSourceThemeManifests(system) {
1076
+ const json = system.sourceThemeManifestsJson;
1077
+ return typeof json === "string" && json.length > 0 && json !== "{}";
1078
+ }
629
1079
  /**
630
1080
  * The one analysis invocation both plugins share: build the emitter
631
1081
  * config, serialize inputs, call the NAPI `analyzeProject`, parse the
@@ -758,7 +1208,8 @@ function loadSystemConfig(engineApi, opts) {
758
1208
  conditionAliasesJson: config.conditionAliases || null,
759
1209
  globalStyleBlocksJson: config.globalStyleBlocks || null,
760
1210
  keyframesJson: config.keyframesBlocks || null,
761
- dependencies: config.dependencies ?? []
1211
+ dependencies: config.dependencies ?? [],
1212
+ sourceThemeManifestsJson: config.sourceThemeManifests || null
762
1213
  };
763
1214
  }
764
1215
  //#endregion
@@ -1083,7 +1534,7 @@ function toWatchKeys(input) {
1083
1534
  */
1084
1535
  function realpathThroughNearestExistingAncestor(path) {
1085
1536
  try {
1086
- return realpathSync(path);
1537
+ return realpathSync$1(path);
1087
1538
  } catch {}
1088
1539
  const suffix = [];
1089
1540
  let current = path;
@@ -1093,9 +1544,9 @@ function realpathThroughNearestExistingAncestor(path) {
1093
1544
  suffix.unshift(basename(current));
1094
1545
  current = parent;
1095
1546
  try {
1096
- return join$1(realpathSync(current), ...suffix);
1547
+ return join$1(realpathSync$1(current), ...suffix);
1097
1548
  } catch {}
1098
1549
  }
1099
1550
  }
1100
1551
  //#endregion
1101
- 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, unresolvableIncludesMessage, validateLayerOrder };
1552
+ export { ANIMUS_LAYERS, DEFAULT_EXTENSIONS, RETIRED_ENGINE_MESSAGE, applyPrefix, applyUnitFallback, assembleStylesheet, assertNoRetiredEngineSelection, buildAnalysisInputs, buildAnalyzeProjectArgs, buildDynamicPropConfig, buildPathAliasesJson, buildSystemPropsModule, camelToKebab, clearEngineCache, collectExternalPackageSources, contentHash, createV2EngineApi, discoverFiles, enforceExternalTokenContracts, extractSystemFilePackages, findAssetSpecifiers, findPackageRoot, formatRustTimingWaterfall, loadSystemConfig, postProcessCss, preprocessMdx, readTsconfigAliasPairs, resolveAssetFile, resolveLightningTargets, resolveThroughPathAliases, runProjectAnalysis, serializeStaticCss, staleDistIncludesMessage, stripLeadingLayerDeclaration, substituteAssetPlaceholders, surfaceManifestDiagnostics, toWatchKeys, unresolvableIncludesMessage, validateLayerOrder };