@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.cjs CHANGED
@@ -24,6 +24,7 @@ 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 module$1 = require("module");
27
28
  let browserslist = require("browserslist");
28
29
  browserslist = __toESM(browserslist);
29
30
  let lightningcss = require("lightningcss");
@@ -121,7 +122,8 @@ function buildAnalyzeProjectArgs(inputs) {
121
122
  inputs.pathAliasesJson,
122
123
  inputs.keyframesJson,
123
124
  inputs.staticCssJson,
124
- inputs.conditionAliasesJson
125
+ inputs.conditionAliasesJson,
126
+ inputs.externalDirsJson
125
127
  ];
126
128
  }
127
129
  //#endregion
@@ -139,7 +141,7 @@ function createV2EngineApi(deps) {
139
141
  const native = loadNativeEngine();
140
142
  return {
141
143
  loadSystemModule: (...args) => native.loadSystemModule(...args),
142
- analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson) => {
144
+ analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson) => {
143
145
  const filesJson = deps.rehydrateFilesJson ? deps.rehydrateFilesJson(filesJsonRaw) : filesJsonRaw;
144
146
  const sent = /* @__PURE__ */ new Map();
145
147
  for (const entry of JSON.parse(filesJson)) sent.set(entry.path, entry.source);
@@ -162,6 +164,7 @@ function createV2EngineApi(deps) {
162
164
  packageResolutionJson: packageResolutionJson ?? void 0,
163
165
  pathAliasesJson: pathAliasesJson ?? void 0,
164
166
  staticCssJson: staticCssJson ?? void 0,
167
+ externalDirsJson: externalDirsJson ?? void 0,
165
168
  devMode
166
169
  };
167
170
  const engine = new native.ExtractEngine(config);
@@ -220,8 +223,10 @@ function assertNoRetiredEngineSelection(engineOption) {
220
223
  //#region pipeline/content-hash.ts
221
224
  /**
222
225
  * MD5 content hash used for file-change detection (HMR diffing) by both
223
- * extraction plugins. The algorithm/encoding is a cross-plugin contract:
224
- * cache keys written by one build path must compare equal in the next.
226
+ * extraction plugins, and for content-addressing copied asset bytes. The
227
+ * algorithm/encoding is a cross-plugin contract: cache keys written by one
228
+ * build path must compare equal in the next. Accepts a Buffer directly so
229
+ * binary sources hash without an intermediate string copy.
225
230
  */
226
231
  function contentHash(source) {
227
232
  return (0, crypto.createHash)("md5").update(source).digest("hex");
@@ -284,6 +289,14 @@ const PACKAGE_SRC_EXCLUDES = [
284
289
  ".test.",
285
290
  ".spec."
286
291
  ];
292
+ /** Exclusions for the compiled-output fallback when a package does not ship src/. */
293
+ const PACKAGE_OUTPUT_EXCLUDES = [
294
+ "node_modules",
295
+ ".test.",
296
+ ".spec.",
297
+ ".d.ts",
298
+ ".map"
299
+ ];
287
300
  const isFile = (path$2) => {
288
301
  try {
289
302
  return (0, fs.statSync)(path$2).isFile();
@@ -305,6 +318,42 @@ function resolveAbsolutePathSpecifier(absSpecifier, extensionsSet) {
305
318
  ...Array.from(extensionsSet, (ext) => (0, path.join)(absSpecifier, `index${ext}`))
306
319
  ].find(isFile) ?? null;
307
320
  }
321
+ /** The package-name half of a bare specifier (`@scope/name/sub` → `@scope/name`). */
322
+ function bareSpecifierPackageName(specifier) {
323
+ const segments = specifier.split("/");
324
+ return specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
325
+ }
326
+ function sourceEntryForSpecifier(specifier, srcDir, extensionsSet) {
327
+ if ((0, path.isAbsolute)(specifier)) {
328
+ const resolved = resolveAbsolutePathSpecifier(specifier, extensionsSet);
329
+ if (!resolved) return null;
330
+ return !(0, path.relative)(srcDir, resolved).startsWith("..") ? resolved : null;
331
+ }
332
+ const packageName = bareSpecifierPackageName(specifier);
333
+ return resolveAbsolutePathSpecifier((0, path.join)(srcDir, specifier.slice(packageName.length + 1) || "index"), extensionsSet);
334
+ }
335
+ /**
336
+ * The D13 dist-freshness check: applicable only when a specifier has BOTH a
337
+ * walked src/ tree and a resolved dist entry that exists on disk outside that
338
+ * tree; stale when the dist entry's mtime is older than the newest walked
339
+ * source file's. Not applicable (never stale) when the entry resolves inside
340
+ * src/ (no dist to skew), when the entry file is missing, or when no source
341
+ * file's mtime is readable. Detection only — reporting is the caller's policy.
342
+ */
343
+ function distEntryIsStale(absEntry, srcDir, srcFiles) {
344
+ if (!(0, path.relative)(srcDir, absEntry).startsWith("..")) return false;
345
+ let distMtime;
346
+ try {
347
+ distMtime = (0, fs.statSync)(absEntry).mtimeMs;
348
+ } catch {
349
+ return false;
350
+ }
351
+ let newestSrcMtime = -Infinity;
352
+ for (const srcFile of srcFiles) try {
353
+ newestSrcMtime = Math.max(newestSrcMtime, (0, fs.statSync)(srcFile).mtimeMs);
354
+ } catch {}
355
+ return distMtime < newestSrcMtime;
356
+ }
308
357
  /**
309
358
  * Shared external-package source collection (spec:
310
359
  * external-package-file-discovery), consumed by both extraction plugins.
@@ -330,6 +379,8 @@ async function collectExternalPackageSources(opts) {
330
379
  const packageMap = {};
331
380
  const sourceEntries = /* @__PURE__ */ new Map();
332
381
  const packageDirs = [];
382
+ const dirOwners = {};
383
+ const fileOwners = {};
333
384
  const outcomes = [];
334
385
  const alreadyIngested = (relPath) => hasEntry(relPath) || pushed.has(relPath);
335
386
  for (const specifier of specifiers) {
@@ -350,17 +401,30 @@ async function collectExternalPackageSources(opts) {
350
401
  }
351
402
  const srcDir = (0, path.join)(findPackageRoot(absEntry), "src");
352
403
  let fileCount = 0;
404
+ let staleDist = false;
353
405
  if ((0, fs.existsSync)(srcDir)) {
354
406
  packageDirs.push(srcDir);
355
- const srcEntry = (0, path.join)(srcDir, "index.ts");
356
- if ((0, fs.existsSync)(srcEntry)) {
407
+ dirOwners[srcDir] ??= specifier;
408
+ const srcEntry = sourceEntryForSpecifier(specifier, srcDir, extensionsSet);
409
+ if (srcEntry) {
357
410
  packageMap[specifier] = (0, path.relative)(rootDir, srcEntry);
358
411
  sourceEntries.set(specifier, srcEntry);
359
412
  } else packageMap[specifier] = (0, path.relative)(rootDir, absEntry);
413
+ if (!(0, path.isAbsolute)(specifier)) {
414
+ const packageName = bareSpecifierPackageName(specifier);
415
+ if (packageName !== specifier && !(packageName in packageMap)) {
416
+ const rootEntry = sourceEntryForSpecifier(packageName, srcDir, extensionsSet);
417
+ if (rootEntry) {
418
+ packageMap[packageName] = (0, path.relative)(rootDir, rootEntry);
419
+ sourceEntries.set(packageName, rootEntry);
420
+ }
421
+ }
422
+ }
360
423
  const pkgFiles = discoverFiles(srcDir, srcDir, [], extensionsSet).filter((absPath) => {
361
424
  const inPkg = (0, path.relative)(srcDir, absPath);
362
425
  return !PACKAGE_SRC_EXCLUDES.some((pattern) => inPkg.includes(pattern));
363
426
  });
427
+ staleDist = distEntryIsStale(absEntry, srcDir, pkgFiles);
364
428
  for (const pkgFile of pkgFiles) {
365
429
  const relPath = (0, path.relative)(rootDir, pkgFile);
366
430
  if (alreadyIngested(relPath)) {
@@ -381,28 +445,48 @@ async function collectExternalPackageSources(opts) {
381
445
  source: processed.source
382
446
  });
383
447
  pushed.add(processed.relPath);
448
+ fileOwners[processed.relPath] ??= specifier;
384
449
  fileCount++;
385
450
  }
386
451
  } else {
387
- packageDirs.push((0, path.dirname)(absEntry));
388
- const relPath = (0, path.relative)(rootDir, absEntry);
389
- packageMap[specifier] = relPath;
390
- if (alreadyIngested(relPath)) fileCount++;
391
- else try {
392
- const source = (0, fs.readFileSync)(absEntry, "utf-8");
452
+ const outputDir = (0, path.dirname)(absEntry);
453
+ packageDirs.push(outputDir);
454
+ dirOwners[outputDir] ??= specifier;
455
+ packageMap[specifier] = (0, path.relative)(rootDir, absEntry);
456
+ const outputExtensions = new Set(extensionsSet);
457
+ outputExtensions.add((0, path.extname)(absEntry));
458
+ const outputFiles = discoverFiles(outputDir, outputDir, [], outputExtensions).filter((file) => {
459
+ const relToOutput = (0, path.relative)(outputDir, file);
460
+ return !PACKAGE_OUTPUT_EXCLUDES.some((pattern) => relToOutput.includes(pattern));
461
+ });
462
+ if (!outputFiles.includes(absEntry)) outputFiles.unshift(absEntry);
463
+ for (const outputFile of outputFiles) {
464
+ const outputRelPath = (0, path.relative)(rootDir, outputFile);
465
+ if (alreadyIngested(outputRelPath)) {
466
+ fileCount++;
467
+ continue;
468
+ }
469
+ let source;
470
+ try {
471
+ source = (0, fs.readFileSync)(outputFile, "utf-8");
472
+ } catch (err) {
473
+ onUnreadable(outputRelPath, err);
474
+ continue;
475
+ }
476
+ const processed = await preprocessFile(source, outputRelPath, outputFile);
477
+ if (!processed) continue;
393
478
  entries.push({
394
- path: relPath,
395
- source
479
+ path: processed.relPath,
480
+ source: processed.source
396
481
  });
397
- pushed.add(relPath);
482
+ pushed.add(processed.relPath);
483
+ fileOwners[processed.relPath] ??= specifier;
398
484
  fileCount++;
399
- } catch (err) {
400
- onUnreadable(relPath, err);
401
485
  }
402
486
  }
403
487
  outcomes.push({
404
488
  specifier,
405
- outcome: fileCount > 0 ? "resolved" : "empty",
489
+ outcome: staleDist ? "stale-dist" : fileCount > 0 ? "resolved" : "empty",
406
490
  fileCount
407
491
  });
408
492
  }
@@ -411,24 +495,33 @@ async function collectExternalPackageSources(opts) {
411
495
  packageMap,
412
496
  sourceEntries,
413
497
  packageDirs,
498
+ dirOwners,
499
+ fileOwners,
414
500
  outcomes
415
501
  };
416
502
  }
417
503
  /**
418
- * Extract external DS package names from `includes` declarations in the system file.
504
+ * Extract external DS package names from inheritance declarations in the
505
+ * system file.
419
506
  *
420
- * Supports two forms:
421
- * - Primary (1.0+): `createSystem({ includes: [identifier, ...] })` constructor arg
422
- * - Legacy: `.includes([identifier, ...])` chain method (RC migration fallback)
507
+ * Supports four forms:
508
+ * - Primary: `createSystem(...).extend(identifier)` chain calls
509
+ * (repeatable, mixable with `.from()` links; each call
510
+ * contributes one source; a library-bundle identifier
511
+ * traces its base import)
512
+ * - Deprecated chain: `createSystem(...).from(identifier)` chain calls
513
+ * - Deprecated alias: `createSystem({ includes: [identifier, ...] })`
514
+ * - Legacy: `.includes([identifier, ...])` chain method (RC
515
+ * migration fallback)
423
516
  *
424
517
  * For each identifier found, traces back to its import declaration and returns
425
518
  * the import specifier: a bare specifier normalized to its package name, a
426
519
  * relative specifier resolved against the system file's directory into an
427
520
  * absolute path (so a sibling package referenced by path contributes discovery
428
- * too). Only packages explicitly declared via `includes` are treated as
429
- * external DS dependencies.
521
+ * too). Only packages explicitly declared through one of these forms are
522
+ * treated as external DS dependencies.
430
523
  *
431
- * Falls back to empty array if no `includes` declaration is found.
524
+ * Falls back to empty array if no declaration is found.
432
525
  */
433
526
  /**
434
527
  * The message for the strict/warn gate over unresolvable includes, or null
@@ -441,6 +534,18 @@ function unresolvableIncludesMessage(outcomes) {
441
534
  if (unresolvable.length === 0) return null;
442
535
  return `[animus-extract] unresolvable include specifier(s): ${unresolvable.join(", ")}`;
443
536
  }
537
+ /**
538
+ * The message for the strict/warn gate over stale dist entries
539
+ * (first-class-extension D13), or null when no declared specifier is stale.
540
+ * A stale dist silently skews merged registry content while discovery
541
+ * compiles the fresh sources, so it surfaces like an unresolvable specifier:
542
+ * non-strict consumers warn with this line, strict consumers throw it.
543
+ */
544
+ function staleDistIncludesMessage(outcomes) {
545
+ const stale = outcomes.filter((record) => record.outcome === "stale-dist").map((record) => record.specifier);
546
+ if (stale.length === 0) return null;
547
+ 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`;
548
+ }
444
549
  function extractSystemFilePackages(systemFilePath) {
445
550
  let source;
446
551
  try {
@@ -463,6 +568,94 @@ function extractSystemFilePackages(systemFilePath) {
463
568
  };
464
569
  collectIdentifiers(constructorRegex);
465
570
  collectIdentifiers(chainRegex);
571
+ const IDENT_START_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*/;
572
+ /** Position after any run of whitespace, line comments, block comments. */
573
+ const skipTrivia = (from) => {
574
+ let pos = from;
575
+ for (;;) {
576
+ while (pos < source.length && /\s/.test(source[pos])) pos++;
577
+ if (source.startsWith("//", pos)) {
578
+ const newline = source.indexOf("\n", pos);
579
+ pos = newline === -1 ? source.length : newline + 1;
580
+ continue;
581
+ }
582
+ if (source.startsWith("/*", pos)) {
583
+ const close = source.indexOf("*/", pos + 2);
584
+ pos = close === -1 ? source.length : close + 2;
585
+ continue;
586
+ }
587
+ return pos;
588
+ }
589
+ };
590
+ /**
591
+ * Consume consecutive extend/from links starting at `from`, collecting
592
+ * each link's base identifier; tolerates trivia between tokens and a
593
+ * trailing comma in the argument list. Returns the position after the
594
+ * last consumed link (`from` itself when none matched).
595
+ */
596
+ const consumeChainLinks = (from) => {
597
+ let pos = from;
598
+ for (;;) {
599
+ let cursor = skipTrivia(pos);
600
+ if (source[cursor] !== ".") return pos;
601
+ cursor = skipTrivia(cursor + 1);
602
+ const method = IDENT_START_RE.exec(source.slice(cursor))?.[0];
603
+ if (method !== "extend" && method !== "from") return pos;
604
+ cursor = skipTrivia(cursor + method.length);
605
+ if (source[cursor] !== "(") return pos;
606
+ cursor = skipTrivia(cursor + 1);
607
+ const base = IDENT_START_RE.exec(source.slice(cursor))?.[0];
608
+ if (!base) return pos;
609
+ cursor = skipTrivia(cursor + base.length);
610
+ while (source[cursor] === ".") {
611
+ const afterDot = skipTrivia(cursor + 1);
612
+ const segment = IDENT_START_RE.exec(source.slice(afterDot))?.[0];
613
+ if (!segment) break;
614
+ cursor = skipTrivia(afterDot + segment.length);
615
+ }
616
+ if (source[cursor] === ",") cursor = skipTrivia(cursor + 1);
617
+ if (source[cursor] !== ")") return pos;
618
+ identifiers.add(base);
619
+ pos = cursor + 1;
620
+ }
621
+ };
622
+ /** Identifier assigned directly before `index` (`const x = <here>`), if any. */
623
+ const boundIdentifierBefore = (index) => {
624
+ const match = /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*$/.exec(source.slice(0, index));
625
+ return match ? match[1] : null;
626
+ };
627
+ const chainRootIdentifiers = /* @__PURE__ */ new Set();
628
+ const createSystemAnchor = /createSystem\s*\(/g;
629
+ let anchorMatch;
630
+ while ((anchorMatch = createSystemAnchor.exec(source)) !== null) {
631
+ let pos = anchorMatch.index + anchorMatch[0].length;
632
+ let depth = 1;
633
+ while (pos < source.length && depth > 0) {
634
+ const ch = source[pos];
635
+ if (ch === "(") depth++;
636
+ else if (ch === ")") depth--;
637
+ pos++;
638
+ }
639
+ consumeChainLinks(pos);
640
+ const bound = boundIdentifierBefore(anchorMatch.index);
641
+ if (bound) chainRootIdentifiers.add(bound);
642
+ }
643
+ const scannedRoots = /* @__PURE__ */ new Set();
644
+ for (;;) {
645
+ const pendingRoots = [...chainRootIdentifiers].filter((root) => !scannedRoots.has(root));
646
+ if (pendingRoots.length === 0) break;
647
+ for (const root of pendingRoots) {
648
+ scannedRoots.add(root);
649
+ const rootUse = new RegExp(`(?<![a-zA-Z0-9_$])${root.replace(/\$/g, "\\$")}(?![a-zA-Z0-9_$])`, "g");
650
+ let useMatch;
651
+ while ((useMatch = rootUse.exec(source)) !== null) {
652
+ const afterIdentifier = useMatch.index + useMatch[0].length;
653
+ if (consumeChainLinks(afterIdentifier) === afterIdentifier) continue;
654
+ const bound = boundIdentifierBefore(useMatch.index);
655
+ if (bound) chainRootIdentifiers.add(bound);
656
+ }
657
+ }
658
+ }
466
659
  if (identifiers.size === 0) return [];
467
660
  const importMap = /* @__PURE__ */ new Map();
468
661
  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;
@@ -483,14 +676,265 @@ function extractSystemFilePackages(systemFilePath) {
483
676
  const specifier = importMap.get(id);
484
677
  if (!specifier) continue;
485
678
  if (specifier.startsWith(".")) packages.add((0, path.resolve)(systemFileDir, specifier));
486
- else {
487
- const pkgName = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0];
488
- packages.add(pkgName);
489
- }
679
+ else packages.add(specifier);
490
680
  }
491
681
  return Array.from(packages);
492
682
  }
493
683
  //#endregion
684
+ //#region pipeline/asset-placeholders.ts
685
+ /**
686
+ * Host-side handling of `asset()` placeholders (global-styles-system):
687
+ * `animus-asset:<specifier>` markers ride through the sandbox and the
688
+ * emitter byte-exact; each HOST plugin substitutes them with its bundler's
689
+ * resolved asset URL after extraction. This module owns the shared scanning
690
+ * and replacement mechanics — resolution and strict gating stay at the
691
+ * plugin call sites.
692
+ */
693
+ /** Mirrors the producer-side constant in `@animus-ui/system`'s `asset.ts`
694
+ * (extract must not take a runtime dependency on system); the pairing is
695
+ * pinned by a contract test in `tests/asset-placeholders.test.ts`. */
696
+ const ASSET_PLACEHOLDER_PREFIX = "animus-asset:";
697
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
698
+ const PREFIX_RE = escapeRegExp(ASSET_PLACEHOLDER_PREFIX);
699
+ /**
700
+ * Quoted `url('animus-asset:<specifier>')` form — the emitter always quotes
701
+ * `url()` values, so a quoted match carries the FULL specifier, including
702
+ * whitespace and parentheses (D5 pins the placeholder as specifier-verbatim,
703
+ * so scanning fidelity is the only place such characters can be honored).
704
+ */
705
+ const QUOTED_PLACEHOLDER_RE = new RegExp(`(['"])${PREFIX_RE}([^'"]*?)\\1`, "g");
706
+ /** Bare-form fallback: a placeholder up to CSS url()/quote/whitespace. */
707
+ const BARE_PLACEHOLDER_RE = new RegExp(`${PREFIX_RE}([^'")\\s]+)`, "g");
708
+ /** Unique asset specifiers referenced by placeholders, in appearance order. */
709
+ function findAssetSpecifiers(css) {
710
+ if (!css.includes("animus-asset:")) return [];
711
+ const seen = /* @__PURE__ */ new Set();
712
+ const remainder = css.replace(QUOTED_PLACEHOLDER_RE, (_match, _quote, specifier) => {
713
+ seen.add(specifier);
714
+ return "";
715
+ });
716
+ for (const match of remainder.matchAll(BARE_PLACEHOLDER_RE)) seen.add(match[1]);
717
+ return [...seen];
718
+ }
719
+ /**
720
+ * Replace each specifier's placeholder with its substitution. Specifiers
721
+ * absent from the map keep their placeholder — the caller decides whether
722
+ * that is a strict failure or a warn-and-emit-literally. A substitution may
723
+ * itself be a bundler marker (e.g. Vite's `__VITE_ASSET__<ref>__`) that the
724
+ * host's own asset pipeline resolves to a hashed file name later.
725
+ *
726
+ * Each replacement is anchored to a following delimiter (quote, `)`,
727
+ * whitespace, or end of input) so a specifier that textually prefixes a
728
+ * longer one can never clobber the longer placeholder; longest-first
729
+ * ordering additionally lets the longer of two mapped specifiers claim its
730
+ * occurrences before the shorter runs.
731
+ */
732
+ function substituteAssetPlaceholders(css, urlBySpecifier) {
733
+ if (urlBySpecifier.size === 0) return css;
734
+ if (!css.includes("animus-asset:")) return css;
735
+ const specifiers = [...urlBySpecifier.keys()].sort((a, b) => b.length - a.length);
736
+ let out = css;
737
+ for (const specifier of specifiers) {
738
+ const placeholder = new RegExp(escapeRegExp(ASSET_PLACEHOLDER_PREFIX + specifier) + String.raw`(?=['")\s]|$)`, "g");
739
+ out = out.replace(placeholder, () => urlBySpecifier.get(specifier));
740
+ }
741
+ return out;
742
+ }
743
+ //#endregion
744
+ //#region pipeline/resolve-asset.ts
745
+ /**
746
+ * Node-side asset specifier resolution shared by the host plugins (the
747
+ * asset() contract, global-styles-system): host path aliases apply FIRST —
748
+ * an alias such as `@fonts` works in application modules, so
749
+ * `asset('@fonts/inter.woff2')` must resolve identically — then direct Node
750
+ * resolution, then the package-root fallback (exports maps rarely list
751
+ * asset subpaths). Bundler-native resolution (Vite's `this.resolve`) is
752
+ * still preferred where available; this resolver covers the paths that have
753
+ * no bundler hook (Next's session, Vite's dev re-analysis).
754
+ */
755
+ const requireByRoot = /* @__PURE__ */ new Map();
756
+ function requireAnchoredAt(rootDir) {
757
+ let req = requireByRoot.get(rootDir);
758
+ if (!req) {
759
+ req = (0, module$1.createRequire)((0, path.join)(rootDir, "package.json"));
760
+ requireByRoot.set(rootDir, req);
761
+ }
762
+ return req;
763
+ }
764
+ function packageRootFromEntry(entry) {
765
+ let current = (0, path.dirname)(entry);
766
+ while (true) {
767
+ if ((0, fs.existsSync)((0, path.join)(current, "package.json"))) return current;
768
+ const parent = (0, path.dirname)(current);
769
+ if (parent === current) return null;
770
+ current = parent;
771
+ }
772
+ }
773
+ const aliasTableCache = /* @__PURE__ */ new Map();
774
+ function parseAliasTable(pathAliasesJson) {
775
+ const cached = aliasTableCache.get(pathAliasesJson);
776
+ if (cached) return cached;
777
+ let aliases;
778
+ try {
779
+ aliases = JSON.parse(pathAliasesJson).aliases ?? [];
780
+ } catch {
781
+ aliases = [];
782
+ }
783
+ aliasTableCache.set(pathAliasesJson, aliases);
784
+ return aliases;
785
+ }
786
+ /**
787
+ * Map a specifier through the harvested host alias table (the same
788
+ * `pathAliasesJson` wire the engine consumes — entries pre-sorted longest
789
+ * pattern first by `buildPathAliasesJson`). Returns an absolute path to an
790
+ * EXISTING file, or null when no alias claims the specifier.
791
+ */
792
+ function resolveThroughPathAliases(specifier, rootDir, pathAliasesJson) {
793
+ if (!pathAliasesJson) return null;
794
+ const aliases = parseAliasTable(pathAliasesJson);
795
+ for (const alias of aliases) {
796
+ let mapped = null;
797
+ if (alias.type === "exact") {
798
+ if (alias.pattern === specifier) mapped = alias.replacement;
799
+ } else if (specifier.startsWith(alias.pattern)) mapped = alias.replacement + specifier.slice(alias.pattern.length);
800
+ if (mapped === null) continue;
801
+ const absolute = (0, path.isAbsolute)(mapped) ? mapped : (0, path.join)(rootDir, mapped);
802
+ if ((0, fs.existsSync)(absolute)) return absolute;
803
+ }
804
+ return null;
805
+ }
806
+ /**
807
+ * Resolve an asset specifier to an absolute file: host aliases, then direct
808
+ * Node resolution anchored at `rootDir`, then the package-root fallback.
809
+ * Returns null when nothing matches — strict gating stays at the caller.
810
+ */
811
+ function resolveAssetFile(specifier, rootDir, pathAliasesJson) {
812
+ const aliased = resolveThroughPathAliases(specifier, rootDir, pathAliasesJson);
813
+ if (aliased) return aliased;
814
+ const requireFromRoot = requireAnchoredAt(rootDir);
815
+ try {
816
+ return requireFromRoot.resolve(specifier);
817
+ } catch {}
818
+ const segments = specifier.split("/");
819
+ const packageName = specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
820
+ const subpath = specifier.slice(packageName.length + 1);
821
+ if (!subpath) return null;
822
+ for (const modulesDir of requireFromRoot.resolve.paths(packageName) ?? []) {
823
+ const packageRoot = (0, path.join)(modulesDir, packageName);
824
+ if (!(0, fs.existsSync)((0, path.join)(packageRoot, "package.json"))) continue;
825
+ const candidate = (0, path.join)(packageRoot, subpath);
826
+ if ((0, fs.existsSync)(candidate)) return candidate;
827
+ }
828
+ try {
829
+ const packageRoot = packageRootFromEntry(requireFromRoot.resolve(packageName));
830
+ if (!packageRoot) return null;
831
+ const candidate = (0, path.join)(packageRoot, subpath);
832
+ return (0, fs.existsSync)(candidate) ? candidate : null;
833
+ } catch {
834
+ return null;
835
+ }
836
+ }
837
+ //#endregion
838
+ //#region pipeline/correlate-external-tokens.ts
839
+ /**
840
+ * Index the loader-captured theme manifests by owning specifier. Module paths
841
+ * are canonical absolute paths (symlinks resolved), so each package dir is
842
+ * realpath'd before prefix-matching; a module outside every known package dir
843
+ * (typically the consumer's own theme) contributes nothing.
844
+ */
845
+ function buildSourceTokenIndex(opts) {
846
+ const index = /* @__PURE__ */ new Map();
847
+ if (!opts.sourceThemeManifestsJson) return index;
848
+ let manifests;
849
+ try {
850
+ manifests = JSON.parse(opts.sourceThemeManifestsJson);
851
+ } catch {
852
+ return index;
853
+ }
854
+ const realDirOwners = [];
855
+ const addOwnerDir = (dir, specifier) => {
856
+ if (!realDirOwners.some((entry) => entry.dir === dir)) realDirOwners.push({
857
+ dir,
858
+ specifier
859
+ });
860
+ };
861
+ for (const [dir, specifier] of Object.entries(opts.dirOwners)) {
862
+ let real = dir;
863
+ try {
864
+ real = (0, fs.realpathSync)(dir);
865
+ } catch {}
866
+ addOwnerDir(real, specifier);
867
+ let packageRoot = real;
868
+ while (packageRoot !== (0, path.dirname)(packageRoot) && !(0, fs.existsSync)((0, path.join)(packageRoot, "package.json"))) packageRoot = (0, path.dirname)(packageRoot);
869
+ if ((0, fs.existsSync)((0, path.join)(packageRoot, "package.json"))) addOwnerDir(packageRoot, specifier);
870
+ }
871
+ for (const [modulePath, exports] of Object.entries(manifests)) {
872
+ const owner = realDirOwners.find(({ dir }) => modulePath === dir || modulePath.startsWith(dir + path.sep));
873
+ if (!owner) continue;
874
+ let tokens = index.get(owner.specifier);
875
+ if (!tokens) {
876
+ tokens = /* @__PURE__ */ new Set();
877
+ index.set(owner.specifier, tokens);
878
+ }
879
+ for (const paths of Object.values(exports)) for (const token of paths) tokens.add(token);
880
+ }
881
+ return index;
882
+ }
883
+ /**
884
+ * The correlation join. Returns one teaching-error message per distinct
885
+ * (component, token, specifier) whose file belongs to a discovered source AND
886
+ * whose token that source's manifest defines. The messages follow the
887
+ * standard severity routing at the CALLER (throw under `strict`, warn
888
+ * otherwise) — this join never reports on its own.
889
+ */
890
+ function correlateExternalTokenDiagnostics(opts) {
891
+ const messages = [];
892
+ const seen = /* @__PURE__ */ new Set();
893
+ for (const diagnostic of opts.diagnostics ?? []) {
894
+ if (diagnostic.kind !== "external-token-candidate" || !diagnostic.token) continue;
895
+ const specifier = opts.fileOwners[diagnostic.file];
896
+ if (!specifier) continue;
897
+ if (!opts.sourceTokens.get(specifier)?.has(diagnostic.token)) continue;
898
+ const key = `${diagnostic.component}\u0000${diagnostic.token}\u0000${specifier}`;
899
+ if (seen.has(key)) continue;
900
+ seen.add(key);
901
+ 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}'`);
902
+ }
903
+ return messages;
904
+ }
905
+ const indexCache = /* @__PURE__ */ new WeakMap();
906
+ /**
907
+ * The complete cross-source token-contract gate both plugins share
908
+ * (extraction-diagnostics): index the loader-captured source manifests,
909
+ * join them against the engine's candidates, and route the resulting
910
+ * teaching errors — throw under `strict`, warn otherwise. One entry point
911
+ * so the two hosts cannot drift on wiring or severity semantics.
912
+ */
913
+ function enforceExternalTokenContracts(opts) {
914
+ const manifestsJson = opts.sourceThemeManifestsJson ?? "";
915
+ const cached = indexCache.get(opts.dirOwners);
916
+ let sourceTokens;
917
+ if (cached && cached.manifestsJson === manifestsJson) sourceTokens = cached.index;
918
+ else {
919
+ sourceTokens = buildSourceTokenIndex({
920
+ sourceThemeManifestsJson: opts.sourceThemeManifestsJson,
921
+ dirOwners: opts.dirOwners
922
+ });
923
+ indexCache.set(opts.dirOwners, {
924
+ manifestsJson,
925
+ index: sourceTokens
926
+ });
927
+ }
928
+ const messages = correlateExternalTokenDiagnostics({
929
+ diagnostics: opts.diagnostics,
930
+ fileOwners: opts.fileOwners,
931
+ sourceTokens
932
+ });
933
+ if (messages.length === 0) return;
934
+ if (opts.strict) throw new Error(`${opts.prefix} ${messages.join("\n")}`);
935
+ for (const message of messages) opts.warn(message);
936
+ }
937
+ //#endregion
494
938
  //#region pipeline/path-aliases.ts
495
939
  /**
496
940
  * Normalize alias pairs: strip a `rootDir` prefix from targets, classify
@@ -643,9 +1087,15 @@ function buildAnalysisInputs(opts) {
643
1087
  pathAliasesJson: opts.pathAliasesJson,
644
1088
  keyframesJson: opts.system.keyframesJson,
645
1089
  staticCssJson: opts.staticCssJson ?? null,
646
- conditionAliasesJson: opts.system.conditionAliasesJson ?? null
1090
+ conditionAliasesJson: opts.system.conditionAliasesJson ?? null,
1091
+ externalDirsJson: opts.externalDirs?.length && hasSourceThemeManifests(opts.system) ? JSON.stringify(opts.externalDirs) : null
647
1092
  };
648
1093
  }
1094
+ /** Whether the loader captured at least one source built-theme manifest. */
1095
+ function hasSourceThemeManifests(system) {
1096
+ const json = system.sourceThemeManifestsJson;
1097
+ return typeof json === "string" && json.length > 0 && json !== "{}";
1098
+ }
649
1099
  /**
650
1100
  * The one analysis invocation both plugins share: build the emitter
651
1101
  * config, serialize inputs, call the NAPI `analyzeProject`, parse the
@@ -778,7 +1228,8 @@ function loadSystemConfig(engineApi, opts) {
778
1228
  conditionAliasesJson: config.conditionAliases || null,
779
1229
  globalStyleBlocksJson: config.globalStyleBlocks || null,
780
1230
  keyframesJson: config.keyframesBlocks || null,
781
- dependencies: config.dependencies ?? []
1231
+ dependencies: config.dependencies ?? [],
1232
+ sourceThemeManifestsJson: config.sourceThemeManifests || null
782
1233
  };
783
1234
  }
784
1235
  //#endregion
@@ -1136,17 +1587,23 @@ exports.collectExternalPackageSources = collectExternalPackageSources;
1136
1587
  exports.contentHash = contentHash;
1137
1588
  exports.createV2EngineApi = createV2EngineApi;
1138
1589
  exports.discoverFiles = discoverFiles;
1590
+ exports.enforceExternalTokenContracts = enforceExternalTokenContracts;
1139
1591
  exports.extractSystemFilePackages = extractSystemFilePackages;
1592
+ exports.findAssetSpecifiers = findAssetSpecifiers;
1140
1593
  exports.findPackageRoot = findPackageRoot;
1141
1594
  exports.formatRustTimingWaterfall = formatRustTimingWaterfall;
1142
1595
  exports.loadSystemConfig = loadSystemConfig;
1143
1596
  exports.postProcessCss = postProcessCss;
1144
1597
  exports.preprocessMdx = preprocessMdx;
1145
1598
  exports.readTsconfigAliasPairs = readTsconfigAliasPairs;
1599
+ exports.resolveAssetFile = resolveAssetFile;
1146
1600
  exports.resolveLightningTargets = resolveLightningTargets;
1601
+ exports.resolveThroughPathAliases = resolveThroughPathAliases;
1147
1602
  exports.runProjectAnalysis = runProjectAnalysis;
1148
1603
  exports.serializeStaticCss = serializeStaticCss;
1604
+ exports.staleDistIncludesMessage = staleDistIncludesMessage;
1149
1605
  exports.stripLeadingLayerDeclaration = stripLeadingLayerDeclaration;
1606
+ exports.substituteAssetPlaceholders = substituteAssetPlaceholders;
1150
1607
  exports.surfaceManifestDiagnostics = surfaceManifestDiagnostics;
1151
1608
  exports.toWatchKeys = toWatchKeys;
1152
1609
  exports.unresolvableIncludesMessage = unresolvableIncludesMessage;