@animus-ui/extract 0.1.18 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +2 -2
  2. package/crates/extract-v2/animus-extract-v2.darwin-arm64.node +0 -0
  3. package/crates/extract-v2/animus-extract-v2.linux-arm64-gnu.node +0 -0
  4. package/crates/extract-v2/animus-extract-v2.linux-x64-gnu.node +0 -0
  5. package/crates/extract-v2/index.d.ts +11 -0
  6. package/dist/index.cjs +6 -2
  7. package/dist/index.mjs +2 -2
  8. package/dist/pipeline/analyze-project-args.d.ts +5 -1
  9. package/dist/pipeline/analyze-project-args.d.ts.map +1 -1
  10. package/dist/pipeline/asset-placeholders.d.ts +33 -0
  11. package/dist/pipeline/asset-placeholders.d.ts.map +1 -1
  12. package/dist/pipeline/core-options.d.ts +4 -0
  13. package/dist/pipeline/core-options.d.ts.map +1 -1
  14. package/dist/pipeline/dynamic-prop-config.d.ts +3 -0
  15. package/dist/pipeline/dynamic-prop-config.d.ts.map +1 -1
  16. package/dist/pipeline/engine-adapter.d.ts +1 -1
  17. package/dist/pipeline/engine-adapter.d.ts.map +1 -1
  18. package/dist/pipeline/engine-retirement.d.ts.map +1 -1
  19. package/dist/pipeline/index.d.ts +3 -3
  20. package/dist/pipeline/index.d.ts.map +1 -1
  21. package/dist/pipeline/manifest-diagnostics.d.ts +41 -2
  22. package/dist/pipeline/manifest-diagnostics.d.ts.map +1 -1
  23. package/dist/pipeline/manifest-schema.d.ts +5 -2
  24. package/dist/pipeline/manifest-schema.d.ts.map +1 -1
  25. package/dist/pipeline/prefix.d.ts +35 -0
  26. package/dist/pipeline/prefix.d.ts.map +1 -1
  27. package/dist/pipeline/property-diagnostics.d.ts +18 -0
  28. package/dist/pipeline/property-diagnostics.d.ts.map +1 -0
  29. package/dist/pipeline/property-names.d.ts +42 -0
  30. package/dist/pipeline/property-names.d.ts.map +1 -0
  31. package/dist/pipeline/property-registrations.d.ts +11 -2
  32. package/dist/pipeline/property-registrations.d.ts.map +1 -1
  33. package/dist/pipeline/run-analysis.d.ts +18 -0
  34. package/dist/pipeline/run-analysis.d.ts.map +1 -1
  35. package/dist/pipeline/source-ingestion.d.ts +3 -0
  36. package/dist/pipeline/source-ingestion.d.ts.map +1 -1
  37. package/dist/pipeline/system-config.d.ts +12 -0
  38. package/dist/pipeline/system-config.d.ts.map +1 -1
  39. package/dist/{pipeline-Dr-I4Xhl.mjs → pipeline-6wmD00rr.mjs} +958 -263
  40. package/dist/{pipeline-OKnmnyFu.cjs → pipeline-A54wEQEg.cjs} +982 -263
  41. package/dist/session/extraction-session.d.ts +19 -3
  42. package/dist/session/extraction-session.d.ts.map +1 -1
  43. package/dist/session/index.d.ts +1 -1
  44. package/dist/session/index.d.ts.map +1 -1
  45. package/dist/session/timing.d.ts.map +1 -1
  46. package/dist/session.cjs +60 -22
  47. package/dist/session.mjs +61 -23
  48. package/index-v2.js +1 -1
  49. package/package.json +4 -4
  50. package/dist/pipeline/timing-waterfall.d.ts +0 -6
  51. package/dist/pipeline/timing-waterfall.d.ts.map +0 -1
@@ -7,7 +7,7 @@ import { realpathSync as realpathSync$1 } from "node:fs";
7
7
  import { createRequire as createRequire$1 } from "module";
8
8
  import browserslist from "browserslist";
9
9
  import { browserslistToTargets, transform } from "lightningcss";
10
- import { isUnitlessProperty } from "@animus-ui/properties";
10
+ import { dependsOnContext, foldInitialValue, isUnitlessProperty, substitutesValue } from "@animus-ui/properties";
11
11
  //#region \0rolldown/runtime.js
12
12
  var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
13
13
  //#endregion
@@ -85,7 +85,8 @@ function buildAnalyzeProjectArgs(inputs) {
85
85
  inputs.transformSourcesJson,
86
86
  inputs.transformProvenanceJson ?? null
87
87
  ];
88
- if (inputs.declarationScalesJson) args.push(inputs.declarationScalesJson);
88
+ if (inputs.declarationScalesJson || inputs.analysisContextJson) args.push(inputs.declarationScalesJson);
89
+ if (inputs.analysisContextJson) args.push(inputs.analysisContextJson);
89
90
  return args;
90
91
  }
91
92
  //#endregion
@@ -105,6 +106,224 @@ function parseInternalWire(json, wire) {
105
106
  return parsed;
106
107
  }
107
108
  //#endregion
