@animus-ui/extract 0.1.22 → 0.1.23

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.
@@ -131,7 +131,167 @@ function parseInternalWire(json, wire) {
131
131
  return parsed;
132
132
  }
133
133
  //#endregion
134
+ //#region pipeline/asset-placeholders.ts
135
+ const ASSET_PLACEHOLDER_PREFIX = "animus-asset:";
136
+ const URL_OPEN_RE = /url\(\s*(['"]?)/gi;
137
+ const IMAGE_SET_OPEN_RE = /(?:-webkit-)?image-set\(/gi;
138
+ const BARE_SPECIFIER_RE = /^[^'")\s]+/;
139
+ /** The index of the `)` that closes the `(` at `open`, outside strings. */
140
+ function closingParen(css, open) {
141
+ let depth = 0;
142
+ let quote = null;
143
+ for (let i = open; i < css.length; i += 1) {
144
+ const c = css[i];
145
+ if (quote !== null) {
146
+ if (c === quote) quote = null;
147
+ } else if (c === "\"" || c === "'") quote = c;
148
+ else if (c === "(") depth += 1;
149
+ else if (c === ")") {
150
+ depth -= 1;
151
+ if (depth === 0) return i;
152
+ }
153
+ }
154
+ return css.length;
155
+ }
156
+ function quotedReference(css, quoteAt) {
157
+ const start = quoteAt + 1;
158
+ if (!css.startsWith("animus-asset:", start)) return null;
159
+ const end = css.indexOf(css[quoteAt], start);
160
+ if (end === -1) return null;
161
+ return {
162
+ start,
163
+ end,
164
+ specifier: css.slice(start + 13, end)
165
+ };
166
+ }
167
+ function assetReferences(css) {
168
+ if (!css.includes("animus-asset:")) return [];
169
+ const references = [];
170
+ for (const open of css.matchAll(URL_OPEN_RE)) {
171
+ const at = (open.index ?? 0) + open[0].length;
172
+ if (open[1]) {
173
+ const reference = quotedReference(css, at - 1);
174
+ if (reference) references.push(reference);
175
+ continue;
176
+ }
177
+ if (!css.startsWith("animus-asset:", at)) continue;
178
+ const specifier = BARE_SPECIFIER_RE.exec(css.slice(at + 13))?.[0];
179
+ if (specifier === void 0) continue;
180
+ const end = at + 13 + specifier.length;
181
+ references.push({
182
+ start: at,
183
+ end,
184
+ specifier
185
+ });
186
+ }
187
+ for (const open of css.matchAll(IMAGE_SET_OPEN_RE)) {
188
+ const paren = (open.index ?? 0) + open[0].length - 1;
189
+ const close = closingParen(css, paren);
190
+ let depth = 0;
191
+ let candidateStart = true;
192
+ for (let i = paren + 1; i < close; i += 1) {
193
+ const c = css[i];
194
+ if (/\s/.test(c)) continue;
195
+ if (c === "\"" || c === "'") {
196
+ if (depth === 0 && candidateStart) {
197
+ const reference = quotedReference(css, i);
198
+ if (reference) references.push(reference);
199
+ }
200
+ const end = css.indexOf(c, i + 1);
201
+ i = end === -1 ? close : end;
202
+ } else if (c === "(") depth += 1;
203
+ else if (c === ")") depth -= 1;
204
+ else if (c === "," && depth === 0) {
205
+ candidateStart = true;
206
+ continue;
207
+ }
208
+ candidateStart = false;
209
+ }
210
+ }
211
+ return references.sort((a, b) => a.start - b.start);
212
+ }
213
+ function findAssetSpecifiers(css) {
214
+ return [...new Set(assetReferences(css).map((reference) => reference.specifier))];
215
+ }
216
+ function substituteAssetPlaceholders(css, urlBySpecifier) {
217
+ if (urlBySpecifier.size === 0) return css;
218
+ let out = css;
219
+ for (const reference of assetReferences(css).reverse()) {
220
+ const url = urlBySpecifier.get(reference.specifier);
221
+ if (url === void 0) continue;
222
+ out = out.slice(0, reference.start) + url + out.slice(reference.end);
223
+ }
224
+ return out;
225
+ }
226
+ /**
227
+ * Specifiers of placeholder text anywhere inside a `url()` or `image-set()`
228
+ * call, a reference or not: after substitution any of it is a broken load.
229
+ */
230
+ function placeholdersInAssetCalls(css) {
231
+ if (!css.includes("animus-asset:")) return [];
232
+ const calls = [...css.matchAll(URL_OPEN_RE), ...css.matchAll(IMAGE_SET_OPEN_RE)].map((open) => {
233
+ const paren = css.indexOf("(", open.index ?? 0);
234
+ return [paren, closingParen(css, paren)];
235
+ });
236
+ const found = /* @__PURE__ */ new Set();
237
+ for (let at = css.indexOf(ASSET_PLACEHOLDER_PREFIX); at !== -1; at = css.indexOf(ASSET_PLACEHOLDER_PREFIX, at + 1)) {
238
+ if (!calls.some(([open, close]) => open < at && at < close)) continue;
239
+ found.add(/^[^'")\s,]*/.exec(css.slice(at + 13))[0]);
240
+ }
241
+ return [...found];
242
+ }
243
+ function findSheetAssetSpecifiers(sheets) {
244
+ return [.../* @__PURE__ */ new Set([
245
+ ...findAssetSpecifiers(sheets.variableCss),
246
+ ...findAssetSpecifiers(sheets.globalCss),
247
+ ...findAssetSpecifiers(sheets.componentCss)
248
+ ])];
249
+ }
250
+ /** The one substitution step every emitted sheet passes through. */
251
+ function substituteSheetAssets(sheets, urlBySpecifier) {
252
+ return {
253
+ variableCss: substituteAssetPlaceholders(sheets.variableCss, urlBySpecifier),
254
+ globalCss: substituteAssetPlaceholders(sheets.globalCss, urlBySpecifier),
255
+ componentCss: substituteAssetPlaceholders(sheets.componentCss, urlBySpecifier)
256
+ };
257
+ }
258
+ const UNSUBSTITUTED_ASSET_CODE = "animus.asset.unsubstituted-placeholder";
259
+ /** A placeholder in emitted CSS is a broken URL in the browser: it fails a
260
+ * strict build and warns otherwise. */
261
+ function reportSurvivingAssetPlaceholders(css, report) {
262
+ const specifiers = placeholdersInAssetCalls(css);
263
+ if (specifiers.length === 0) return;
264
+ const message = `${report.prefix} asset() placeholders reached ${report.surface ?? "emitted CSS"} unsubstituted: ${specifiers.join(", ")} (${UNSUBSTITUTED_ASSET_CODE})`;
265
+ if (report.strict) throw new Error(message);
266
+ report.warn(message);
267
+ }
268
+ /**
269
+ * The runtime code an analysis generates: each dynamic prop config, which the
270
+ * shared prop map module carries, and each component's replacement. Extraction
271
+ * lifts asset() out of them into root variables, so none should hold one.
272
+ * Both embed values as JSON strings, so their quote and backslash escapes are
273
+ * undone for scanning: the text is for `findAssetSpecifiers`, not to run.
274
+ */
275
+ function generatedModuleCode(manifest) {
276
+ return [JSON.stringify(manifest.dynamic_props), ...Object.values(manifest.components).map((component) => component.replacement)].join("\n").replace(/\\(["\\])/g, "$1");
277
+ }
278
+ //#endregion
134
279
  //#region pipeline/manifest-diagnostics.ts
280
+ /** `file`, then its line and column where the record has them. */
281
+ function locationOf(diagnostic) {
282
+ if (diagnostic.line === void 0) return diagnostic.file;
283
+ if (diagnostic.column === void 0) return `${diagnostic.file}:${diagnostic.line}`;
284
+ return `${diagnostic.file}:${diagnostic.line}:${diagnostic.column}`;
285
+ }
286
+ /** A located record's `file:line:column: `; nothing for one without a line,
287
+ * whose message names its file. */
288
+ function locatedPrefix(diagnostic) {
289
+ return diagnostic.line === void 0 ? "" : `${locationOf(diagnostic)}: `;
290
+ }
291
+ /** What the record dropped, when its message does not already quote it. */
292
+ function droppedSuffix(diagnostic) {
293
+ return diagnostic.dropped && !diagnostic.message.includes(diagnostic.dropped) ? ` — dropped: ${diagnostic.dropped}` : "";
294
+ }
135
295
  /** Stable code for selector forms with no substitutable subject; the
136
296
  * extraction engine mints the same string. */
137
297
  const SELECTOR_UNSUPPORTED_SUBJECT = "animus.selector.unsupported-subject";
@@ -144,6 +304,63 @@ function isUnresolvedParentDrop(diagnostic) {
144
304
  function unresolvedParentName(diagnostic) {
145
305
  return UNRESOLVED_PARENT_RE.exec(diagnostic.message)?.[1] ?? null;
146
306
  }
307
+ const DIAGNOSTIC_LEVELS = /* @__PURE__ */ new Set([
308
+ "off",
309
+ "info",
310
+ "warn",
311
+ "error"
312
+ ]);
313
+ /** The level `levels` sets for `code`: its exact entry, else the entry of
314
+ * the longest prefix it starts with. */
315
+ function levelFor(code, levels) {
316
+ if (code === void 0 || levels === void 0) return void 0;
317
+ if (Object.hasOwn(levels, code)) return levels[code];
318
+ let longest;
319
+ for (const [key, level] of Object.entries(levels)) {
320
+ if (!key.endsWith(".*")) continue;
321
+ const prefix = key.slice(0, -1);
322
+ if (code.startsWith(prefix) && prefix.length > (longest?.length ?? -1)) longest = {
323
+ length: prefix.length,
324
+ level
325
+ };
326
+ }
327
+ return longest?.level;
328
+ }
329
+ /** The level a record prints at: the option's entry for its code, else
330
+ * `error` for an error-severity record under `strict`, else its own. */
331
+ function effectiveLevel(diagnostic, policy) {
332
+ const level = levelFor(diagnostic.code, policy.levels);
333
+ if (level !== void 0) return level;
334
+ if (diagnostic.severity === "info") return "info";
335
+ return policy.strict && diagnostic.severity === "error" ? "error" : "warn";
336
+ }
337
+ /** Every code Animus is known to report: the engine's table, as the native
338
+ * module publishes it (`diagnosticCodes()`), the pipeline's, and the asset
339
+ * code. The system package mints vocabulary codes at run time, so a valid
340
+ * code can be missing. */
341
+ function knownDiagnosticCodes(engineCodesJson) {
342
+ const engineCodes = engineCodesJson ? Object.keys(parseInternalWire(engineCodesJson, "diagnosticCodes (the engine code table)")) : [];
343
+ return /* @__PURE__ */ new Set([
344
+ ...engineCodes,
345
+ ...DIAGNOSTIC_SEVERITY.keys(),
346
+ UNSUBSTITUTED_ASSET_CODE
347
+ ]);
348
+ }
349
+ /** The keys of `levels` that name no known code, exactly or as a prefix. */
350
+ function unknownDiagnosticKeys(levels, known) {
351
+ return Object.keys(levels).filter((key) => {
352
+ if (!key.endsWith(".*")) return !known.has(key);
353
+ const prefix = key.slice(0, -1);
354
+ return ![...known].some((code) => code.startsWith(prefix));
355
+ });
356
+ }
357
+ /** The failure of diagnostics at `error` level, from `strict` or the
358
+ * `diagnostics` option: a host fails the build on it whatever its own
359
+ * strictness. It prints as the plain `Error` strict always threw. */
360
+ var DiagnosticFailure = class extends Error {};
361
+ /** Level options already checked against the known codes: one warning per
362
+ * unknown key, however often a host analyzes. */
363
+ const checkedLevels = /* @__PURE__ */ new WeakSet();
147
364
  /** True when the selector carries at least one substitutable `&` subject
148
365
  * outside quoted text. */
149
366
  function hasSelectorSubject(value) {
@@ -186,6 +403,9 @@ function collectSelectorAliasDiagnostics(selectorAliasesJson) {
186
403
  const UNREADABLE_SOURCE_FILE = "animus.ingestion.unreadable-source-file";
187
404
  /** A kit the system extends resolved, yet discovery found none of its files. */
188
405
  const NO_KIT_FILES = "animus.discovery.no-kit-files";
406
+ /** A system file's `createSystem` binding that discovery could not follow to
407
+ * Animus's factory, so it is no root. */
408
+ const UNPROVEN_ROOT_BINDING = "animus.discovery.unproven-root-binding";
189
409
  /** What a skipped source file costs: the analysis never sees what it
190
410
  * renders. */
191
411
  const SKIPPED_SOURCE_COST = "skipped: its renders are not seen, so nothing is pruned and every component, variant option and state is kept until it is analyzed";
@@ -262,6 +482,7 @@ const DIAGNOSTIC_SEVERITY = /* @__PURE__ */ new Map([
262
482
  [SELECTOR_UNSUPPORTED_SUBJECT, "error"],
263
483
  [UNREADABLE_SOURCE_FILE, "error"],
264
484
  [NO_KIT_FILES, "warn"],
485
+ [UNPROVEN_ROOT_BINDING, "warn"],
265
486
  [VOCABULARY_COLLISION, "warn"],
266
487
  [VOCABULARY_LEGACY_VERB, "warn"],
267
488
  [INVALID_PROPERTY_REGISTRATION, "error"],
@@ -386,25 +607,33 @@ function tokenCollisionDiagnostics(system) {
386
607
  */
387
608
  function surfaceManifestDiagnostics(manifest, warn, policy = {}) {
388
609
  const errors = [];
610
+ if (policy.levels && policy.knownCodes && !checkedLevels.has(policy.levels)) {
611
+ checkedLevels.add(policy.levels);
612
+ for (const key of unknownDiagnosticKeys(policy.levels, policy.knownCodes)) warn(`⚠ diagnostics option: '${key}' matches no Animus diagnostic code — check its spelling (a code the system package mints at run time is known only once it is reported)`);
613
+ }
389
614
  const diagnostics = policy.prepend?.length ? [...policy.prepend, ...manifest.diagnostics ?? []] : manifest.diagnostics ?? [];
390
615
  for (const diagnostic of diagnostics) {
616
+ const level = effectiveLevel(diagnostic, policy);
617
+ if (level === "off") continue;
391
618
  let line = null;
392
- if (diagnostic.kind === "bail") line = `⚠ ${diagnostic.component} not extracted: ${diagnostic.message}`;
393
- else if (diagnostic.kind === "skip") line = `⚠ ${diagnostic.component}: skipped ${diagnostic.message}`;
394
- else if (diagnostic.kind === "warn") line = `${diagnostic.severity === "info" ? "ℹ" : "⚠"} ${diagnostic.file}: ${diagnostic.component}: ${diagnostic.message}`;
619
+ const mark = level === "info" ? "ℹ" : "⚠";
620
+ const message = `${diagnostic.message}${droppedSuffix(diagnostic)}`;
621
+ if (diagnostic.kind === "bail") line = `${mark} ${locatedPrefix(diagnostic)}${diagnostic.component} not extracted: ${message}`;
622
+ else if (diagnostic.kind === "skip") line = `${mark} ${locatedPrefix(diagnostic)}${diagnostic.component}: skipped ${message}`;
623
+ else if (diagnostic.kind === "warn") line = `${mark} ${locationOf(diagnostic)}: ${diagnostic.component}: ${message}`;
395
624
  if (line === null) continue;
396
625
  if (diagnostic.code && !diagnostic.message.includes(diagnostic.code)) line += ` [${diagnostic.code}]`;
397
- if (diagnostic.severity === "info") {
626
+ if (level === "info") {
398
627
  policy.info?.(line);
399
628
  continue;
400
629
  }
401
- if (policy.strict && diagnostic.severity === "error") {
402
- errors.push(`${diagnostic.code ?? "error"} — ${diagnostic.component}: ${diagnostic.message}`);
630
+ if (level === "error") {
631
+ errors.push(`${diagnostic.code ?? "error"} — ${locatedPrefix(diagnostic)}${diagnostic.component}: ${message}`);
403
632
  continue;
404
633
  }
405
634
  warn(line);
406
635
  }
407
- if (errors.length > 0) throw new Error(`[animus] strict: ${errors.length} error diagnostic(s):\n${errors.join("\n")}`);
636
+ if (errors.length > 0) throw new DiagnosticFailure(`[animus] strict: ${errors.length} error diagnostic(s):\n${errors.join("\n")}`);
408
637
  }
409
638
  //#endregion
410
639
  //#region pipeline/mdx-preprocessor.ts
@@ -1056,17 +1285,17 @@ const NODE_NEXT_EXTENSION_MAP = {
1056
1285
  ".mjs": [".mts"],
1057
1286
  ".cjs": [".cts"]
1058
1287
  };
1059
- function resolveRelativeSource(importerPath, specifier, files) {
1060
- if (!specifier.startsWith(".")) return null;
1288
+ /** The paths a relative specifier can name from `importerPath`, in probe
1289
+ * order: as written with each source suffix, then a NodeNext emitted
1290
+ * extension's sources. Empty for a non-relative specifier. */
1291
+ function relativeSourceCandidates(importerPath, specifier) {
1292
+ if (!specifier.startsWith(".")) return [];
1061
1293
  const base = node_path.posix.normalize(node_path.posix.join(node_path.posix.dirname(canonicalResolverPath(importerPath)), specifier));
1062
- for (const suffix of RELATIVE_PROBE_SUFFIXES) {
1063
- const candidate = `${base}${suffix}`;
1064
- const actualPath = files.get(candidate);
1065
- if (actualPath !== void 0) return actualPath;
1066
- }
1067
1294
  const explicitExtension = extension(base);
1068
- for (const sourceExtension of NODE_NEXT_EXTENSION_MAP[explicitExtension] ?? []) {
1069
- const candidate = `${base.slice(0, -explicitExtension.length)}${sourceExtension}`;
1295
+ return [...RELATIVE_PROBE_SUFFIXES.map((suffix) => `${base}${suffix}`), ...(NODE_NEXT_EXTENSION_MAP[explicitExtension] ?? []).map((sourceExtension) => `${base.slice(0, -explicitExtension.length)}${sourceExtension}`)];
1296
+ }
1297
+ function resolveRelativeSource(importerPath, specifier, files) {
1298
+ for (const candidate of relativeSourceCandidates(importerPath, specifier)) {
1070
1299
  const actualPath = files.get(candidate);
1071
1300
  if (actualPath !== void 0) return actualPath;
1072
1301
  }
@@ -1412,6 +1641,7 @@ function createV2EngineApi(deps) {
1412
1641
  return {
1413
1642
  loadSystemModule: (...args) => native.loadSystemModule(...args),
1414
1643
  extractFacts: (filesJson) => native.extractFacts(filesJson),
1644
+ diagnosticCodes: () => native.diagnosticCodes(),
1415
1645
  analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson, transformSourcesJson, transformProvenanceJson, declarationScalesJson, analysisContextJson) => {
1416
1646
  const filesJson = deps.rehydrateFilesJson ? deps.rehydrateFilesJson(filesJsonRaw) : filesJsonRaw;
1417
1647
  const sent = /* @__PURE__ */ new Map();
@@ -1477,6 +1707,10 @@ function assertNoRetiredEngineSelection(engineOption) {
1477
1707
  }
1478
1708
  //#endregion
1479
1709
  //#region pipeline/core-options.ts
1710
+ /**
1711
+ * One option schema shared by the Vite, Next, and CLI drivers: key semantics,
1712
+ * default exclusions, and mode authority cannot drift per driver.
1713
+ */
1480
1714
  const DRIVER_NAMESPACES = [
1481
1715
  "vite",
1482
1716
  "next",
@@ -1510,6 +1744,7 @@ const CORE_OPTION_KEYS = /* @__PURE__ */ new Set([
1510
1744
  "exclude",
1511
1745
  "extensions",
1512
1746
  "strict",
1747
+ "diagnostics",
1513
1748
  "verbose",
1514
1749
  "prefix",
1515
1750
  "prefixContextualVars",
@@ -1569,6 +1804,7 @@ const isString = (value) => Object(value) !== value && Object.prototype.toString
1569
1804
  const isBoolean = (value) => Object(value) !== value && Object.prototype.toString.call(value) === "[object Boolean]";
1570
1805
  const isVerbose = (value) => isBoolean(value) || value === "trace";
1571
1806
  const isStringArray = (value) => Array.isArray(value) && value.every(isString);
1807
+ const isDiagnosticLevels = (value) => Object.prototype.toString.call(value) === "[object Object]" && Object.values(Object(value)).every((level) => isString(level) && DIAGNOSTIC_LEVELS.has(level));
1572
1808
  const isStringOrStringArray = (value) => isString(value) || isStringArray(value);
1573
1809
  const CORE_VALUE_GATES = [
1574
1810
  {
@@ -1591,6 +1827,11 @@ const CORE_VALUE_GATES = [
1591
1827
  ok: isBoolean,
1592
1828
  expected: "a boolean"
1593
1829
  },
1830
+ {
1831
+ key: "diagnostics",
1832
+ ok: isDiagnosticLevels,
1833
+ expected: "an object mapping codes to 'off', 'info', 'warn' or 'error'"
1834
+ },
1594
1835
  {
1595
1836
  key: "verbose",
1596
1837
  ok: isVerbose,
@@ -2419,10 +2660,10 @@ function staleDistIncludesMessage(outcomes) {
2419
2660
  if (stale.length === 0) return null;
2420
2661
  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`;
2421
2662
  }
2422
- /** The engine's own parse of a module's imports, through its
2663
+ /** The engine's own parse of a module's bindings, through its
2423
2664
  * `extractFacts`; undefined for an engine without one. A parse that
2424
- * panicked reports no imports, so it counts as no parse. */
2425
- function engineImportParser(engine) {
2665
+ * panicked counts as no parse. */
2666
+ function engineModuleParser(engine) {
2426
2667
  const { extractFacts } = engine;
2427
2668
  if (!extractFacts) return void 0;
2428
2669
  return (source, path$3) => {
@@ -2431,36 +2672,200 @@ function engineImportParser(engine) {
2431
2672
  path: path$3,
2432
2673
  source
2433
2674
  }])), "extractFacts").files[path$3];
2434
- return facts && !facts.parsePanicked ? facts.imports : null;
2675
+ return facts && !facts.parsePanicked ? {
2676
+ imports: facts.imports,
2677
+ exports: facts.exports
2678
+ } : null;
2435
2679
  } catch {
2436
2680
  return null;
2437
2681
  }
2438
2682
  };
2439
2683
  }
2684
+ const ANIMUS_SYSTEM_SPECIFIER = /^@animus-ui\/system(?:\/.*)?$/;
2685
+ /** The file a specifier names from `importer`: a relative one by the
2686
+ * ingestion's probe order, a bare one by the host's package resolution. */
2687
+ async function resolveModuleFile(importer, specifier, resolvePackage) {
2688
+ if (specifier.startsWith(".")) return relativeSourceCandidates(importer, specifier).find(isFile) ?? null;
2689
+ try {
2690
+ return await resolvePackage?.(specifier) ?? null;
2691
+ } catch {
2692
+ return null;
2693
+ }
2694
+ }
2695
+ const escapeRegExp$1 = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2696
+ /**
2697
+ * Follows `name`, exported by the module `specifier` names from `importer`,
2698
+ * through re-exports to its declaration. It is Animus's factory only when
2699
+ * that is `createSystem` of `@animus-ui/system`, and another identity only
2700
+ * when a module is shown to declare it; anything discovery cannot follow is
2701
+ * unknown.
2702
+ */
2703
+ async function exportIdentity(importer, specifier, name, following, seen) {
2704
+ if (ANIMUS_SYSTEM_SPECIFIER.test(specifier)) return name === "createSystem" ? { kind: "animus" } : {
2705
+ kind: "other",
2706
+ what: `'${name}' of ${specifier}`
2707
+ };
2708
+ const file = await resolveModuleFile(importer, specifier, following.resolvePackage);
2709
+ if (file === null) return {
2710
+ kind: "unknown",
2711
+ why: `'${specifier}' could not be resolved`
2712
+ };
2713
+ const key = `${file}#${name}`;
2714
+ if (seen.has(key)) return {
2715
+ kind: "unknown",
2716
+ why: `'${specifier}' re-exports it in a cycle`
2717
+ };
2718
+ seen.add(key);
2719
+ let source;
2720
+ try {
2721
+ source = (0, fs.readFileSync)(file, "utf-8");
2722
+ } catch {
2723
+ return {
2724
+ kind: "unknown",
2725
+ why: `'${specifier}' could not be read`
2726
+ };
2727
+ }
2728
+ const record = following.parseModule(source, file);
2729
+ if (record === null) return {
2730
+ kind: "unknown",
2731
+ why: `'${specifier}' could not be parsed`
2732
+ };
2733
+ const exported = record.exports.find((entry) => entry.exported === name);
2734
+ if (exported?.source) return exportIdentity(file, exported.source, exported.original ?? name, following, seen);
2735
+ const local = exported?.local ?? name;
2736
+ const imported = record.imports.find((entry) => entry.local === local);
2737
+ if (exported && imported) return exportIdentity(file, imported.source, imported.imported, following, seen);
2738
+ if (exported || new RegExp(`\\bexport\\s+(?:async\\s+)?(?:function\\*?|class|const|let|var)\\s+${escapeRegExp$1(name)}\\b`).test(source)) return {
2739
+ kind: "other",
2740
+ what: `the '${name}' that '${specifier}' declares`
2741
+ };
2742
+ const stars = [];
2743
+ for (const match of source.matchAll(/\bexport\s*\*\s*from\s*['"]([^'"]+)['"]/g)) stars.push(await exportIdentity(file, match[1], name, following, seen));
2744
+ const definite = stars.filter((identity) => identity.kind !== "unknown");
2745
+ if (definite.length === 1) return definite[0];
2746
+ if (definite.length > 1) return {
2747
+ kind: "unknown",
2748
+ why: `more than one \`export *\` of '${specifier}' provides '${name}'`
2749
+ };
2750
+ return stars.find((identity) => identity.kind === "unknown") ?? {
2751
+ kind: "unknown",
2752
+ why: `'${specifier}' has no export '${name}' discovery can read`
2753
+ };
2754
+ }
2755
+ /** 1-based line and column of `offset` in `source`. */
2756
+ function locationIn(source, offset) {
2757
+ const before = source.slice(0, offset);
2758
+ const lineStart = before.lastIndexOf("\n") + 1;
2759
+ return {
2760
+ line: before.split("\n").length,
2761
+ column: offset - lineStart + 1
2762
+ };
2763
+ }
2764
+ const NAMESPACE_IMPORT = /\bimport\s+(?:[a-zA-Z_$][a-zA-Z0-9_$]*\s*,\s*)?\*\s*as\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s+from\s+['"]([^'"]+)['"]/g;
2765
+ /**
2766
+ * The callees that are Animus's `createSystem` in a parsed system file. A
2767
+ * binding counts once its identity is followed to the factory: a named
2768
+ * import, through local and package re-exports; a namespace import's
2769
+ * member; or a property destructured from a namespace import, `require` or
2770
+ * `import()` of a module. A binding named `createSystem` shown to be
2771
+ * something else is no root. One whose identity discovery cannot follow
2772
+ * keeps the admission spelling gave it, so a resolution limit never drops a
2773
+ * kit. Both are reported. The bare name also anchors, as a global, unless
2774
+ * the file binds it.
2775
+ */
2776
+ async function systemRoots(systemFilePath, source, imports, following) {
2777
+ const roots = /* @__PURE__ */ new Set();
2778
+ const identityOf = (specifier, name) => exportIdentity(systemFilePath, specifier, name, following, /* @__PURE__ */ new Set());
2779
+ const called = (callee) => new RegExp(`(?<![a-zA-Z0-9_$.])${escapeRegExp$1(callee)}\\s*\\(`).test(source);
2780
+ /** Admits a binding named `createSystem` by its identity: an unknown one
2781
+ * keeps `spellingAdmits`, and every unproven one is reported. */
2782
+ const admit = (local, offset, identity, spellingAdmits) => {
2783
+ if (identity.kind === "animus") {
2784
+ roots.add(local);
2785
+ return;
2786
+ }
2787
+ if (identity.kind === "unknown" && spellingAdmits) roots.add(local);
2788
+ const shown = identity.kind === "other" ? `it is ${identity.what}, so it is not read as a system root` : `discovery cannot follow what it is bound to (${identity.why}), so it is ${spellingAdmits ? "still read" : "not read"} as a system root by its name alone`;
2789
+ following.report?.({
2790
+ file: systemFilePath,
2791
+ component: local,
2792
+ kind: "warn",
2793
+ message: `'${local}' is not proven to be Animus's createSystem: ${shown} — import createSystem from '@animus-ui/system', directly or through a re-export discovery can follow`,
2794
+ code: UNPROVEN_ROOT_BINDING,
2795
+ severity: severityFor(UNPROVEN_ROOT_BINDING),
2796
+ ...locationIn(source, offset)
2797
+ });
2798
+ };
2799
+ let bindsName = false;
2800
+ for (const binding of imports) {
2801
+ const named = binding.imported === "createSystem" || binding.local === "createSystem";
2802
+ if (binding.local === "createSystem") bindsName = true;
2803
+ if (named) {
2804
+ const statement = new RegExp(`\\bimport\\b[^;]*?\\bfrom\\s*['"]${escapeRegExp$1(binding.source)}['"]`, "g");
2805
+ let offset = 0;
2806
+ for (const match of source.matchAll(statement)) {
2807
+ const at = match[0].search(new RegExp(`(?<![a-zA-Z0-9_$])${escapeRegExp$1(binding.local)}\\b`));
2808
+ if (at !== -1) {
2809
+ offset = match.index + at;
2810
+ break;
2811
+ }
2812
+ }
2813
+ admit(binding.local, offset, await identityOf(binding.source, binding.imported), binding.imported === "createSystem");
2814
+ continue;
2815
+ }
2816
+ if (binding.source.startsWith(".") && called(binding.local) && (await identityOf(binding.source, binding.imported)).kind === "animus") roots.add(binding.local);
2817
+ }
2818
+ const namespaces = /* @__PURE__ */ new Map();
2819
+ for (const [, namespace, specifier] of source.matchAll(NAMESPACE_IMPORT)) {
2820
+ namespaces.set(namespace, specifier);
2821
+ const member = `${namespace}.createSystem`;
2822
+ if (ANIMUS_SYSTEM_SPECIFIER.test(specifier) || specifier.startsWith(".") && called(member) && (await identityOf(specifier, "createSystem")).kind === "animus") roots.add(member);
2823
+ }
2824
+ const destructuredFrom = (initializer) => {
2825
+ const loaded = /^\(?\s*(?:(?:await\s+)?import|require)\(\s*['"]([^'"]+)['"]\s*\)\s*\)?$/.exec(initializer);
2826
+ if (loaded) return identityOf(loaded[1], "createSystem");
2827
+ const namespace = namespaces.get(initializer);
2828
+ if (namespace !== void 0) return identityOf(namespace, "createSystem");
2829
+ return Promise.resolve({
2830
+ kind: "unknown",
2831
+ why: `it is destructured from '${initializer}', which is not a module namespace`
2832
+ });
2833
+ };
2834
+ for (const match of source.matchAll(/\b(?:const|let|var)\s*\{([^}]*)\}\s*=\s*([^;\n]+)/g)) {
2835
+ const [whole, pattern, initializer] = match;
2836
+ for (const property of pattern.split(",")) {
2837
+ const binding = /^\s*createSystem\s*(?::\s*([a-zA-Z_$][a-zA-Z0-9_$]*))?\s*(?:=[\s\S]*)?$/.exec(property);
2838
+ if (!binding) continue;
2839
+ const local = binding[1] ?? "createSystem";
2840
+ if (local === "createSystem") bindsName = true;
2841
+ admit(local, match.index + whole.indexOf("createSystem"), await destructuredFrom(initializer.trim()), local === "createSystem");
2842
+ }
2843
+ }
2844
+ const declaresName = /\b(?:function\*?|class)\s+createSystem\b|\b(?:const|let|var)\s+createSystem\b/.test(source);
2845
+ if (!bindsName && !declaresName) roots.add("createSystem");
2846
+ return roots;
2847
+ }
2440
2848
  /** A pattern matching a call of any of `callees` as a whole name, or null
2441
2849
  * when there is none to anchor on. */
2442
2850
  function rootCallPattern(callees) {
2443
2851
  if (callees.length === 0) return null;
2444
2852
  return `(?<![a-zA-Z0-9_$.])(?:${callees.map((callee) => callee.replace(/[.$]/g, "\\$&")).join("|")})`;
2445
2853
  }
2446
- function extractSystemFilePackages(systemFilePath, parseImports) {
2854
+ async function extractSystemFilePackages(systemFilePath, parseModule, report, resolvePackage) {
2447
2855
  let source;
2448
2856
  try {
2449
2857
  source = (0, fs.readFileSync)(systemFilePath, "utf-8");
2450
2858
  } catch {
2451
2859
  return [];
2452
2860
  }
2453
- const parsed = parseImports?.(source, systemFilePath) ?? null;
2454
- const parsedImports = parsed && parsed.length > 0 ? parsed : null;
2861
+ const parsed = parseModule?.(source, systemFilePath) ?? null;
2862
+ const parsedImports = parsed && parsed.imports.length > 0 ? parsed.imports : null;
2455
2863
  let rootCall = "createSystem";
2456
- if (parsedImports) {
2457
- const shadowed = parsedImports.some((binding) => binding.local === "createSystem" && binding.imported !== "createSystem") || /\b(?:function\*?|class)\s+createSystem\b|\b(?:const|let|var)\s+createSystem\b/.test(source);
2458
- rootCall = rootCallPattern([.../* @__PURE__ */ new Set([
2459
- ...parsedImports.filter((binding) => binding.imported === "createSystem").map((binding) => binding.local),
2460
- ...Array.from(source.matchAll(/\bimport\s+(?:[a-zA-Z_$][a-zA-Z0-9_$]*\s*,\s*)?\*\s*as\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s+from\s+['"]@animus-ui\/system(?:\/[^'"]*)?['"]/g), (match) => `${match[1]}.createSystem`),
2461
- ...shadowed ? [] : ["createSystem"]
2462
- ])]);
2463
- }
2864
+ if (parseModule && parsedImports) rootCall = rootCallPattern([...await systemRoots(systemFilePath, source, parsedImports, {
2865
+ parseModule,
2866
+ resolvePackage,
2867
+ report
2868
+ })]);
2464
2869
  const identifiers = /* @__PURE__ */ new Set();
2465
2870
  const constructorRegex = rootCall === null ? null : new RegExp(`${rootCall}\\s*\\(\\s*\\{[^}]*?\\bincludes\\s*:\\s*\\[([^\\]]*)\\]`, "gs");
2466
2871
  const chainRegex = /\.includes\s*\(\s*\[([^\]]*)\]\s*\)/gs;
@@ -2715,151 +3120,6 @@ function createSourceCorpus(host, ingestor = createSourceIngestor(host)) {
2715
3120
  };
2716
3121
  }
2717
3122
  //#endregion
2718
- //#region pipeline/asset-placeholders.ts
2719
- const ASSET_PLACEHOLDER_PREFIX = "animus-asset:";
2720
- const URL_OPEN_RE = /url\(\s*(['"]?)/gi;
2721
- const IMAGE_SET_OPEN_RE = /(?:-webkit-)?image-set\(/gi;
2722
- const BARE_SPECIFIER_RE = /^[^'")\s]+/;
2723
- /** The index of the `)` that closes the `(` at `open`, outside strings. */
2724
- function closingParen(css, open) {
2725
- let depth = 0;
2726
- let quote = null;
2727
- for (let i = open; i < css.length; i += 1) {
2728
- const c = css[i];
2729
- if (quote !== null) {
2730
- if (c === quote) quote = null;
2731
- } else if (c === "\"" || c === "'") quote = c;
2732
- else if (c === "(") depth += 1;
2733
- else if (c === ")") {
2734
- depth -= 1;
2735
- if (depth === 0) return i;
2736
- }
2737
- }
2738
- return css.length;
2739
- }
2740
- function quotedReference(css, quoteAt) {
2741
- const start = quoteAt + 1;
2742
- if (!css.startsWith("animus-asset:", start)) return null;
2743
- const end = css.indexOf(css[quoteAt], start);
2744
- if (end === -1) return null;
2745
- return {
2746
- start,
2747
- end,
2748
- specifier: css.slice(start + 13, end)
2749
- };
2750
- }
2751
- function assetReferences(css) {
2752
- if (!css.includes("animus-asset:")) return [];
2753
- const references = [];
2754
- for (const open of css.matchAll(URL_OPEN_RE)) {
2755
- const at = (open.index ?? 0) + open[0].length;
2756
- if (open[1]) {
2757
- const reference = quotedReference(css, at - 1);
2758
- if (reference) references.push(reference);
2759
- continue;
2760
- }
2761
- if (!css.startsWith("animus-asset:", at)) continue;
2762
- const specifier = BARE_SPECIFIER_RE.exec(css.slice(at + 13))?.[0];
2763
- if (specifier === void 0) continue;
2764
- const end = at + 13 + specifier.length;
2765
- references.push({
2766
- start: at,
2767
- end,
2768
- specifier
2769
- });
2770
- }
2771
- for (const open of css.matchAll(IMAGE_SET_OPEN_RE)) {
2772
- const paren = (open.index ?? 0) + open[0].length - 1;
2773
- const close = closingParen(css, paren);
2774
- let depth = 0;
2775
- let candidateStart = true;
2776
- for (let i = paren + 1; i < close; i += 1) {
2777
- const c = css[i];
2778
- if (/\s/.test(c)) continue;
2779
- if (c === "\"" || c === "'") {
2780
- if (depth === 0 && candidateStart) {
2781
- const reference = quotedReference(css, i);
2782
- if (reference) references.push(reference);
2783
- }
2784
- const end = css.indexOf(c, i + 1);
2785
- i = end === -1 ? close : end;
2786
- } else if (c === "(") depth += 1;
2787
- else if (c === ")") depth -= 1;
2788
- else if (c === "," && depth === 0) {
2789
- candidateStart = true;
2790
- continue;
2791
- }
2792
- candidateStart = false;
2793
- }
2794
- }
2795
- return references.sort((a, b) => a.start - b.start);
2796
- }
2797
- function findAssetSpecifiers(css) {
2798
- return [...new Set(assetReferences(css).map((reference) => reference.specifier))];
2799
- }
2800
- function substituteAssetPlaceholders(css, urlBySpecifier) {
2801
- if (urlBySpecifier.size === 0) return css;
2802
- let out = css;
2803
- for (const reference of assetReferences(css).reverse()) {
2804
- const url = urlBySpecifier.get(reference.specifier);
2805
- if (url === void 0) continue;
2806
- out = out.slice(0, reference.start) + url + out.slice(reference.end);
2807
- }
2808
- return out;
2809
- }
2810
- /**
2811
- * Specifiers of placeholder text anywhere inside a `url()` or `image-set()`
2812
- * call, a reference or not: after substitution any of it is a broken load.
2813
- */
2814
- function placeholdersInAssetCalls(css) {
2815
- if (!css.includes("animus-asset:")) return [];
2816
- const calls = [...css.matchAll(URL_OPEN_RE), ...css.matchAll(IMAGE_SET_OPEN_RE)].map((open) => {
2817
- const paren = css.indexOf("(", open.index ?? 0);
2818
- return [paren, closingParen(css, paren)];
2819
- });
2820
- const found = /* @__PURE__ */ new Set();
2821
- for (let at = css.indexOf(ASSET_PLACEHOLDER_PREFIX); at !== -1; at = css.indexOf(ASSET_PLACEHOLDER_PREFIX, at + 1)) {
2822
- if (!calls.some(([open, close]) => open < at && at < close)) continue;
2823
- found.add(/^[^'")\s,]*/.exec(css.slice(at + 13))[0]);
2824
- }
2825
- return [...found];
2826
- }
2827
- function findSheetAssetSpecifiers(sheets) {
2828
- return [.../* @__PURE__ */ new Set([
2829
- ...findAssetSpecifiers(sheets.variableCss),
2830
- ...findAssetSpecifiers(sheets.globalCss),
2831
- ...findAssetSpecifiers(sheets.componentCss)
2832
- ])];
2833
- }
2834
- /** The one substitution step every emitted sheet passes through. */
2835
- function substituteSheetAssets(sheets, urlBySpecifier) {
2836
- return {
2837
- variableCss: substituteAssetPlaceholders(sheets.variableCss, urlBySpecifier),
2838
- globalCss: substituteAssetPlaceholders(sheets.globalCss, urlBySpecifier),
2839
- componentCss: substituteAssetPlaceholders(sheets.componentCss, urlBySpecifier)
2840
- };
2841
- }
2842
- const UNSUBSTITUTED_ASSET_CODE = "animus.asset.unsubstituted-placeholder";
2843
- /** A placeholder in emitted CSS is a broken URL in the browser: it fails a
2844
- * strict build and warns otherwise. */
2845
- function reportSurvivingAssetPlaceholders(css, report) {
2846
- const specifiers = placeholdersInAssetCalls(css);
2847
- if (specifiers.length === 0) return;
2848
- const message = `${report.prefix} asset() placeholders reached ${report.surface ?? "emitted CSS"} unsubstituted: ${specifiers.join(", ")} (${UNSUBSTITUTED_ASSET_CODE})`;
2849
- if (report.strict) throw new Error(message);
2850
- report.warn(message);
2851
- }
2852
- /**
2853
- * The runtime code an analysis generates: each dynamic prop config, which the
2854
- * shared prop map module carries, and each component's replacement. Extraction
2855
- * lifts asset() out of them into root variables, so none should hold one.
2856
- * Both embed values as JSON strings, so their quote and backslash escapes are
2857
- * undone for scanning: the text is for `findAssetSpecifiers`, not to run.
2858
- */
2859
- function generatedModuleCode(manifest) {
2860
- return [JSON.stringify(manifest.dynamic_props), ...Object.values(manifest.components).map((component) => component.replacement)].join("\n").replace(/\\(["\\])/g, "$1");
2861
- }
2862
- //#endregion
2863
3123
  //#region pipeline/resolve-asset.ts
2864
3124
  /**
2865
3125
  * Asset resolution for hosts with no bundler hook. Host path aliases apply
@@ -3660,9 +3920,12 @@ function hasSourceThemeManifests(system) {
3660
3920
  const surfacedSystems = /* @__PURE__ */ new WeakSet();
3661
3921
  /** The loaded system's own diagnostics, the first time it is analyzed. A
3662
3922
  * strict error keeps failing every analysis until the system changes. */
3663
- function systemDiagnostics(system, strict) {
3923
+ function systemDiagnostics(system, strict, levels) {
3664
3924
  const diagnostics = [...collectSelectorAliasDiagnostics(system.selectorAliasesJson), ...systemLoadDiagnostics(system)];
3665
- const blocking = strict === true && diagnostics.some((d) => d.severity === "error");
3925
+ const blocking = diagnostics.some((d) => effectiveLevel(d, {
3926
+ strict,
3927
+ levels
3928
+ }) === "error");
3666
3929
  if (surfacedSystems.has(system) && !blocking) return [];
3667
3930
  surfacedSystems.add(system);
3668
3931
  return diagnostics;
@@ -3672,7 +3935,7 @@ function systemDiagnostics(system, strict) {
3672
3935
  * the call site (strict-mode throw vs warn).
3673
3936
  */
3674
3937
  function runProjectAnalysis(engineApi, opts) {
3675
- const { analyzeProject } = engineApi();
3938
+ const { analyzeProject, diagnosticCodes } = engineApi();
3676
3939
  let t = performance.now();
3677
3940
  const inputs = buildAnalysisInputs(opts);
3678
3941
  const serializeMs = Math.round(performance.now() - t);
@@ -3693,8 +3956,10 @@ function runProjectAnalysis(engineApi, opts) {
3693
3956
  surfaceManifestDiagnostics(manifest, opts.warn, {
3694
3957
  strict: opts.strict,
3695
3958
  info: opts.info,
3959
+ levels: opts.diagnostics,
3960
+ knownCodes: opts.diagnostics ? knownDiagnosticCodes(diagnosticCodes?.()) : void 0,
3696
3961
  prepend: [
3697
- ...systemDiagnostics(opts.system, opts.strict),
3962
+ ...systemDiagnostics(opts.system, opts.strict, opts.diagnostics),
3698
3963
  ...propertyDiagnostics,
3699
3964
  ...opts.extraDiagnostics ?? []
3700
3965
  ]
@@ -3960,7 +4225,7 @@ function runStructuralSelfCheck(input) {
3960
4225
  const failures = [];
3961
4226
  if (input.componentCount === 0) failures.push("No component CSS produced — check the system file and its includes list");
3962
4227
  else if (input.assembledCss !== void 0 ? !/@layer\s+anm-base\s*\{/.test(input.assembledCss) : input.componentCss.trim().length === 0) failures.push(`${input.componentCount} component(s) discovered but the emitted component CSS is empty`);
3963
- for (const { specifier, outcome } of input.externalOutcomes ?? []) if (outcome === "empty") failures.push(`include '${specifier}' resolved but discovered no component sources`);
4228
+ for (const { specifier, outcome } of input.externalOutcomes ?? []) if (outcome === "empty") failures.push(`include '${specifier}' resolved but discovered no component sources [${NO_KIT_FILES}]`);
3964
4229
  else if (outcome === "unresolvable") failures.push(`include '${specifier}' could not be resolved`);
3965
4230
  if ((input.variableMapJson == null || Object.keys(parseInternalWire(input.variableMapJson, "variableMapJson (the theme's token → variable map)")).length > 0) && !input.variableCss.includes(":root")) failures.push("No :root variable block found in variable CSS");
3966
4231
  if ((input.assembledCss ?? `${input.variableCss}\n${input.globalCss}\n${input.componentCss}`).includes("__TRANSFORM__")) failures.push("Unresolved __TRANSFORM__ placeholders found in CSS output");
@@ -4157,12 +4422,24 @@ Object.defineProperty(exports, "DEFAULT_EXTENSIONS", {
4157
4422
  return DEFAULT_EXTENSIONS;
4158
4423
  }
4159
4424
  });
4425
+ Object.defineProperty(exports, "DIAGNOSTIC_LEVELS", {
4426
+ enumerable: true,
4427
+ get: function() {
4428
+ return DIAGNOSTIC_LEVELS;
4429
+ }
4430
+ });
4160
4431
  Object.defineProperty(exports, "DRIVER_NAMESPACES", {
4161
4432
  enumerable: true,
4162
4433
  get: function() {
4163
4434
  return DRIVER_NAMESPACES;
4164
4435
  }
4165
4436
  });
4437
+ Object.defineProperty(exports, "DiagnosticFailure", {
4438
+ enumerable: true,
4439
+ get: function() {
4440
+ return DiagnosticFailure;
4441
+ }
4442
+ });
4166
4443
  Object.defineProperty(exports, "ENGINE_TRANSFORM_EXTENSIONS", {
4167
4444
  enumerable: true,
4168
4445
  get: function() {
@@ -4349,10 +4626,10 @@ Object.defineProperty(exports, "enforceExternalTokenContracts", {
4349
4626
  return enforceExternalTokenContracts;
4350
4627
  }
4351
4628
  });
4352
- Object.defineProperty(exports, "engineImportParser", {
4629
+ Object.defineProperty(exports, "engineModuleParser", {
4353
4630
  enumerable: true,
4354
4631
  get: function() {
4355
- return engineImportParser;
4632
+ return engineModuleParser;
4356
4633
  }
4357
4634
  });
4358
4635
  Object.defineProperty(exports, "excludeCollectedPackages", {