109
+ //#region pipeline/manifest-diagnostics.ts
110
+ /** Stable code for selector forms with no substitutable subject; the
111
+ * extraction engine mints the same string. */
112
+ const SELECTOR_UNSUPPORTED_SUBJECT = "animus.selector.unsupported-subject";
113
+ /** The one matcher for the engine's unresolved-parent chain-drop message;
114
+ * consumers match through the exported helpers, never their own copy. */
115
+ const UNRESOLVED_PARENT_RE = /chain dropped: could not resolve parent component '([^']+)'/;
116
+ function isUnresolvedParentDrop(diagnostic) {
117
+ return UNRESOLVED_PARENT_RE.test(diagnostic.message);
118
+ }
119
+ function unresolvedParentName(diagnostic) {
120
+ return UNRESOLVED_PARENT_RE.exec(diagnostic.message)?.[1] ?? null;
121
+ }
122
+ /** True when the selector carries at least one substitutable `&` subject
123
+ * outside quoted text. */
124
+ function hasSelectorSubject(value) {
125
+ let quote = null;
126
+ let escaped = false;
127
+ for (const c of value) {
128
+ if (escaped) {
129
+ escaped = false;
130
+ continue;
131
+ }
132
+ if (c === "\\") {
133
+ escaped = true;
134
+ continue;
135
+ }
136
+ if (quote !== null) {
137
+ if (c === quote) quote = null;
138
+ } else if (c === "\"" || c === "'") quote = c;
139
+ else if (c === "&") return true;
140
+ }
141
+ return false;
142
+ }
143
+ /**
144
+ * Coded diagnostics for selector-shaped alias values with no substitutable
145
+ * subject; alias values never reach the evaluator's own key guard.
146
+ */
147
+ function collectSelectorAliasDiagnostics(selectorAliasesJson) {
148
+ if (!selectorAliasesJson) return [];
149
+ const aliases = parseInternalWire(selectorAliasesJson, "selectorAliasesJson (the system loader's selector-alias registry)");
150
+ const diagnostics = [];
151
+ for (const [name, value] of Object.entries(aliases)) if (value.includes("&") && !hasSelectorSubject(value)) diagnostics.push({
152
+ file: "system",
153
+ component: name,
154
+ kind: "warn",
155
+ message: `selector alias '${name}' value '${value}' has no substitutable '&' subject outside quoted text (${SELECTOR_UNSUPPORTED_SUBJECT})`,
156
+ code: SELECTOR_UNSUPPORTED_SUBJECT,
157
+ severity: severityFor(SELECTOR_UNSUPPORTED_SUBJECT)
158
+ });
159
+ return diagnostics;
160
+ }
161
+ const UNREADABLE_SOURCE_FILE = "animus.ingestion.unreadable-source-file";
162
+ /** What a skipped source file costs: the analysis never sees what it
163
+ * renders. */
164
+ 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";
165
+ /**
166
+ * A configured source file that could not be read is a LOST INPUT: the
167
+ * emitted artifacts silently lack whatever that file declared.
168
+ */
169
+ /** Whether a failed source read means the file was deleted after discovery
170
+ * (an editor's delete-and-recreate, a branch switch): a deletion, not lost
171
+ * input. */
172
+ function isDeletedSource(error) {
173
+ return error?.code === "ENOENT";
174
+ }
175
+ function unreadableSourceDiagnostic(relPath, error) {
176
+ return {
177
+ file: relPath,
178
+ component: "source",
179
+ kind: "warn",
180
+ message: `configured source file ${relPath} could not be read: ${String(error)} (${SKIPPED_SOURCE_COST})`,
181
+ code: UNREADABLE_SOURCE_FILE,
182
+ severity: severityFor(UNREADABLE_SOURCE_FILE)
183
+ };
184
+ }
185
+ /** The collision entry code minted by the system package's merge. */
186
+ const VOCABULARY_COLLISION = "animus.vocabulary.collision";
187
+ /** A theme `@property` registration browsers would ignore; it is not emitted. */
188
+ const INVALID_PROPERTY_REGISTRATION = "animus.theme.invalid-property-registration";
189
+ /** Minted when a sealed kit with registered vocabulary is consumed through
190
+ * the deprecated `from()`/`includes:` verbs. */
191
+ const VOCABULARY_LEGACY_VERB = "animus.vocabulary.legacy-verb";
192
+ /** A `var()` fallback that never applies: the property is registered with an
193
+ * initial value. */
194
+ const PROPERTY_FALLBACK_SUPPRESSED = "animus.property.fallback-suppressed";
195
+ /** The same, where the fallback heads a chain of further `var()` reads. */
196
+ const PROPERTY_FALLBACK_CHAIN_SUPPRESSED = "animus.property.fallback-chain-suppressed";
197
+ /** A declared property set in keyframes or transitioned without a typed
198
+ * registration, so it does not interpolate. */
199
+ const PROPERTY_UNREGISTERED_ANIMATION = "animus.property.unregistered-animation";
200
+ /** A custom property whose resolved value is `var()` of itself, which makes
201
+ * it invalid at computed-value time. */
202
+ const PROPERTY_SELF_REFERENCE = "animus.property.self-reference";
203
+ /** A custom property that reads itself only inside another `var()`'s
204
+ * fallback, so whether it is cyclic depends on the browser. */
205
+ const PROPERTY_FALLBACK_SELF_REFERENCE = "animus.property.fallback-self-reference";
206
+ /** A prefix renamed contextual variables without `prefixContextualVars`, so
207
+ * their declared names no longer resolve. */
208
+ const PREFIX_CONTEXTUAL_VARS_UNPREFIXED = "animus.prefix.contextual-vars-unprefixed";
209
+ /** Under `prefixContextualVars`, a final name that collides with a runtime
210
+ * transport variable or a theme variable of the same spelling. */
211
+ const PREFIX_NAME_CONFLICT = "animus.prefix.name-conflict";
212
+ const DIAGNOSTIC_SEVERITY = /* @__PURE__ */ new Map([
213
+ [SELECTOR_UNSUPPORTED_SUBJECT, "error"],
214
+ [UNREADABLE_SOURCE_FILE, "error"],
215
+ [VOCABULARY_COLLISION, "warn"],
216
+ [VOCABULARY_LEGACY_VERB, "warn"],
217
+ [INVALID_PROPERTY_REGISTRATION, "error"],
218
+ [PROPERTY_FALLBACK_SUPPRESSED, "info"],
219
+ [PROPERTY_FALLBACK_CHAIN_SUPPRESSED, "warn"],
220
+ [PROPERTY_UNREGISTERED_ANIMATION, "info"],
221
+ [PROPERTY_SELF_REFERENCE, "warn"],
222
+ [PROPERTY_FALLBACK_SELF_REFERENCE, "warn"],
223
+ [PREFIX_CONTEXTUAL_VARS_UNPREFIXED, "warn"],
224
+ [PREFIX_NAME_CONFLICT, "error"]
225
+ ]);
226
+ /** An unlisted code is `warn`: a witness kind from a newer system package
227
+ * must not fail the strict build of a host that predates its writer. */
228
+ function severityFor(code) {
229
+ return (code === void 0 ? void 0 : DIAGNOSTIC_SEVERITY.get(code)) ?? "warn";
230
+ }
231
+ /**
232
+ * The one mapper of a sealed system's witness entries for every host: the
233
+ * loader's evaluation host shims `console`, so the record is the channel.
234
+ */
235
+ function vocabularyWitnessDiagnostics(vocabularyWitnessesJson) {
236
+ if (!vocabularyWitnessesJson) return [];
237
+ const entries = parseInternalWire(vocabularyWitnessesJson, "vocabularyWitnessesJson (the sealed system's vocabulary witness record)");
238
+ const diagnostics = [];
239
+ for (const entry of entries) if (entry.code === "animus.vocabulary.collision") diagnostics.push({
240
+ file: "system",
241
+ component: entry.name ?? "keyframes",
242
+ kind: "warn",
243
+ message: `keyframes vocabulary "${entry.name}" is registered by both ${entry.loser} and ${entry.winner} — ${entry.winner} wins; rename one collection (${entry.code})`,
244
+ code: entry.code,
245
+ severity: severityFor(entry.code)
246
+ });
247
+ else if (entry.code === "animus.vocabulary.legacy-verb") diagnostics.push({
248
+ file: "system",
249
+ component: "keyframes",
250
+ kind: "warn",
251
+ message: `a sealed system (${entry.source ?? `'${entry.verb}' source`}) with registered vocabulary [${(entry.names ?? []).join(", ")}] was consumed through the deprecated '${entry.verb}' verb, which cannot carry it — those collections do NOT reach this consumer; use createSystem().extend(source) (${entry.code})`,
252
+ code: entry.code,
253
+ severity: severityFor(entry.code)
254
+ });
255
+ else diagnostics.push({
256
+ file: "system",
257
+ component: "vocabulary",
258
+ kind: "warn",
259
+ message: `unrecognized vocabulary witness entry ${JSON.stringify(entry)} — a newer @animus-ui/system may have recorded a witness kind this host predates${entry.code ? ` (${entry.code})` : ""}`,
260
+ code: entry.code,
261
+ severity: severityFor(entry.code)
262
+ });
263
+ return diagnostics;
264
+ }
265
+ /** The diagnostics a loaded system's own records carry: vocabulary
266
+ * witnesses and invalid `@property` registrations. */
267
+ function systemLoadDiagnostics(system) {
268
+ const registrations = (system.invalidPropertyRegistrations ?? []).map(({ name, reason }) => ({
269
+ file: "system",
270
+ component: name,
271
+ kind: "warn",
272
+ message: `@property ${name} was not emitted: ${reason}. Browsers ignore an invalid registration and minifying it fails; fix it where the theme declares ${name} (${INVALID_PROPERTY_REGISTRATION})`,
273
+ code: INVALID_PROPERTY_REGISTRATION,
274
+ severity: severityFor(INVALID_PROPERTY_REGISTRATION)
275
+ }));
276
+ const prefixed = system.legacyPrefixedContextualVars ?? [];
277
+ const unprefixed = prefixed.length === 0 ? [] : [{
278
+ file: "system",
279
+ component: "prefix",
280
+ kind: "warn",
281
+ message: `the prefix renames the contextual variables ${prefixed.join(", ")}, but their declared names are still read and written as written, so scale reads of them fail and their writes miss. Set prefixContextualVars: true to resolve them under the prefix (${PREFIX_CONTEXTUAL_VARS_UNPREFIXED})`,
282
+ code: PREFIX_CONTEXTUAL_VARS_UNPREFIXED,
283
+ severity: severityFor(PREFIX_CONTEXTUAL_VARS_UNPREFIXED)
284
+ }];
285
+ const conflicts = (system.prefixNameConflicts ?? []).map(({ name, final, reason }) => ({
286
+ file: "system",
287
+ component: `--${name}`,
288
+ kind: "error",
289
+ message: reason === "transport" ? `the prefix would emit the contextual variable ${name} as --${final}, inside the --animus- names of the runtime's transport variables, where it can collide with a prop's own variable. Use a different prefix (${PREFIX_NAME_CONFLICT})` : `the theme variable --${name} is spelled like the final name of the contextual variable ${final}, so under prefixContextualVars a reference to --${name} reaches ${final} and the theme variable is unreachable. Rename one of them (${PREFIX_NAME_CONFLICT})`,
290
+ code: PREFIX_NAME_CONFLICT,
291
+ severity: severityFor(PREFIX_NAME_CONFLICT)
292
+ }));
293
+ return [
294
+ ...vocabularyWitnessDiagnostics(system.vocabularyWitnessesJson),
295
+ ...registrations,
296
+ ...unprefixed,
297
+ ...conflicts
298
+ ];
299
+ }
300
+ /**
301
+ * The single strict-escalation policy point for both extraction plugins:
302
+ * error-severity diagnostics throw together under `strict`, else warn.
303
+ */
304
+ function surfaceManifestDiagnostics(manifest, warn, policy = {}) {
305
+ const errors = [];
306
+ const diagnostics = policy.prepend?.length ? [...policy.prepend, ...manifest.diagnostics ?? []] : manifest.diagnostics ?? [];
307
+ for (const diagnostic of diagnostics) {
308
+ let line = null;
309
+ if (diagnostic.kind === "bail") line = `⚠ ${diagnostic.component} not extracted: ${diagnostic.message}`;
310
+ else if (diagnostic.kind === "skip") line = `⚠ ${diagnostic.component}: skipped ${diagnostic.message}`;
311
+ else if (diagnostic.kind === "warn") line = `${diagnostic.severity === "info" ? "ℹ" : "⚠"} ${diagnostic.file}: ${diagnostic.component}: ${diagnostic.message}`;
312
+ if (line === null) continue;
313
+ if (diagnostic.code && !diagnostic.message.includes(diagnostic.code)) line += ` [${diagnostic.code}]`;
314
+ if (diagnostic.severity === "info") {
315
+ policy.info?.(line);
316
+ continue;
317
+ }
318
+ if (policy.strict && diagnostic.severity === "error") {
319
+ errors.push(`${diagnostic.code ?? "error"} — ${diagnostic.component}: ${diagnostic.message}`);
320
+ continue;
321
+ }
322
+ warn(line);
323
+ }
324
+ if (errors.length > 0) throw new Error(`[animus] strict: ${errors.length} error diagnostic(s):\n${errors.join("\n")}`);
325
+ }
326
+ //#endregion
108
327
  //#region pipeline/mdx-preprocessor.ts
109
328
  /**
110
329
  * MDX compiled to scanner-consumable JSX, plus the extension sets every
@@ -1029,7 +1248,8 @@ function withoutInvalidOriginals(result, invalidOriginals) {
1029
1248
  ...result,
1030
1249
  originalEntries: result.originalEntries.filter((entry) => !invalidOriginals.has(entry.path)),
1031
1250
  analysisEntries: result.analysisEntries.filter((entry) => analysisPaths.has(entry.path)),
1032
- ownership
1251
+ ownership,
1252
+ skippedOriginals: [...invalidOriginals].sort()
1033
1253
  };
1034
1254
  }
1035
1255
  /**
@@ -1069,11 +1289,12 @@ function createSourceIngestor(host) {
1069
1289
  const invalidOriginals = new Set(fatal.map((diagnostic) => diagnostic.originalPath));
1070
1290
  if (diagnostics.length === 0) return invalidOriginals;
1071
1291
  if (host.strict() && fatal.length > 0) {
1072
- const lines = fatal.map((diagnostic) => `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}`);
1292
+ const lines = fatal.map((diagnostic) => `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message} (${SKIPPED_SOURCE_COST})`);
1073
1293
  throw new Error(`${host.prefix} ${lines.join(`\n${host.prefix} `)}`);
1074
1294
  }
1075
1295
  for (const diagnostic of diagnostics) {
1076
- const line = `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}`;
1296
+ const skipped = isAdvisorySourceDiagnostic(diagnostic) ? "" : ` (${SKIPPED_SOURCE_COST})`;
1297
+ const line = `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}${skipped}`;
1077
1298
  let warned = warnedByOriginal.get(diagnostic.originalPath);
1078
1299
  if (!warned) {
1079
1300
  warned = /* @__PURE__ */ new Set();
@@ -1108,7 +1329,7 @@ function createV2EngineApi(deps) {
1108
1329
  return {
1109
1330
  loadSystemModule: (...args) => native.loadSystemModule(...args),
1110
1331
  extractFacts: (filesJson) => native.extractFacts(filesJson),
1111
- analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson, transformSourcesJson, transformProvenanceJson, declarationScalesJson) => {
1332
+ analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson, transformSourcesJson, transformProvenanceJson, declarationScalesJson, analysisContextJson) => {
1112
1333
  const filesJson = deps.rehydrateFilesJson ? deps.rehydrateFilesJson(filesJsonRaw) : filesJsonRaw;
1113
1334
  const sent = /* @__PURE__ */ new Map();
1114
1335
  for (const entry of parseFilesJson(filesJson, label)) sent.set(entry.path, entry.source);
@@ -1135,6 +1356,7 @@ function createV2EngineApi(deps) {
1135
1356
  transformSourcesJson: transformSourcesJson ?? void 0,
1136
1357
  transformProvenanceJson: transformProvenanceJson ?? void 0,
1137
1358
  declarationScalesJson,
1359
+ analysisContextJson,
1138
1360
  devMode
1139
1361
  };
1140
1362
  const engine = new native.ExtractEngine(config);
@@ -1166,7 +1388,7 @@ function createV2EngineApi(deps) {
1166
1388
  }
1167
1389
  //#endregion
1168
1390
  //#region pipeline/engine-retirement.ts
1169
- const RETIRED_ENGINE_MESSAGE = "animus: extraction engine 'v1' was retired (openspec: retire-extract-v1) — v2 is the only engine; remove the engine option / ANIMUS_ENGINE override";
1391
+ const RETIRED_ENGINE_MESSAGE = "animus: the 'v1' extraction engine is no longer supported, and v2 is the only engine. Remove the engine option from your Animus config and unset the ANIMUS_ENGINE environment variable.";
1170
1392
  function assertNoRetiredEngineSelection(engineOption) {
1171
1393
  if (engineOption === "v1" || process.env.ANIMUS_ENGINE === "v1") throw new Error(RETIRED_ENGINE_MESSAGE);
1172
1394
  }
@@ -1207,6 +1429,7 @@ const CORE_OPTION_KEYS = /* @__PURE__ */ new Set([
1207
1429
  "strict",
1208
1430
  "verbose",
1209
1431
  "prefix",
1432
+ "prefixContextualVars",
1210
1433
  "targets",
1211
1434
  "minify",
1212
1435
  "staticCss",
@@ -1300,6 +1523,11 @@ const CORE_VALUE_GATES = [
1300
1523
  ok: isString,
1301
1524
  expected: "a string"
1302
1525
  },
1526
+ {
1527
+ key: "prefixContextualVars",
1528
+ ok: isBoolean,
1529
+ expected: "a boolean"
1530
+ },
1303
1531
  {
1304
1532
  key: "targets",
1305
1533
  ok: isStringOrStringArray,
@@ -1998,31 +2226,148 @@ function createSourceCorpus(host, ingestor = createSourceIngestor(host)) {
1998
2226
  //#endregion
1999
2227
  //#region pipeline/asset-placeholders.ts
2000
2228
  const ASSET_PLACEHOLDER_PREFIX = "animus-asset:";
2001
- const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2002
- const PREFIX_RE = escapeRegExp(ASSET_PLACEHOLDER_PREFIX);
2003
- const QUOTED_PLACEHOLDER_RE = new RegExp(`(['"])${PREFIX_RE}([^'"]*?)\\1`, "g");
2004
- const BARE_PLACEHOLDER_RE = new RegExp(`${PREFIX_RE}([^'")\\s]+)`, "g");
2005
- function findAssetSpecifiers(css) {
2229
+ const URL_OPEN_RE = /url\(\s*(['"]?)/gi;
2230
+ const IMAGE_SET_OPEN_RE = /(?:-webkit-)?image-set\(/gi;
2231
+ const BARE_SPECIFIER_RE = /^[^'")\s]+/;
2232
+ /** The index of the `)` that closes the `(` at `open`, outside strings. */
2233
+ function closingParen$1(css, open) {
2234
+ let depth = 0;
2235
+ let quote = null;
2236
+ for (let i = open; i < css.length; i += 1) {
2237
+ const c = css[i];
2238
+ if (quote !== null) {
2239
+ if (c === quote) quote = null;
2240
+ } else if (c === "\"" || c === "'") quote = c;
2241
+ else if (c === "(") depth += 1;
2242
+ else if (c === ")") {
2243
+ depth -= 1;
2244
+ if (depth === 0) return i;
2245
+ }
2246
+ }
2247
+ return css.length;
2248
+ }
2249
+ function quotedReference(css, quoteAt) {
2250
+ const start = quoteAt + 1;
2251
+ if (!css.startsWith("animus-asset:", start)) return null;
2252
+ const end = css.indexOf(css[quoteAt], start);
2253
+ if (end === -1) return null;
2254
+ return {
2255
+ start,
2256
+ end,
2257
+ specifier: css.slice(start + 13, end)
2258
+ };
2259
+ }
2260
+ function assetReferences(css) {
2006
2261
  if (!css.includes("animus-asset:")) return [];
2007
- const seen = /* @__PURE__ */ new Set();
2008
- const remainder = css.replace(QUOTED_PLACEHOLDER_RE, (_match, _quote, specifier) => {
2009
- seen.add(specifier);
2010
- return "";
2011
- });
2012
- for (const match of remainder.matchAll(BARE_PLACEHOLDER_RE)) seen.add(match[1]);
2013
- return [...seen];
2262
+ const references = [];
2263
+ for (const open of css.matchAll(URL_OPEN_RE)) {
2264
+ const at = (open.index ?? 0) + open[0].length;
2265
+ if (open[1]) {
2266
+ const reference = quotedReference(css, at - 1);
2267
+ if (reference) references.push(reference);
2268
+ continue;
2269
+ }
2270
+ if (!css.startsWith("animus-asset:", at)) continue;
2271
+ const specifier = BARE_SPECIFIER_RE.exec(css.slice(at + 13))?.[0];
2272
+ if (specifier === void 0) continue;
2273
+ const end = at + 13 + specifier.length;
2274
+ references.push({
2275
+ start: at,
2276
+ end,
2277
+ specifier
2278
+ });
2279
+ }
2280
+ for (const open of css.matchAll(IMAGE_SET_OPEN_RE)) {
2281
+ const paren = (open.index ?? 0) + open[0].length - 1;
2282
+ const close = closingParen$1(css, paren);
2283
+ let depth = 0;
2284
+ let candidateStart = true;
2285
+ for (let i = paren + 1; i < close; i += 1) {
2286
+ const c = css[i];
2287
+ if (/\s/.test(c)) continue;
2288
+ if (c === "\"" || c === "'") {
2289
+ if (depth === 0 && candidateStart) {
2290
+ const reference = quotedReference(css, i);
2291
+ if (reference) references.push(reference);
2292
+ }
2293
+ const end = css.indexOf(c, i + 1);
2294
+ i = end === -1 ? close : end;
2295
+ } else if (c === "(") depth += 1;
2296
+ else if (c === ")") depth -= 1;
2297
+ else if (c === "," && depth === 0) {
2298
+ candidateStart = true;
2299
+ continue;
2300
+ }
2301
+ candidateStart = false;
2302
+ }
2303
+ }
2304
+ return references.sort((a, b) => a.start - b.start);
2305
+ }
2306
+ function findAssetSpecifiers(css) {
2307
+ return [...new Set(assetReferences(css).map((reference) => reference.specifier))];
2014
2308
  }
2015
2309
  function substituteAssetPlaceholders(css, urlBySpecifier) {
2016
2310
  if (urlBySpecifier.size === 0) return css;
2017
- if (!css.includes("animus-asset:")) return css;
2018
- const specifiers = [...urlBySpecifier.keys()].sort((a, b) => b.length - a.length);
2019
2311
  let out = css;
2020
- for (const specifier of specifiers) {
2021
- const placeholder = new RegExp(escapeRegExp(ASSET_PLACEHOLDER_PREFIX + specifier) + String.raw`(?=['")\s]|$)`, "g");
2022
- out = out.replace(placeholder, () => urlBySpecifier.get(specifier));
2312
+ for (const reference of assetReferences(css).reverse()) {
2313
+ const url = urlBySpecifier.get(reference.specifier);
2314
+ if (url === void 0) continue;
2315
+ out = out.slice(0, reference.start) + url + out.slice(reference.end);
2023
2316
  }
2024
2317
  return out;
2025
2318
  }
2319
+ /**
2320
+ * Specifiers of placeholder text anywhere inside a `url()` or `image-set()`
2321
+ * call, a reference or not: after substitution any of it is a broken load.
2322
+ */
2323
+ function placeholdersInAssetCalls(css) {
2324
+ if (!css.includes("animus-asset:")) return [];
2325
+ const calls = [...css.matchAll(URL_OPEN_RE), ...css.matchAll(IMAGE_SET_OPEN_RE)].map((open) => {
2326
+ const paren = css.indexOf("(", open.index ?? 0);
2327
+ return [paren, closingParen$1(css, paren)];
2328
+ });
2329
+ const found = /* @__PURE__ */ new Set();
2330
+ for (let at = css.indexOf(ASSET_PLACEHOLDER_PREFIX); at !== -1; at = css.indexOf(ASSET_PLACEHOLDER_PREFIX, at + 1)) {
2331
+ if (!calls.some(([open, close]) => open < at && at < close)) continue;
2332
+ found.add(/^[^'")\s,]*/.exec(css.slice(at + 13))[0]);
2333
+ }
2334
+ return [...found];
2335
+ }
2336
+ function findSheetAssetSpecifiers(sheets) {
2337
+ return [.../* @__PURE__ */ new Set([
2338
+ ...findAssetSpecifiers(sheets.variableCss),
2339
+ ...findAssetSpecifiers(sheets.globalCss),
2340
+ ...findAssetSpecifiers(sheets.componentCss)
2341
+ ])];
2342
+ }
2343
+ /** The one substitution step every emitted sheet passes through. */
2344
+ function substituteSheetAssets(sheets, urlBySpecifier) {
2345
+ return {
2346
+ variableCss: substituteAssetPlaceholders(sheets.variableCss, urlBySpecifier),
2347
+ globalCss: substituteAssetPlaceholders(sheets.globalCss, urlBySpecifier),
2348
+ componentCss: substituteAssetPlaceholders(sheets.componentCss, urlBySpecifier)
2349
+ };
2350
+ }
2351
+ const UNSUBSTITUTED_ASSET_CODE = "animus.asset.unsubstituted-placeholder";
2352
+ /** A placeholder in emitted CSS is a broken URL in the browser: it fails a
2353
+ * strict build and warns otherwise. */
2354
+ function reportSurvivingAssetPlaceholders(css, report) {
2355
+ const specifiers = placeholdersInAssetCalls(css);
2356
+ if (specifiers.length === 0) return;
2357
+ const message = `${report.prefix} asset() placeholders reached ${report.surface ?? "emitted CSS"} unsubstituted: ${specifiers.join(", ")} (${UNSUBSTITUTED_ASSET_CODE})`;
2358
+ if (report.strict) throw new Error(message);
2359
+ report.warn(message);
2360
+ }
2361
+ /**
2362
+ * The runtime code an analysis generates: each dynamic prop config, which the
2363
+ * shared prop map module carries, and each component's replacement. Extraction
2364
+ * lifts asset() out of them into root variables, so none should hold one.
2365
+ * Both embed values as JSON strings, so their quote and backslash escapes are
2366
+ * undone for scanning: the text is for `findAssetSpecifiers`, not to run.
2367
+ */
2368
+ function generatedModuleCode(manifest) {
2369
+ return [JSON.stringify(manifest.dynamic_props), ...Object.values(manifest.components).map((component) => component.replacement)].join("\n").replace(/\\(["\\])/g, "$1");
2370
+ }
2026
2371
  //#endregion
2027
2372
  //#region pipeline/resolve-asset.ts
2028
2373
  /**
@@ -2246,160 +2591,505 @@ function postProcessCss(css, opts) {
2246
2591
  }
2247
2592
  }
2248
2593
  //#endregion
2249
- //#region pipeline/manifest-diagnostics.ts
2250
- /** Stable code for selector forms with no substitutable subject; the
2251
- * extraction engine mints the same string. */
2252
- const SELECTOR_UNSUPPORTED_SUBJECT = "animus.selector.unsupported-subject";
2253
- /** The one matcher for the engine's unresolved-parent chain-drop message;
2254
- * consumers match through the exported helpers, never their own copy. */
2255
- const UNRESOLVED_PARENT_RE = /chain dropped: could not resolve parent component '([^']+)'/;
2256
- function isUnresolvedParentDrop(diagnostic) {
2257
- return UNRESOLVED_PARENT_RE.test(diagnostic.message);
2258
- }
2259
- function unresolvedParentName(diagnostic) {
2260
- return UNRESOLVED_PARENT_RE.exec(diagnostic.message)?.[1] ?? null;
2594
+ //#region pipeline/property-names.ts
2595
+ function createPropertyNames(declared, prefix, themeNames = []) {
2596
+ const finals = /* @__PURE__ */ new Map();
2597
+ for (const name of [...new Set(declared)].sort()) finals.set(name, `${prefix}-${name}`);
2598
+ const aliases = /* @__PURE__ */ new Map();
2599
+ for (const [name, final] of finals) if (!finals.has(final)) aliases.set(final, name);
2600
+ const others = /* @__PURE__ */ new Map();
2601
+ for (const name of [...new Set(themeNames)].sort()) if (!finals.has(name)) others.set(name, `${prefix}-${name}`);
2602
+ return {
2603
+ identity: (spelling) => finals.has(spelling) ? spelling : aliases.get(spelling) ?? (others.has(spelling) ? spelling : void 0),
2604
+ finalName: (name) => finals.get(name) ?? others.get(name),
2605
+ ambiguous: () => [...others.keys()].filter((name) => aliases.has(name)).map((name) => ({
2606
+ name,
2607
+ contextual: aliases.get(name) ?? name
2608
+ }))
2609
+ };
2261
2610
  }
2262
- /** True when the selector carries at least one substitutable `&` subject
2263
- * outside quoted text. */
2264
- function hasSelectorSubject(value) {
2265
- let quote = null;
2266
- let escaped = false;
2267
- for (const c of value) {
2268
- if (escaped) {
2269
- escaped = false;
2270
- continue;
2271
- }
2272
- if (c === "\\") {
2273
- escaped = true;
2611
+ const NAME_CHAR = /[\w-]/;
2612
+ /**
2613
+ * The name starting at `from`, read as CSS tokenizes an identifier: ASCII
2614
+ * letters, digits, `-` and `_`, every non-ASCII code point, and escapes.
2615
+ */
2616
+ function readCustomPropertyName(css, from) {
2617
+ let end = from;
2618
+ let name = "";
2619
+ while (end < css.length) {
2620
+ if (isNameChar(css[end])) {
2621
+ name += css[end];
2622
+ end += 1;
2274
2623
  continue;
2275
2624
  }
2276
- if (quote !== null) {
2277
- if (c === quote) quote = null;
2278
- } else if (c === "\"" || c === "'") quote = c;
2279
- else if (c === "&") return true;
2625
+ const escape = escapeAt(css, end);
2626
+ if (escape === null) break;
2627
+ name += escape.codePoint;
2628
+ end += escape.length;
2280
2629
  }
2281
- return false;
2630
+ return {
2631
+ end,
2632
+ name
2633
+ };
2634
+ }
2635
+ /** A valid escape at `at`: up to six hex digits and one following
2636
+ * whitespace, or any other code point but a newline. */
2637
+ function escapeAt(css, at) {
2638
+ if (css[at] !== "\\") return null;
2639
+ const next = css.codePointAt(at + 1);
2640
+ if (next === void 0 || next === 10 || next === 13 || next === 12) return null;
2641
+ const digits = /^[0-9a-fA-F]{1,6}/.exec(css.slice(at + 1, at + 7))?.[0];
2642
+ if (digits === void 0) return {
2643
+ codePoint: String.fromCodePoint(next),
2644
+ length: 1 + String.fromCodePoint(next).length
2645
+ };
2646
+ const after = css.slice(at + 1 + digits.length);
2647
+ const space = after.startsWith("\r\n") ? 2 : /^[ \t\n\r\f]/.test(after) ? 1 : 0;
2648
+ const value = parseInt(digits, 16);
2649
+ const valid = value !== 0 && value <= 1114111 && (value < 55296 || value > 57343);
2650
+ return {
2651
+ codePoint: String.fromCodePoint(valid ? value : 65533),
2652
+ length: 1 + digits.length + space
2653
+ };
2282
2654
  }
2283
2655
  /**
2284
- * Coded diagnostics for selector-shaped alias values with no substitutable
2285
- * subject; alias values never reach the evaluator's own key guard.
2656
+ * Gives every custom-property token that names a managed property its final
2657
+ * name, in one pass: declaration keys, `var()` reads at any depth (fallbacks
2658
+ * included), `@property` names, transition-property lists and style queries.
2659
+ * A name is read whole, escapes decoded, so an undeclared name that starts
2660
+ * with a managed one is never renamed in part. Quoted strings, `url()` and
2661
+ * comments are copied unchanged.
2286
2662
  */
2287
- function collectSelectorAliasDiagnostics(selectorAliasesJson) {
2288
- if (!selectorAliasesJson) return [];
2289
- const aliases = parseInternalWire(selectorAliasesJson, "selectorAliasesJson (the system loader's selector-alias registry)");
2290
- const diagnostics = [];
2291
- for (const [name, value] of Object.entries(aliases)) if (value.includes("&") && !hasSelectorSubject(value)) diagnostics.push({
2292
- file: "system",
2293
- component: name,
2294
- kind: "warn",
2295
- message: `selector alias '${name}' value '${value}' has no substitutable '&' subject outside quoted text (${SELECTOR_UNSUPPORTED_SUBJECT})`,
2296
- code: SELECTOR_UNSUPPORTED_SUBJECT,
2297
- severity: severityFor(SELECTOR_UNSUPPORTED_SUBJECT)
2298
- });
2299
- return diagnostics;
2663
+ function renameCustomProperties(css, names) {
2664
+ let out = "";
2665
+ let i = 0;
2666
+ while (i < css.length) {
2667
+ const c = css[i];
2668
+ if (c === "\"" || c === "'") {
2669
+ const end = closingQuote$1(css, i);
2670
+ out += css.slice(i, end);
2671
+ i = end;
2672
+ } else if (css.startsWith("/*", i)) {
2673
+ const close = css.indexOf("*/", i + 2);
2674
+ const end = close === -1 ? css.length : close + 2;
2675
+ out += css.slice(i, end);
2676
+ i = end;
2677
+ } else if (/^url\(/i.test(css.slice(i, i + 4)) && !isNameChar(css[i - 1])) {
2678
+ const end = closingUrl(css, i + 4);
2679
+ out += css.slice(i, end);
2680
+ i = end;
2681
+ } else if (css.startsWith("--", i) && !isNameChar(css[i - 1])) {
2682
+ const { end, name } = readCustomPropertyName(css, i + 2);
2683
+ const identity = names.identity(name);
2684
+ const final = identity === void 0 ? void 0 : names.finalName(identity);
2685
+ out += final === void 0 ? css.slice(i, end) : `--${final}`;
2686
+ i = end;
2687
+ } else {
2688
+ out += c;
2689
+ i += 1;
2690
+ }
2691
+ }
2692
+ return out;
2300
2693
  }
2301
- const UNREADABLE_SOURCE_FILE = "animus.ingestion.unreadable-source-file";
2694
+ /** A name code point, or a UTF-16 unit of a non-ASCII one. */
2695
+ function isNameChar(c) {
2696
+ return c !== void 0 && (NAME_CHAR.test(c) || c.charCodeAt(0) >= 128);
2697
+ }
2698
+ function closingQuote$1(css, open) {
2699
+ const quote = css[open];
2700
+ let i = open + 1;
2701
+ while (i < css.length && css[i] !== quote) i += css[i] === "\\" ? 2 : 1;
2702
+ return Math.min(i + 1, css.length);
2703
+ }
2704
+ /** The index after the `)` that closes an unquoted or quoted `url(`. */
2705
+ function closingUrl(css, from) {
2706
+ let i = from;
2707
+ while (i < css.length && css[i] !== ")") i = css[i] === "\"" || css[i] === "'" ? closingQuote$1(css, i) : i + 1;
2708
+ return Math.min(i + 1, css.length);
2709
+ }
2710
+ //#endregion
2711
+ //#region pipeline/property-registrations.ts
2712
+ const PROPERTY_RULE = /@property\s+(--[\w-]+)\s*\{[^}]*\}\n?/g;
2713
+ /** The data types a registration's `syntax` may name. */
2714
+ const SYNTAX_TYPES = /* @__PURE__ */ new Set([
2715
+ "angle",
2716
+ "color",
2717
+ "custom-ident",
2718
+ "image",
2719
+ "integer",
2720
+ "length",
2721
+ "length-percentage",
2722
+ "number",
2723
+ "percentage",
2724
+ "resolution",
2725
+ "string",
2726
+ "time",
2727
+ "transform-function",
2728
+ "transform-list",
2729
+ "url"
2730
+ ]);
2731
+ const SYNTAX_COMPONENT = /^(?:<([a-z-]+)>[+#]?|-?[a-zA-Z_][\w-]*)$/;
2732
+ const INITIAL_VALUE = /(initial-value\s*:\s*)([^;}]*)/;
2733
+ const MATH_FUNCTION = /\b(?:calc|min|max|clamp)\(/i;
2302
2734
  /**
2303
- * A configured source file that could not be read is a LOST INPUT: the
2304
- * emitted artifacts silently lack whatever that file declared.
2735
+ * Removes the `@property` rules browsers would ignore — an unknown syntax
2736
+ * component, a syntax other than `*` without an initial value, an initial
2737
+ * value that substitutes another value or depends on context, or one the
2738
+ * syntax does not accept. Minifying a stylesheet that contains such a rule
2739
+ * can also fail. An all-absolute math initial value is written as its
2740
+ * computed value, which the minifier reads.
2305
2741
  */
2306
- function unreadableSourceDiagnostic(relPath, error) {
2742
+ function splitInvalidPropertyRegistrations(css) {
2743
+ const invalid = [];
2307
2744
  return {
2308
- file: relPath,
2309
- component: "source",
2310
- kind: "warn",
2311
- message: `configured source file ${relPath} could not be read and was skipped: ${String(error)}`,
2312
- code: UNREADABLE_SOURCE_FILE,
2313
- severity: severityFor(UNREADABLE_SOURCE_FILE)
2745
+ css: css.replace(PROPERTY_RULE, (rule, name) => {
2746
+ const checked = checkRegistration(rule);
2747
+ if ("rule" in checked) return checked.rule;
2748
+ invalid.push({
2749
+ name,
2750
+ reason: checked.reason
2751
+ });
2752
+ return "";
2753
+ }),
2754
+ invalid
2314
2755
  };
2315
2756
  }
2316
- /** The collision entry code minted by the system package's merge. */
2317
- const VOCABULARY_COLLISION = "animus.vocabulary.collision";
2318
- /** A theme `@property` registration browsers would ignore; it is not emitted. */
2319
- const INVALID_PROPERTY_REGISTRATION = "animus.theme.invalid-property-registration";
2320
- /** Minted when a sealed kit with registered vocabulary is consumed through
2321
- * the deprecated `from()`/`includes:` verbs. */
2322
- const VOCABULARY_LEGACY_VERB = "animus.vocabulary.legacy-verb";
2323
- const DIAGNOSTIC_SEVERITY = /* @__PURE__ */ new Map([
2324
- [SELECTOR_UNSUPPORTED_SUBJECT, "error"],
2325
- [UNREADABLE_SOURCE_FILE, "error"],
2326
- [VOCABULARY_COLLISION, "warn"],
2327
- [VOCABULARY_LEGACY_VERB, "warn"],
2328
- [INVALID_PROPERTY_REGISTRATION, "error"]
2329
- ]);
2330
- /** An unlisted code is `warn`: a witness kind from a newer system package
2331
- * must not fail the strict build of a host that predates its writer. */
2332
- function severityFor(code) {
2333
- return (code === void 0 ? void 0 : DIAGNOSTIC_SEVERITY.get(code)) ?? "warn";
2757
+ /** The registrations in `css`, by custom-property name. */
2758
+ function propertyRegistrations(css) {
2759
+ const registrations = /* @__PURE__ */ new Map();
2760
+ for (const match of css.matchAll(PROPERTY_RULE)) registrations.set(match[1], readRegistration(match[0]));
2761
+ return registrations;
2334
2762
  }
2335
- /**
2336
- * The one mapper of a sealed system's witness entries for every host: the
2337
- * loader's evaluation host shims `console`, so the record is the channel.
2338
- */
2339
- function vocabularyWitnessDiagnostics(vocabularyWitnessesJson) {
2340
- if (!vocabularyWitnessesJson) return [];
2341
- const entries = parseInternalWire(vocabularyWitnessesJson, "vocabularyWitnessesJson (the sealed system's vocabulary witness record)");
2342
- const diagnostics = [];
2343
- for (const entry of entries) if (entry.code === "animus.vocabulary.collision") diagnostics.push({
2344
- file: "system",
2345
- component: entry.name ?? "keyframes",
2346
- kind: "warn",
2347
- message: `keyframes vocabulary "${entry.name}" is registered by both ${entry.loser} and ${entry.winner} — ${entry.winner} wins; rename one collection (${entry.code})`,
2348
- code: entry.code,
2349
- severity: severityFor(entry.code)
2350
- });
2351
- else if (entry.code === "animus.vocabulary.legacy-verb") diagnostics.push({
2352
- file: "system",
2353
- component: "keyframes",
2354
- kind: "warn",
2355
- message: `a sealed system (${entry.source ?? `'${entry.verb}' source`}) with registered vocabulary [${(entry.names ?? []).join(", ")}] was consumed through the deprecated '${entry.verb}' verb, which cannot carry it — those collections do NOT reach this consumer; use createSystem().extend(source) (${entry.code})`,
2356
- code: entry.code,
2357
- severity: severityFor(entry.code)
2358
- });
2359
- else diagnostics.push({
2360
- file: "system",
2361
- component: "vocabulary",
2362
- kind: "warn",
2363
- message: `unrecognized vocabulary witness entry ${JSON.stringify(entry)} — a newer @animus-ui/system may have recorded a witness kind this host predates${entry.code ? ` (${entry.code})` : ""}`,
2364
- code: entry.code,
2365
- severity: severityFor(entry.code)
2366
- });
2367
- return diagnostics;
2763
+ function readRegistration(rule) {
2764
+ return {
2765
+ syntax: /syntax:\s*"([^"]*)"/.exec(rule)?.[1]?.trim() ?? "",
2766
+ initialValue: /initial-value\s*:\s*([^;}]*)/.exec(rule)?.[1]?.trim()
2767
+ };
2368
2768
  }
2369
- /** The diagnostics a loaded system's own records carry: vocabulary
2370
- * witnesses and invalid `@property` registrations. */
2371
- function systemLoadDiagnostics(system) {
2372
- const registrations = (system.invalidPropertyRegistrations ?? []).map(({ name, reason }) => ({
2373
- file: "system",
2374
- component: name,
2375
- kind: "warn",
2376
- message: `@property ${name} was not emitted: ${reason}. Browsers ignore an invalid registration and minifying it fails; fix it where the theme declares ${name} (${INVALID_PROPERTY_REGISTRATION})`,
2377
- code: INVALID_PROPERTY_REGISTRATION,
2378
- severity: severityFor(INVALID_PROPERTY_REGISTRATION)
2379
- }));
2380
- return [...vocabularyWitnessDiagnostics(system.vocabularyWitnessesJson), ...registrations];
2769
+ /** The rule to keep, with an all-absolute math initial value folded, or why
2770
+ * a browser would ignore it. */
2771
+ function checkRegistration(rule) {
2772
+ const { syntax, initialValue } = readRegistration(rule);
2773
+ if (initialValue !== void 0 && substitutesValue(initialValue)) return { reason: `initialValue "${initialValue}" substitutes another value; var(), env() and attr() are not allowed in an initial value` };
2774
+ let kept = rule;
2775
+ if (syntax !== "*") {
2776
+ const unknown = syntax.split("|").map((component) => ({
2777
+ text: component.trim(),
2778
+ match: SYNTAX_COMPONENT.exec(component.trim())
2779
+ })).find(({ match }) => match === null || match[1] !== void 0 && !SYNTAX_TYPES.has(match[1]));
2780
+ if (unknown !== void 0) return { reason: `syntax "${syntax}" has the unknown component "${unknown.text}"` };
2781
+ if (initialValue === void 0) return { reason: `syntax "${syntax}" needs an initialValue` };
2782
+ if (dependsOnContext(initialValue)) return { reason: `initialValue "${initialValue}" depends on context; font- and container-relative units are not allowed` };
2783
+ const folded = foldInitialValue(syntax, initialValue);
2784
+ if (folded !== void 0 && !folded.accepted) return { reason: `initialValue "${initialValue}" computes to the ${folded.type} ${folded.text}, which syntax "${syntax}" does not accept` };
2785
+ if (folded !== void 0) kept = rule.replace(INITIAL_VALUE, `$1${folded.text}`);
2786
+ }
2787
+ try {
2788
+ transform({
2789
+ filename: "animus-registrations.css",
2790
+ code: Buffer.from(kept)
2791
+ });
2792
+ return { rule: kept };
2793
+ } catch (error) {
2794
+ const detail = error instanceof Error ? error.message : String(error);
2795
+ if (initialValue !== void 0 && MATH_FUNCTION.test(initialValue)) return { reason: `the CSS parser that builds the stylesheet cannot read the math in initialValue "${initialValue}" (${detail}); write its computed value instead` };
2796
+ return { reason: `the CSS parser rejects \`${kept.trim()}\` (${detail})` };
2797
+ }
2381
2798
  }
2799
+ //#endregion
2800
+ //#region pipeline/property-diagnostics.ts
2382
2801
  /**
2383
- * The single strict-escalation policy point for both extraction plugins:
2384
- * error-severity diagnostics throw together under `strict`, else warn.
2802
+ * Reports custom properties that will not behave as their CSS suggests: a
2803
+ * property that refers to itself, a fallback a registered initial value
2804
+ * applies ahead of, and an animated property that cannot interpolate. Names
2805
+ * are read as CSS tokenizes them, so escapes, function case, comments and
2806
+ * quoted text match the prefix pass. The CSS itself is never changed.
2385
2807
  */
2386
- function surfaceManifestDiagnostics(manifest, warn, policy = {}) {
2387
- const errors = [];
2388
- const diagnostics = policy.prepend?.length ? [...policy.prepend, ...manifest.diagnostics ?? []] : manifest.diagnostics ?? [];
2389
- for (const diagnostic of diagnostics) {
2390
- let line = null;
2391
- if (diagnostic.kind === "bail") line = `⚠ ${diagnostic.component} not extracted: ${diagnostic.message}`;
2392
- else if (diagnostic.kind === "skip") line = `⚠ ${diagnostic.component}: skipped ${diagnostic.message}`;
2393
- else if (diagnostic.kind === "warn") line = `⚠ ${diagnostic.file}: ${diagnostic.component}: ${diagnostic.message}`;
2394
- if (line === null) continue;
2395
- if (diagnostic.code && !diagnostic.message.includes(diagnostic.code)) line += ` [${diagnostic.code}]`;
2396
- if (policy.strict && diagnostic.severity === "error") {
2397
- errors.push(`${diagnostic.code ?? "error"} — ${diagnostic.component}: ${diagnostic.message}`);
2808
+ function checkCustomProperties(input) {
2809
+ const registered = propertyRegistrations(input.system.variableCss);
2810
+ const declared = new Set(input.system.contextualProperties ?? []);
2811
+ const findings = [];
2812
+ const sheets = [{
2813
+ css: input.componentCss,
2814
+ owner: componentOwner(input.manifest)
2815
+ }, {
2816
+ css: input.globalCss,
2817
+ owner: globalOwner
2818
+ }];
2819
+ for (const { css, owner } of sheets) {
2820
+ if (!/var\(|transition|@keyframes/i.test(css)) continue;
2821
+ const parsed = declarations(css);
2822
+ const discreteRules = new Set(parsed.filter((d) => d.property === "transition-behavior" && /\ballow-discrete\b/.test(d.value)).map((d) => d.rule));
2823
+ for (const declaration of parsed) {
2824
+ const found = (code, property, message) => findings.push({
2825
+ code,
2826
+ property,
2827
+ message,
2828
+ owner: owner(declaration)
2829
+ });
2830
+ if (isSelfReference(declaration)) {
2831
+ found(PROPERTY_SELF_REFERENCE, declaration.property, `${declaration.property} resolves to var(${declaration.property}), a reference to itself, so it is invalid at computed-value time. Give it a value other than its own name`);
2832
+ continue;
2833
+ }
2834
+ if (readsItselfOnlyInFallback(declaration)) found(PROPERTY_FALLBACK_SELF_REFERENCE, declaration.property, `${declaration.property}: ${declaration.value} reads ${declaration.property} only inside another var()'s fallback, so the result depends on how the browser treats fallback references: one that evaluates a fallback only when it is used keeps the value, one that counts every reference makes ${declaration.property} cyclic and invalid at computed-value time`);
2835
+ for (const read of varReads(declaration.value)) {
2836
+ const registration = registered.get(read.name);
2837
+ if (read.fallback === void 0 || registration?.initialValue === void 0) continue;
2838
+ const chain = read.fallback.startsWith("var(");
2839
+ found(chain ? PROPERTY_FALLBACK_CHAIN_SUPPRESSED : PROPERTY_FALLBACK_SUPPRESSED, read.name, `var(${read.name}, ${read.fallback}): where its registration applies, ${read.name}'s initial-value ${registration.initialValue} is used ahead of the ${chain ? "fallback chain" : "fallback"} whenever ${read.name} is unset; the fallback still serves browsers without the registration`);
2840
+ }
2841
+ for (const animated of animatedProperties(declaration, discreteRules)) {
2842
+ if (animated.discrete || !declared.has(animated.name)) continue;
2843
+ const reason = interpolationGap(registered.get(animated.name));
2844
+ if (reason === null) continue;
2845
+ found(PROPERTY_UNREGISTERED_ANIMATION, animated.name, `${animated.name} is ${animated.how} but ${reason}, so it changes in one step instead of interpolating. Register it with a typed syntax to animate it`);
2846
+ }
2847
+ }
2848
+ }
2849
+ return toDiagnostics(findings);
2850
+ }
2851
+ /** Declarations in engine-emitted CSS: one `property: value` per `;`. */
2852
+ function declarations(css) {
2853
+ const found = [];
2854
+ const stack = [];
2855
+ let rules = 0;
2856
+ let segment = 0;
2857
+ let parens = 0;
2858
+ let quote = null;
2859
+ const take = (end) => {
2860
+ const text = css.slice(segment, end);
2861
+ const colon = text.indexOf(":");
2862
+ const current = stack[stack.length - 1];
2863
+ if (current === void 0 || colon <= 0) return;
2864
+ found.push({
2865
+ property: text.slice(0, colon).trim(),
2866
+ value: text.slice(colon + 1).replace(/;$/, "").trim(),
2867
+ start: segment + (text.length - text.trimStart().length),
2868
+ end,
2869
+ rule: current.rule,
2870
+ context: stack.map((entry) => entry.prelude)
2871
+ });
2872
+ };
2873
+ for (let i = 0; i < css.length; i += 1) {
2874
+ const c = css[i];
2875
+ if (quote !== null) {
2876
+ if (c === "\\") i += 1;
2877
+ else if (c === quote) quote = null;
2398
2878
  continue;
2399
2879
  }
2400
- warn(line);
2880
+ if (c === "\"" || c === "'") quote = c;
2881
+ else if (c === "(") parens += 1;
2882
+ else if (c === ")") parens -= 1;
2883
+ else if (parens === 0 && c === "{") {
2884
+ stack.push({
2885
+ prelude: css.slice(segment, i).trim(),
2886
+ rule: rules
2887
+ });
2888
+ rules += 1;
2889
+ segment = i + 1;
2890
+ } else if (parens === 0 && c === ";") {
2891
+ take(i + 1);
2892
+ segment = i + 1;
2893
+ } else if (parens === 0 && c === "}") {
2894
+ take(i);
2895
+ stack.pop();
2896
+ segment = i + 1;
2897
+ }
2898
+ }
2899
+ return found;
2900
+ }
2901
+ /** `text` as a whole custom-property name, escapes decoded; `undefined`
2902
+ * when it is not one. */
2903
+ function customPropertyName(text) {
2904
+ if (!text.startsWith("--")) return void 0;
2905
+ const { end, name } = readCustomPropertyName(text, 2);
2906
+ return end === text.length ? `--${name}` : void 0;
2907
+ }
2908
+ function isSelfReference(declaration) {
2909
+ const property = customPropertyName(declaration.property);
2910
+ const reads = varReads(declaration.value);
2911
+ return property !== void 0 && reads.length === 1 && reads[0].name === property && reads[0].fallback === void 0 && withoutComments(declaration.value).trim() === withoutComments(declaration.value.slice(reads[0].start, reads[0].end)).trim();
2912
+ }
2913
+ /** Whether every read of the declared property sits inside another read's
2914
+ * fallback, as in `--a: var(--b, var(--a))`. */
2915
+ function readsItselfOnlyInFallback(declaration) {
2916
+ const property = customPropertyName(declaration.property);
2917
+ if (property === void 0) return false;
2918
+ const reads = varReads(declaration.value);
2919
+ const own = reads.filter((read) => read.name === property);
2920
+ return own.length > 0 && own.every((read) => reads.some((outer) => outer !== read && outer.start < read.start && read.end <= outer.end));
2921
+ }
2922
+ /** Every `var()` read in `value`, nested fallbacks included, in any case
2923
+ * and spacing. Quoted text and comments hold none. */
2924
+ function varReads(value) {
2925
+ const reads = [];
2926
+ for (let at = 0; at < value.length;) {
2927
+ const c = value[at];
2928
+ if (c === "\"" || c === "'") {
2929
+ at = closingQuote(value, at);
2930
+ continue;
2931
+ }
2932
+ if (value.startsWith("/*", at)) {
2933
+ at = skipSpace(value, at);
2934
+ continue;
2935
+ }
2936
+ if (value.slice(at, at + 4).toLowerCase() !== "var(" || /[\w-]/.test(value[at - 1] ?? "")) {
2937
+ at += 1;
2938
+ continue;
2939
+ }
2940
+ const nameAt = skipSpace(value, at + 4);
2941
+ if (!value.startsWith("--", nameAt)) {
2942
+ at += 4;
2943
+ continue;
2944
+ }
2945
+ const { end: nameEnd, name } = readCustomPropertyName(value, nameAt + 2);
2946
+ const after = skipSpace(value, nameEnd);
2947
+ const close = closingParen(value, at + 4);
2948
+ reads.push({
2949
+ name: `--${name}`,
2950
+ fallback: value[after] === "," ? value.slice(after + 1, close - 1).trim() : void 0,
2951
+ start: at,
2952
+ end: close
2953
+ });
2954
+ at = after;
2955
+ }
2956
+ return reads;
2957
+ }
2958
+ /** The index after the `)` that closes the call whose arguments start at
2959
+ * `from`. Quoted text and comments are skipped. */
2960
+ function closingParen(value, from) {
2961
+ let depth = 1;
2962
+ let at = from;
2963
+ while (at < value.length && depth > 0) {
2964
+ const c = value[at];
2965
+ if (c === "\"" || c === "'") at = closingQuote(value, at);
2966
+ else if (value.startsWith("/*", at)) at = skipSpace(value, at);
2967
+ else {
2968
+ if (c === "(") depth += 1;
2969
+ else if (c === ")") depth -= 1;
2970
+ at += 1;
2971
+ }
2972
+ }
2973
+ return at;
2974
+ }
2975
+ function closingQuote(value, open) {
2976
+ let at = open + 1;
2977
+ while (at < value.length && value[at] !== value[open]) at += value[at] === "\\" ? 2 : 1;
2978
+ return Math.min(at + 1, value.length);
2979
+ }
2980
+ /** The index after CSS whitespace and comments starting at `from`. */
2981
+ function skipSpace(value, from) {
2982
+ let at = from;
2983
+ for (;;) {
2984
+ while (/[ \t\n\r\f]/.test(value[at] ?? "")) at += 1;
2985
+ if (!value.startsWith("/*", at)) return at;
2986
+ const close = value.indexOf("*/", at + 2);
2987
+ at = close === -1 ? value.length : close + 2;
2988
+ }
2989
+ }
2990
+ function withoutComments(text) {
2991
+ return text.replace(/\/\*[\s\S]*?\*\//g, "");
2992
+ }
2993
+ /** Custom properties this declaration sets in a keyframe or names in a
2994
+ * transition. */
2995
+ function animatedProperties(declaration, discreteRules) {
2996
+ const keyframes = declaration.context.find((prelude) => prelude.startsWith("@keyframes"));
2997
+ const set = customPropertyName(declaration.property);
2998
+ if (keyframes !== void 0 && set !== void 0) return [{
2999
+ name: set,
3000
+ how: `set in ${keyframes}`,
3001
+ discrete: false
3002
+ }];
3003
+ if (declaration.property !== "transition" && declaration.property !== "transition-property") return [];
3004
+ return topLevelItems(declaration.value).flatMap((item) => {
3005
+ const property = topLevelTokens(item.replace(/\/\*[\s\S]*?\*\//g, " ")).map(customPropertyName).find((name) => name !== void 0);
3006
+ return property !== void 0 ? [{
3007
+ name: property,
3008
+ how: "transitioned",
3009
+ discrete: /\ballow-discrete\b/.test(item) || discreteRules.has(declaration.rule)
3010
+ }] : [];
3011
+ });
3012
+ }
3013
+ /** The whitespace-separated tokens of `item` outside any parentheses. */
3014
+ function topLevelTokens(item) {
3015
+ const tokens = [];
3016
+ let depth = 0;
3017
+ let token = "";
3018
+ for (const c of item) {
3019
+ if (c === "(") depth += 1;
3020
+ else if (c === ")") depth -= 1;
3021
+ if (depth === 0 && /\s/.test(c)) {
3022
+ if (token) tokens.push(token);
3023
+ token = "";
3024
+ } else token += c;
3025
+ }
3026
+ if (token) tokens.push(token);
3027
+ return tokens;
3028
+ }
3029
+ function topLevelItems(value) {
3030
+ const items = [];
3031
+ let depth = 0;
3032
+ let start = 0;
3033
+ for (let i = 0; i < value.length; i += 1) if (value[i] === "(") depth += 1;
3034
+ else if (value[i] === ")") depth -= 1;
3035
+ else if (value[i] === "," && depth === 0) {
3036
+ items.push(value.slice(start, i));
3037
+ start = i + 1;
3038
+ }
3039
+ items.push(value.slice(start));
3040
+ return items;
3041
+ }
3042
+ /** Why a property cannot interpolate, or `null` when it can. */
3043
+ function interpolationGap(registration) {
3044
+ if (registration === void 0) return "it is not registered";
3045
+ if (registration.syntax === "*") return "it is registered with the universal syntax \"*\"";
3046
+ return null;
3047
+ }
3048
+ function componentOwner(manifest) {
3049
+ const components = Object.values(manifest.components ?? {});
3050
+ return (declaration) => {
3051
+ const selector = [...declaration.context].reverse().find((prelude) => !prelude.startsWith("@"));
3052
+ const classes = (selector ?? "").match(/\.-?[_a-zA-Z][\w-]*/g) ?? [];
3053
+ for (const dotted of classes) {
3054
+ const name = dotted.slice(1);
3055
+ const owner = components.find((component) => name === component.class_name || name.startsWith(`${component.class_name}--`) || component.replacement.includes(`"${name}"`));
3056
+ if (owner) return {
3057
+ file: owner.file,
3058
+ component: owner.binding
3059
+ };
3060
+ }
3061
+ return {
3062
+ file: "styles",
3063
+ component: selector ?? "stylesheet"
3064
+ };
3065
+ };
3066
+ }
3067
+ function globalOwner(declaration) {
3068
+ const keyframes = declaration.context.find((prelude) => prelude.startsWith("@keyframes"));
3069
+ const selector = [...declaration.context].reverse().find((prelude) => !prelude.startsWith("@"));
3070
+ return {
3071
+ file: "system",
3072
+ component: keyframes ?? selector ?? "global styles"
3073
+ };
3074
+ }
3075
+ /** One diagnostic per code, property and owner. */
3076
+ function toDiagnostics(findings) {
3077
+ const seen = /* @__PURE__ */ new Set();
3078
+ const diagnostics = [];
3079
+ for (const finding of findings) {
3080
+ const key = `${finding.code}|${finding.property}|${finding.owner.file}|${finding.owner.component}`;
3081
+ if (seen.has(key)) continue;
3082
+ seen.add(key);
3083
+ diagnostics.push({
3084
+ file: finding.owner.file,
3085
+ component: finding.owner.component,
3086
+ kind: "warn",
3087
+ message: `${finding.message} (${finding.code})`,
3088
+ code: finding.code,
3089
+ severity: severityFor(finding.code)
3090
+ });
2401
3091
  }
2402
- if (errors.length > 0) throw new Error(`[animus] strict: ${errors.length} error diagnostic(s):\n${errors.join("\n")}`);
3092
+ return diagnostics;
2403
3093
  }
2404
3094
  //#endregion
2405
3095
  //#region pipeline/unit-fallback.ts
@@ -2478,12 +3168,33 @@ function buildAnalysisInputs(opts) {
2478
3168
  externalDirsJson: opts.externalDirs?.length && hasSourceThemeManifests(opts.system) ? JSON.stringify(opts.externalDirs) : null
2479
3169
  };
2480
3170
  if (opts.system.declarationScalesJson) inputs.declarationScalesJson = opts.system.declarationScalesJson;
3171
+ const context = opts.analysisContext;
3172
+ if (context?.skippedSources?.length || context?.unbundledComputedImports || context?.packageDirs?.length) inputs.analysisContextJson = JSON.stringify({
3173
+ skippedSources: context.skippedSources ?? [],
3174
+ unbundledComputedImports: context.unbundledComputedImports ?? false,
3175
+ packageDirs: context.packageDirs ?? []
3176
+ });
2481
3177
  return inputs;
2482
3178
  }
2483
3179
  function hasSourceThemeManifests(system) {
2484
3180
  const json = system.sourceThemeManifestsJson ?? "";
2485
3181
  return json.length > 0 && json !== "{}";
2486
3182
  }
3183
+ /** Systems whose load diagnostics were surfaced: each loaded system reports
3184
+ * them once, not again on every analysis and hot update. The set is per
3185
+ * process. Next's webpack compilers share one analysis in one process, but
3186
+ * its build worker and Turbopack builds analyze in two processes, which
3187
+ * repeat every diagnostic alike, component ones included. */
3188
+ const surfacedSystems = /* @__PURE__ */ new WeakSet();
3189
+ /** The loaded system's own diagnostics, the first time it is analyzed. A
3190
+ * strict error keeps failing every analysis until the system changes. */
3191
+ function systemDiagnostics(system, strict) {
3192
+ const diagnostics = [...collectSelectorAliasDiagnostics(system.selectorAliasesJson), ...systemLoadDiagnostics(system)];
3193
+ const blocking = strict === true && diagnostics.some((d) => d.severity === "error");
3194
+ if (surfacedSystems.has(system) && !blocking) return [];
3195
+ surfacedSystems.add(system);
3196
+ return diagnostics;
3197
+ }
2487
3198
  /**
2488
3199
  * The one analysis invocation both plugins share. Error handling stays at
2489
3200
  * the call site (strict-mode throw vs warn).
@@ -2498,11 +3209,21 @@ function runProjectAnalysis(engineApi, opts) {
2498
3209
  const extractMs = Math.round(performance.now() - t);
2499
3210
  t = performance.now();
2500
3211
  const manifest = JSON.parse(manifestJson);
3212
+ const systemErrors = systemLoadDiagnostics(opts.system).filter((diagnostic) => diagnostic.kind === "error");
3213
+ if (systemErrors.length > 0) manifest.diagnostics = [...systemErrors, ...manifest.diagnostics ?? []];
3214
+ const componentCss = applyUnitFallback(manifest.css);
3215
+ const propertyDiagnostics = checkCustomProperties({
3216
+ system: opts.system,
3217
+ manifest,
3218
+ componentCss,
3219
+ globalCss: manifest.sheets.global
3220
+ });
2501
3221
  surfaceManifestDiagnostics(manifest, opts.warn, {
2502
3222
  strict: opts.strict,
3223
+ info: opts.info,
2503
3224
  prepend: [
2504
- ...collectSelectorAliasDiagnostics(opts.system.selectorAliasesJson),
2505
- ...systemLoadDiagnostics(opts.system),
3225
+ ...systemDiagnostics(opts.system, opts.strict),
3226
+ ...propertyDiagnostics,
2506
3227
  ...opts.extraDiagnostics ?? []
2507
3228
  ]
2508
3229
  });
@@ -2511,7 +3232,7 @@ function runProjectAnalysis(engineApi, opts) {
2511
3232
  manifest,
2512
3233
  manifestJson,
2513
3234
  globalCss: manifest.sheets.global,
2514
- componentCss: applyUnitFallback(manifest.css),
3235
+ componentCss,
2515
3236
  inputs,
2516
3237
  timings: {
2517
3238
  serializeMs,
@@ -2596,87 +3317,60 @@ function applyPrefix(prefix, variableMapJson, variableCss, themeJson, contextual
2596
3317
  }
2597
3318
  return result;
2598
3319
  }
2599
- //#endregion
2600
- //#region pipeline/property-registrations.ts
2601
- const PROPERTY_RULE = /@property\s+(--[\w-]+)\s*\{[^}]*\}\n?/g;
2602
- /** The data types a registration's `syntax` may name. */
2603
- const SYNTAX_TYPES = /* @__PURE__ */ new Set([
2604
- "angle",
2605
- "color",
2606
- "custom-ident",
2607
- "image",
2608
- "integer",
2609
- "length",
2610
- "length-percentage",
2611
- "number",
2612
- "percentage",
2613
- "resolution",
2614
- "string",
2615
- "time",
2616
- "transform-function",
2617
- "transform-list",
2618
- "url"
2619
- ]);
2620
- const SYNTAX_COMPONENT = /^(?:<([a-z-]+)>[+#]?|-?[a-zA-Z_][\w-]*)$/;
2621
- /** The syntax types whose values can carry a font- or container-relative
2622
- * unit, which makes a typed initial value depend on context. */
2623
- const DIMENSION_TYPES = /* @__PURE__ */ new Set([
2624
- "image",
2625
- "length",
2626
- "length-percentage",
2627
- "transform-function",
2628
- "transform-list"
2629
- ]);
2630
- const RELATIVE_UNIT = /(?:\d|\.)(?:r?em|r?ex|r?cap|r?ch|r?ic|r?lh|cq(?:w|h|i|b|min|max))\b/i;
2631
- const SUBSTITUTION = /\b(?:var|env|attr)\(/i;
2632
- /** Quoted strings and `url()` hold text, never units or substitutions. */
2633
- const LITERAL_TEXT = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|url\([^)]*\)/gi;
3320
+ /** The runtime's transport variables are `--animus-<prop>`. */
3321
+ const TRANSPORT_NAMESPACE = "animus-";
2634
3322
  /**
2635
- * Removes the `@property` rules browsers would ignore — an unknown syntax
2636
- * component, a syntax other than `*` without an initial value, an initial
2637
- * value that depends on context, or one the syntax does not accept.
2638
- * Minifying a stylesheet that contains such a rule can also fail.
3323
+ * The prefix applied through one map: every variable the theme defines and
3324
+ * every declared contextual variable takes `--${prefix}-${name}`, each
3325
+ * reference renamed once. Each contextual entry carries its declared and
3326
+ * final name, so lookups run on the declared identity and the extractor
3327
+ * emits the final name.
2639
3328
  */
2640
- function splitInvalidPropertyRegistrations(css) {
2641
- const invalid = [];
3329
+ function applyPropertyNames(prefix, artifacts) {
3330
+ const byScale = artifacts.contextualVarsJson ? parseInternalWire(artifacts.contextualVarsJson, "contextualVarsJson (the theme's contextual variable names)") : null;
3331
+ const declared = Object.values(byScale ?? {}).flat();
3332
+ const variableMap = parseInternalWire(artifacts.variableMapJson, "variableMapJson (the theme's token variables)");
3333
+ const managed = createPropertyNames(declared, prefix, [...Object.values(variableMap).map((name) => name.replace(/^--/, "")), ...[...artifacts.variableCss.matchAll(THEME_DEFINITION)].map((m) => m[1])]);
3334
+ const nameConflicts = [...[...new Set(declared)].flatMap((name) => {
3335
+ const final = managed.finalName(name) ?? name;
3336
+ return final.startsWith(TRANSPORT_NAMESPACE) ? [{
3337
+ name,
3338
+ final,
3339
+ reason: "transport"
3340
+ }] : [];
3341
+ }), ...managed.ambiguous().map(({ name, contextual }) => ({
3342
+ name,
3343
+ final: contextual,
3344
+ reason: "theme-variable"
3345
+ }))];
3346
+ const rename = (value) => renameCustomProperties(value, managed);
2642
3347
  return {
2643
- css: css.replace(PROPERTY_RULE, (rule, name) => {
2644
- const reason = registrationFailure(rule);
2645
- if (reason === null) return rule;
2646
- invalid.push({
2647
- name,
2648
- reason
2649
- });
2650
- return "";
2651
- }),
2652
- invalid
3348
+ variableMapJson: JSON.stringify(Object.fromEntries(Object.entries(variableMap).map(([key, name]) => [key, rename(name)]))),
3349
+ variableCss: rename(artifacts.variableCss),
3350
+ themeJson: renameTokenValues(artifacts.themeJson, rename),
3351
+ contextualVarsJson: byScale === null ? null : JSON.stringify(Object.fromEntries(Object.entries(byScale).map(([scale, names]) => [scale, names.map((name) => ({
3352
+ name,
3353
+ var: managed.finalName(name) ?? name
3354
+ }))]))),
3355
+ declarationScalesJson: artifacts.declarationScalesJson === null ? null : renameDeclarationRecords(artifacts.declarationScalesJson, rename),
3356
+ nameConflicts,
3357
+ contextualProperties: [...new Set(declared)].map((name) => `--${managed.finalName(name) ?? name}`)
2653
3358
  };
2654
3359
  }
2655
- function registrationFailure(rule) {
2656
- const syntax = /syntax:\s*"([^"]*)"/.exec(rule)?.[1]?.trim() ?? "";
2657
- const initialValue = /initial-value\s*:\s*([^;}]*)/.exec(rule)?.[1]?.trim();
2658
- if (syntax !== "*") {
2659
- const components = syntax.split("|").map((component) => ({
2660
- text: component.trim(),
2661
- match: SYNTAX_COMPONENT.exec(component.trim())
2662
- }));
2663
- const unknown = components.find(({ match }) => match === null || match[1] !== void 0 && !SYNTAX_TYPES.has(match[1]));
2664
- if (unknown !== void 0) return `syntax "${syntax}" has the unknown component "${unknown.text}"`;
2665
- if (initialValue === void 0) return `syntax "${syntax}" needs an initialValue`;
2666
- const dimensional = components.some(({ match }) => match?.[1] !== void 0 && DIMENSION_TYPES.has(match[1]));
2667
- const value = initialValue.replace(LITERAL_TEXT, "");
2668
- if (SUBSTITUTION.test(value) || dimensional && RELATIVE_UNIT.test(value)) return `initialValue "${initialValue}" depends on context; font- and container-relative units and var(), env() and attr() are not allowed`;
2669
- }
2670
- try {
2671
- transform({
2672
- filename: "animus-registrations.css",
2673
- code: Buffer.from(rule)
2674
- });
2675
- return null;
2676
- } catch (error) {
2677
- const detail = error instanceof Error ? error.message : String(error);
2678
- return `the CSS parser rejects \`${rule.trim()}\` (${detail})`;
2679
- }
3360
+ /** A custom property the theme's CSS defines or registers. */
3361
+ const THEME_DEFINITION = /(?:@property\s+|(?:^|[\s;{]))--([\w-]+)(?=\s*[:{])/g;
3362
+ /** Renames inside the token map's values. */
3363
+ function renameTokenValues(themeJson, rename) {
3364
+ const tokens = parseInternalWire(themeJson, "themeJson (the theme's flattened token values)");
3365
+ return JSON.stringify(Object.fromEntries(Object.entries(tokens).map(([path, value]) => [path, rename(value)])));
3366
+ }
3367
+ /** Renames inside every declaration-scale record value. */
3368
+ function renameDeclarationRecords(declarationScalesJson, rename) {
3369
+ const scales = parseInternalWire(declarationScalesJson, "declarationScalesJson (the theme's declaration scales)");
3370
+ return JSON.stringify(Object.fromEntries(Object.entries(scales).map(([name, scale]) => [name, {
3371
+ ...scale,
3372
+ values: Object.fromEntries(Object.entries(scale.values).map(([key, record]) => [key, Object.fromEntries(Object.entries(record).map(([property, value]) => [property, rename(value)]))]))
3373
+ }])));
2680
3374
  }
2681
3375
  //#endregion
2682
3376
  //#region pipeline/system-config.ts
@@ -2693,7 +3387,28 @@ function loadSystemConfig(engineApi, opts) {
2693
3387
  let variableCss = registrations.css;
2694
3388
  let contextualVarsJson = config.contextualVarsJson || null;
2695
3389
  let declarationScalesJson = config.declarationScalesJson || null;
2696
- if (opts.prefix) {
3390
+ const declaredNames = Object.values(parseInternalWire(contextualVarsJson ?? "{}", "contextualVarsJson (the theme's contextual variable names)")).flat();
3391
+ let contextualProperties = declaredNames.map((name) => `--${name}`);
3392
+ let legacyPrefixedContextualVars = [];
3393
+ let prefixNameConflicts = [];
3394
+ if (opts.prefix && opts.prefixContextualVars) {
3395
+ const resolved = applyPropertyNames(opts.prefix, {
3396
+ variableMapJson,
3397
+ variableCss,
3398
+ themeJson: scalesJson,
3399
+ contextualVarsJson,
3400
+ declarationScalesJson
3401
+ });
3402
+ variableMapJson = resolved.variableMapJson;
3403
+ variableCss = resolved.variableCss;
3404
+ scalesJson = resolved.themeJson;
3405
+ contextualVarsJson = resolved.contextualVarsJson;
3406
+ contextualProperties = resolved.contextualProperties;
3407
+ prefixNameConflicts = resolved.nameConflicts;
3408
+ declarationScalesJson = resolved.declarationScalesJson;
3409
+ } else if (opts.prefix) {
3410
+ legacyPrefixedContextualVars = declaredNames;
3411
+ contextualProperties = declaredNames.map((name) => `--${opts.prefix}-${name}`);
2697
3412
  const prefixed = applyPrefix(opts.prefix, variableMapJson, variableCss, scalesJson, contextualVarsJson || void 0);
2698
3413
  variableMapJson = prefixed.variableMapJson;
2699
3414
  variableCss = prefixed.variableCss;
@@ -2708,6 +3423,7 @@ function loadSystemConfig(engineApi, opts) {
2708
3423
  variableMapJson,
2709
3424
  variableCss,
2710
3425
  contextualVarsJson,
3426
+ contextualProperties: [...new Set(contextualProperties)],
2711
3427
  selectorAliasesJson: config.selectorAliases || null,
2712
3428
  conditionAliasesJson: config.conditionAliases || null,
2713
3429
  transformSourcesJson: config.transformSources || null,
@@ -2719,7 +3435,10 @@ function loadSystemConfig(engineApi, opts) {
2719
3435
  sourceThemeManifestsJson: config.sourceThemeManifests || null
2720
3436
  };
2721
3437
  if (declarationScalesJson) system.declarationScalesJson = declarationScalesJson;
3438
+ if (config.propertyRecordsJson) system.propertyRecordsJson = config.propertyRecordsJson;
2722
3439
  if (registrations.invalid.length > 0) system.invalidPropertyRegistrations = registrations.invalid;
3440
+ if (prefixNameConflicts.length > 0) system.prefixNameConflicts = prefixNameConflicts;
3441
+ if (legacyPrefixedContextualVars.length > 0) system.legacyPrefixedContextualVars = [...new Set(legacyPrefixedContextualVars)];
2723
3442
  return system;
2724
3443
  }
2725
3444
  //#endregion
@@ -2773,6 +3492,7 @@ function buildDynamicPropConfig(dynamicProps) {
2773
3492
  if (meta.negative) entry.negative = true;
2774
3493
  if (meta.strict) entry.strict = true;
2775
3494
  if (meta.keywords?.length) entry.keywords = meta.keywords;
3495
+ if (meta.currentVar) entry.currentVar = meta.currentVar;
2776
3496
  configEntries[propName] = entry;
2777
3497
  }
2778
3498
  return configEntries;
@@ -2803,31 +3523,6 @@ function buildSystemPropsModule(opts) {
2803
3523
  return `export const systemPropMap = ${opts.systemPropMapJson};\nexport const systemPropGroups = ${opts.groupRegistryJson};\nexport const dynamicPropConfig = ${JSON.stringify(dynamicPropConfig)};\nexport const transforms = ${transformsSource(opts.admittedTransforms)};\n` + (opts.typedSystemProps.length > 0 ? `export const typedSystemProps = ${JSON.stringify(opts.typedSystemProps)};\n` : "");
2804
3524
  }
2805
3525
  //#endregion
2806
- //#region pipeline/timing-waterfall.ts
2807
- const RUST_TIMING_PHASES = [
2808
- ["parseAndWalk", "parse+walk"],
2809
- ["importResolution", "imports"],
2810
- ["extensionProvenance", "provenance"],
2811
- ["topologicalSort", "topo-sort"],
2812
- ["chainEvaluation", "chains"],
2813
- ["jsxScanning", "jsx-scan"],
2814
- ["systemPropAggregation", "sys-props"],
2815
- ["usageLedger", "usage"],
2816
- ["reconciliation", "reconcile"],
2817
- ["cssGeneration", "css-gen"],
2818
- ["manifestSerialization", "serialize"]
2819
- ];
2820
- function formatRustTimingWaterfall(timing, opts) {
2821
- const lines = [];
2822
- for (const [key, label] of RUST_TIMING_PHASES) {
2823
- const ms = timing[key] ?? 0;
2824
- const pad = " ".repeat(Math.max(0, opts.labelWidth - label.length));
2825
- const extra = key === "parseAndWalk" ? ` (${timing.fileCount ?? 0} files, ${timing.cacheHits ?? 0} cached)` : "";
2826
- lines.push(`${opts.indent}${label}${pad}${String(ms).padStart(5)}ms${extra}`);
2827
- }
2828
- return lines;
2829
- }
2830
- //#endregion
2831
3526
  //#region pipeline/tsconfig-paths.ts
2832
3527
  /**
2833
3528
  * tsconfig `paths` → alias pairs, for drivers with no live bundler config.
@@ -3094,4 +3789,4 @@ function realpathThroughNearestExistingAncestor(path) {
3094
3789
  }
3095
3790
  }
3096
3791
  //#endregion
3097
- export { AnimusConfigError as $, resolveLightningTargets as A, extractSystemFilePackages as B, isUnresolvedParentDrop as C, ANIMUS_LAYERS as Ct, unresolvedParentName as D, __require as Dt, unreadableSourceDiagnostic as E, validateLayerOrder as Et, findAssetSpecifiers as F, staleDistIncludesMessage as G, firstOwners as H, substituteAssetPlaceholders as I, createSourceIdentity as J, unresolvableIncludesMessage as K, createSourceCorpus as L, enforceExternalTokenContracts as M, resolveAssetFile as N, vocabularyWitnessDiagnostics as O, resolveThroughPathAliases as P, discoverFiles as Q, collectExternalPackageSources as R, VOCABULARY_LEGACY_VERB as S, buildAnalyzeProjectArgs as St, systemLoadDiagnostics as T, stripLeadingLayerDeclaration as Tt, isExcludedPackageRelativePath as U, findPackageRoot as V, resolveAbsolutePathSpecifier as W, sharesVolumeRoot as X, isPathWithinRoot as Y, compareDiscoveryOrder as Z, clearEngineCache as _, DEFAULT_EXTENSIONS as _t, assertNoErrorDiagnostics as a, assertKnownOptionKeys as at, INVALID_PROPERTY_REGISTRATION as b, preprocessMdx as bt, buildSystemPropsModule as c, resolveVerbosity as ct, loadSystemConfig as d, createV2EngineApi as dt, CORE_OPTION_KEYS as et, applyPrefix as f, ingestSourceEntries as ft, buildAnalysisInputs as g, adaptSvelteSource as gt, stableStringify as h, projectExternalFileOwners as ht, snapshotFilePlans as i, STRUCTURAL_EXCLUDE as it, buildPathAliasesJson as j, postProcessCss as k, buildDynamicPropConfig as l, RETIRED_ENGINE_MESSAGE as lt, camelToKebab as m, parseFilesJson as mt, diffFilePlans as n, DRIVER_NAMESPACES as nt, readTsconfigAliasPairs as o, createExcludeMatcher as ot, serializeStaticCss as p, isAdvisorySourceDiagnostic as pt, walkPackageSources as q, hashReplacementPlans as r, REPLACEABLE_DEFAULT_EXCLUDE as rt, formatRustTimingWaterfall as s, resolveMode as st, toWatchKeys as t, DEFAULT_EXCLUDE as tt, runStructuralSelfCheck as u, assertNoRetiredEngineSelection as ut, runProjectAnalysis as v, ENGINE_TRANSFORM_EXTENSIONS as vt, surfaceManifestDiagnostics as w, assembleStylesheet as wt, VOCABULARY_COLLISION as x, contentHash as xt, applyUnitFallback as y, isEngineTransformExtension as yt, excludeCollectedPackages as z };
3792
+ export { assertKnownOptionKeys as $, substituteSheetAssets as A, validateLayerOrder as At, unresolvableIncludesMessage as B, resolveAssetFile as C, unresolvedParentName as Ct, generatedModuleCode as D, ANIMUS_LAYERS as Dt, findSheetAssetSpecifiers as E, buildAnalyzeProjectArgs as Et, findPackageRoot as F, compareDiscoveryOrder as G, createSourceIdentity as H, firstOwners as I, CORE_OPTION_KEYS as J, discoverFiles as K, isExcludedPackageRelativePath as L, collectExternalPackageSources as M, excludeCollectedPackages as N, reportSurvivingAssetPlaceholders as O, assembleStylesheet as Ot, extractSystemFilePackages as P, STRUCTURAL_EXCLUDE as Q, resolveAbsolutePathSpecifier as R, enforceExternalTokenContracts as S, unreadableSourceDiagnostic as St, findAssetSpecifiers as T, contentHash as Tt, isPathWithinRoot as U, walkPackageSources as V, sharesVolumeRoot as W, DRIVER_NAMESPACES as X, DEFAULT_EXCLUDE as Y, REPLACEABLE_DEFAULT_EXCLUDE as Z, runProjectAnalysis as _, VOCABULARY_LEGACY_VERB as _t, assertNoErrorDiagnostics as a, createV2EngineApi as at, resolveLightningTargets as b, surfaceManifestDiagnostics as bt, buildDynamicPropConfig as c, parseFilesJson as ct, applyPrefix as d, DEFAULT_EXTENSIONS as dt, createExcludeMatcher as et, serializeStaticCss as f, ENGINE_TRANSFORM_EXTENSIONS as ft, clearEngineCache as g, VOCABULARY_COLLISION as gt, buildAnalysisInputs as h, INVALID_PROPERTY_REGISTRATION as ht, snapshotFilePlans as i, assertNoRetiredEngineSelection as it, createSourceCorpus as j, __require as jt, substituteAssetPlaceholders as k, stripLeadingLayerDeclaration as kt, runStructuralSelfCheck as l, projectExternalFileOwners as lt, stableStringify as m, preprocessMdx as mt, diffFilePlans as n, resolveVerbosity as nt, readTsconfigAliasPairs as o, ingestSourceEntries as ot, camelToKebab as p, isEngineTransformExtension as pt, AnimusConfigError as q, hashReplacementPlans as r, RETIRED_ENGINE_MESSAGE as rt, buildSystemPropsModule as s, isAdvisorySourceDiagnostic as st, toWatchKeys as t, resolveMode as tt, loadSystemConfig as u, adaptSvelteSource as ut, applyUnitFallback as v, isDeletedSource as vt, resolveThroughPathAliases as w, vocabularyWitnessDiagnostics as wt, buildPathAliasesJson as x, systemLoadDiagnostics as xt, postProcessCss as y, isUnresolvedParentDrop as yt, staleDistIncludesMessage as z };