@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
@@ -105,7 +105,8 @@ function buildAnalyzeProjectArgs(inputs) {
105
105
  inputs.transformSourcesJson,
106
106
  inputs.transformProvenanceJson ?? null
107
107
  ];
108
- if (inputs.declarationScalesJson) args.push(inputs.declarationScalesJson);
108
+ if (inputs.declarationScalesJson || inputs.analysisContextJson) args.push(inputs.declarationScalesJson);
109
+ if (inputs.analysisContextJson) args.push(inputs.analysisContextJson);
109
110
  return args;
110
111
  }
111
112
  //#endregion
@@ -125,6 +126,224 @@ function parseInternalWire(json, wire) {
125
126
  return parsed;
126
127
  }
127
128
  //#endregion
129
+ //#region pipeline/manifest-diagnostics.ts
130
+ /** Stable code for selector forms with no substitutable subject; the
131
+ * extraction engine mints the same string. */
132
+ const SELECTOR_UNSUPPORTED_SUBJECT = "animus.selector.unsupported-subject";
133
+ /** The one matcher for the engine's unresolved-parent chain-drop message;
134
+ * consumers match through the exported helpers, never their own copy. */
135
+ const UNRESOLVED_PARENT_RE = /chain dropped: could not resolve parent component '([^']+)'/;
136
+ function isUnresolvedParentDrop(diagnostic) {
137
+ return UNRESOLVED_PARENT_RE.test(diagnostic.message);
138
+ }
139
+ function unresolvedParentName(diagnostic) {
140
+ return UNRESOLVED_PARENT_RE.exec(diagnostic.message)?.[1] ?? null;
141
+ }
142
+ /** True when the selector carries at least one substitutable `&` subject
143
+ * outside quoted text. */
144
+ function hasSelectorSubject(value) {
145
+ let quote = null;
146
+ let escaped = false;
147
+ for (const c of value) {
148
+ if (escaped) {
149
+ escaped = false;
150
+ continue;
151
+ }
152
+ if (c === "\\") {
153
+ escaped = true;
154
+ continue;
155
+ }
156
+ if (quote !== null) {
157
+ if (c === quote) quote = null;
158
+ } else if (c === "\"" || c === "'") quote = c;
159
+ else if (c === "&") return true;
160
+ }
161
+ return false;
162
+ }
163
+ /**
164
+ * Coded diagnostics for selector-shaped alias values with no substitutable
165
+ * subject; alias values never reach the evaluator's own key guard.
166
+ */
167
+ function collectSelectorAliasDiagnostics(selectorAliasesJson) {
168
+ if (!selectorAliasesJson) return [];
169
+ const aliases = parseInternalWire(selectorAliasesJson, "selectorAliasesJson (the system loader's selector-alias registry)");
170
+ const diagnostics = [];
171
+ for (const [name, value] of Object.entries(aliases)) if (value.includes("&") && !hasSelectorSubject(value)) diagnostics.push({
172
+ file: "system",
173
+ component: name,
174
+ kind: "warn",
175
+ message: `selector alias '${name}' value '${value}' has no substitutable '&' subject outside quoted text (${SELECTOR_UNSUPPORTED_SUBJECT})`,
176
+ code: SELECTOR_UNSUPPORTED_SUBJECT,
177
+ severity: severityFor(SELECTOR_UNSUPPORTED_SUBJECT)
178
+ });
179
+ return diagnostics;
180
+ }
181
+ const UNREADABLE_SOURCE_FILE = "animus.ingestion.unreadable-source-file";
182
+ /** What a skipped source file costs: the analysis never sees what it
183
+ * renders. */
184
+ 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";
185
+ /**
186
+ * A configured source file that could not be read is a LOST INPUT: the
187
+ * emitted artifacts silently lack whatever that file declared.
188
+ */
189
+ /** Whether a failed source read means the file was deleted after discovery
190
+ * (an editor's delete-and-recreate, a branch switch): a deletion, not lost
191
+ * input. */
192
+ function isDeletedSource(error) {
193
+ return error?.code === "ENOENT";
194
+ }
195
+ function unreadableSourceDiagnostic(relPath, error) {
196
+ return {
197
+ file: relPath,
198
+ component: "source",
199
+ kind: "warn",
200
+ message: `configured source file ${relPath} could not be read: ${String(error)} (${SKIPPED_SOURCE_COST})`,
201
+ code: UNREADABLE_SOURCE_FILE,
202
+ severity: severityFor(UNREADABLE_SOURCE_FILE)
203
+ };
204
+ }
205
+ /** The collision entry code minted by the system package's merge. */
206
+ const VOCABULARY_COLLISION = "animus.vocabulary.collision";
207
+ /** A theme `@property` registration browsers would ignore; it is not emitted. */
208
+ const INVALID_PROPERTY_REGISTRATION = "animus.theme.invalid-property-registration";
209
+ /** Minted when a sealed kit with registered vocabulary is consumed through
210
+ * the deprecated `from()`/`includes:` verbs. */
211
+ const VOCABULARY_LEGACY_VERB = "animus.vocabulary.legacy-verb";
212
+ /** A `var()` fallback that never applies: the property is registered with an
213
+ * initial value. */
214
+ const PROPERTY_FALLBACK_SUPPRESSED = "animus.property.fallback-suppressed";
215
+ /** The same, where the fallback heads a chain of further `var()` reads. */
216
+ const PROPERTY_FALLBACK_CHAIN_SUPPRESSED = "animus.property.fallback-chain-suppressed";
217
+ /** A declared property set in keyframes or transitioned without a typed
218
+ * registration, so it does not interpolate. */
219
+ const PROPERTY_UNREGISTERED_ANIMATION = "animus.property.unregistered-animation";
220
+ /** A custom property whose resolved value is `var()` of itself, which makes
221
+ * it invalid at computed-value time. */
222
+ const PROPERTY_SELF_REFERENCE = "animus.property.self-reference";
223
+ /** A custom property that reads itself only inside another `var()`'s
224
+ * fallback, so whether it is cyclic depends on the browser. */
225
+ const PROPERTY_FALLBACK_SELF_REFERENCE = "animus.property.fallback-self-reference";
226
+ /** A prefix renamed contextual variables without `prefixContextualVars`, so
227
+ * their declared names no longer resolve. */
228
+ const PREFIX_CONTEXTUAL_VARS_UNPREFIXED = "animus.prefix.contextual-vars-unprefixed";
229
+ /** Under `prefixContextualVars`, a final name that collides with a runtime
230
+ * transport variable or a theme variable of the same spelling. */
231
+ const PREFIX_NAME_CONFLICT = "animus.prefix.name-conflict";
232
+ const DIAGNOSTIC_SEVERITY = /* @__PURE__ */ new Map([
233
+ [SELECTOR_UNSUPPORTED_SUBJECT, "error"],
234
+ [UNREADABLE_SOURCE_FILE, "error"],
235
+ [VOCABULARY_COLLISION, "warn"],
236
+ [VOCABULARY_LEGACY_VERB, "warn"],
237
+ [INVALID_PROPERTY_REGISTRATION, "error"],
238
+ [PROPERTY_FALLBACK_SUPPRESSED, "info"],
239
+ [PROPERTY_FALLBACK_CHAIN_SUPPRESSED, "warn"],
240
+ [PROPERTY_UNREGISTERED_ANIMATION, "info"],
241
+ [PROPERTY_SELF_REFERENCE, "warn"],
242
+ [PROPERTY_FALLBACK_SELF_REFERENCE, "warn"],
243
+ [PREFIX_CONTEXTUAL_VARS_UNPREFIXED, "warn"],
244
+ [PREFIX_NAME_CONFLICT, "error"]
245
+ ]);
246
+ /** An unlisted code is `warn`: a witness kind from a newer system package
247
+ * must not fail the strict build of a host that predates its writer. */
248
+ function severityFor(code) {
249
+ return (code === void 0 ? void 0 : DIAGNOSTIC_SEVERITY.get(code)) ?? "warn";
250
+ }
251
+ /**
252
+ * The one mapper of a sealed system's witness entries for every host: the
253
+ * loader's evaluation host shims `console`, so the record is the channel.
254
+ */
255
+ function vocabularyWitnessDiagnostics(vocabularyWitnessesJson) {
256
+ if (!vocabularyWitnessesJson) return [];
257
+ const entries = parseInternalWire(vocabularyWitnessesJson, "vocabularyWitnessesJson (the sealed system's vocabulary witness record)");
258
+ const diagnostics = [];
259
+ for (const entry of entries) if (entry.code === "animus.vocabulary.collision") diagnostics.push({
260
+ file: "system",
261
+ component: entry.name ?? "keyframes",
262
+ kind: "warn",
263
+ message: `keyframes vocabulary "${entry.name}" is registered by both ${entry.loser} and ${entry.winner} — ${entry.winner} wins; rename one collection (${entry.code})`,
264
+ code: entry.code,
265
+ severity: severityFor(entry.code)
266
+ });
267
+ else if (entry.code === "animus.vocabulary.legacy-verb") diagnostics.push({
268
+ file: "system",
269
+ component: "keyframes",
270
+ kind: "warn",
271
+ 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})`,
272
+ code: entry.code,
273
+ severity: severityFor(entry.code)
274
+ });
275
+ else diagnostics.push({
276
+ file: "system",
277
+ component: "vocabulary",
278
+ kind: "warn",
279
+ 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})` : ""}`,
280
+ code: entry.code,
281
+ severity: severityFor(entry.code)
282
+ });
283
+ return diagnostics;
284
+ }
285
+ /** The diagnostics a loaded system's own records carry: vocabulary
286
+ * witnesses and invalid `@property` registrations. */
287
+ function systemLoadDiagnostics(system) {
288
+ const registrations = (system.invalidPropertyRegistrations ?? []).map(({ name, reason }) => ({
289
+ file: "system",
290
+ component: name,
291
+ kind: "warn",
292
+ 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})`,
293
+ code: INVALID_PROPERTY_REGISTRATION,
294
+ severity: severityFor(INVALID_PROPERTY_REGISTRATION)
295
+ }));
296
+ const prefixed = system.legacyPrefixedContextualVars ?? [];
297
+ const unprefixed = prefixed.length === 0 ? [] : [{
298
+ file: "system",
299
+ component: "prefix",
300
+ kind: "warn",
301
+ 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})`,
302
+ code: PREFIX_CONTEXTUAL_VARS_UNPREFIXED,
303
+ severity: severityFor(PREFIX_CONTEXTUAL_VARS_UNPREFIXED)
304
+ }];
305
+ const conflicts = (system.prefixNameConflicts ?? []).map(({ name, final, reason }) => ({
306
+ file: "system",
307
+ component: `--${name}`,
308
+ kind: "error",
309
+ 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})`,
310
+ code: PREFIX_NAME_CONFLICT,
311
+ severity: severityFor(PREFIX_NAME_CONFLICT)
312
+ }));
313
+ return [
314
+ ...vocabularyWitnessDiagnostics(system.vocabularyWitnessesJson),
315
+ ...registrations,
316
+ ...unprefixed,
317
+ ...conflicts
318
+ ];
319
+ }
320
+ /**
321
+ * The single strict-escalation policy point for both extraction plugins:
322
+ * error-severity diagnostics throw together under `strict`, else warn.
323
+ */
324
+ function surfaceManifestDiagnostics(manifest, warn, policy = {}) {
325
+ const errors = [];
326
+ const diagnostics = policy.prepend?.length ? [...policy.prepend, ...manifest.diagnostics ?? []] : manifest.diagnostics ?? [];
327
+ for (const diagnostic of diagnostics) {
328
+ let line = null;
329
+ if (diagnostic.kind === "bail") line = `⚠ ${diagnostic.component} not extracted: ${diagnostic.message}`;
330
+ else if (diagnostic.kind === "skip") line = `⚠ ${diagnostic.component}: skipped ${diagnostic.message}`;
331
+ else if (diagnostic.kind === "warn") line = `${diagnostic.severity === "info" ? "ℹ" : "⚠"} ${diagnostic.file}: ${diagnostic.component}: ${diagnostic.message}`;
332
+ if (line === null) continue;
333
+ if (diagnostic.code && !diagnostic.message.includes(diagnostic.code)) line += ` [${diagnostic.code}]`;
334
+ if (diagnostic.severity === "info") {
335
+ policy.info?.(line);
336
+ continue;
337
+ }
338
+ if (policy.strict && diagnostic.severity === "error") {
339
+ errors.push(`${diagnostic.code ?? "error"} — ${diagnostic.component}: ${diagnostic.message}`);
340
+ continue;
341
+ }
342
+ warn(line);
343
+ }
344
+ if (errors.length > 0) throw new Error(`[animus] strict: ${errors.length} error diagnostic(s):\n${errors.join("\n")}`);
345
+ }
346
+ //#endregion
128
347
  //#region pipeline/mdx-preprocessor.ts
129
348
  /**
130
349
  * MDX compiled to scanner-consumable JSX, plus the extension sets every
@@ -1049,7 +1268,8 @@ function withoutInvalidOriginals(result, invalidOriginals) {
1049
1268
  ...result,
1050
1269
  originalEntries: result.originalEntries.filter((entry) => !invalidOriginals.has(entry.path)),
1051
1270
  analysisEntries: result.analysisEntries.filter((entry) => analysisPaths.has(entry.path)),
1052
- ownership
1271
+ ownership,
1272
+ skippedOriginals: [...invalidOriginals].sort()
1053
1273
  };
1054
1274
  }
1055
1275
  /**
@@ -1089,11 +1309,12 @@ function createSourceIngestor(host) {
1089
1309
  const invalidOriginals = new Set(fatal.map((diagnostic) => diagnostic.originalPath));
1090
1310
  if (diagnostics.length === 0) return invalidOriginals;
1091
1311
  if (host.strict() && fatal.length > 0) {
1092
- const lines = fatal.map((diagnostic) => `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}`);
1312
+ const lines = fatal.map((diagnostic) => `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message} (${SKIPPED_SOURCE_COST})`);
1093
1313
  throw new Error(`${host.prefix} ${lines.join(`\n${host.prefix} `)}`);
1094
1314
  }
1095
1315
  for (const diagnostic of diagnostics) {
1096
- const line = `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}`;
1316
+ const skipped = isAdvisorySourceDiagnostic(diagnostic) ? "" : ` (${SKIPPED_SOURCE_COST})`;
1317
+ const line = `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}${skipped}`;
1097
1318
  let warned = warnedByOriginal.get(diagnostic.originalPath);
1098
1319
  if (!warned) {
1099
1320
  warned = /* @__PURE__ */ new Set();
@@ -1128,7 +1349,7 @@ function createV2EngineApi(deps) {
1128
1349
  return {
1129
1350
  loadSystemModule: (...args) => native.loadSystemModule(...args),
1130
1351
  extractFacts: (filesJson) => native.extractFacts(filesJson),
1131
- analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson, transformSourcesJson, transformProvenanceJson, declarationScalesJson) => {
1352
+ analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson, transformSourcesJson, transformProvenanceJson, declarationScalesJson, analysisContextJson) => {
1132
1353
  const filesJson = deps.rehydrateFilesJson ? deps.rehydrateFilesJson(filesJsonRaw) : filesJsonRaw;
1133
1354
  const sent = /* @__PURE__ */ new Map();
1134
1355
  for (const entry of parseFilesJson(filesJson, label)) sent.set(entry.path, entry.source);
@@ -1155,6 +1376,7 @@ function createV2EngineApi(deps) {
1155
1376
  transformSourcesJson: transformSourcesJson ?? void 0,
1156
1377
  transformProvenanceJson: transformProvenanceJson ?? void 0,
1157
1378
  declarationScalesJson,
1379
+ analysisContextJson,
1158
1380
  devMode
1159
1381
  };
1160
1382
  const engine = new native.ExtractEngine(config);
@@ -1186,7 +1408,7 @@ function createV2EngineApi(deps) {
1186
1408
  }
1187
1409
  //#endregion
1188
1410
  //#region pipeline/engine-retirement.ts
1189
- 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";
1411
+ 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.";
1190
1412
  function assertNoRetiredEngineSelection(engineOption) {
1191
1413
  if (engineOption === "v1" || process.env.ANIMUS_ENGINE === "v1") throw new Error(RETIRED_ENGINE_MESSAGE);
1192
1414
  }
@@ -1227,6 +1449,7 @@ const CORE_OPTION_KEYS = /* @__PURE__ */ new Set([
1227
1449
  "strict",
1228
1450
  "verbose",
1229
1451
  "prefix",
1452
+ "prefixContextualVars",
1230
1453
  "targets",
1231
1454
  "minify",
1232
1455
  "staticCss",
@@ -1320,6 +1543,11 @@ const CORE_VALUE_GATES = [
1320
1543
  ok: isString,
1321
1544
  expected: "a string"
1322
1545
  },
1546
+ {
1547
+ key: "prefixContextualVars",
1548
+ ok: isBoolean,
1549
+ expected: "a boolean"
1550
+ },
1323
1551
  {
1324
1552
  key: "targets",
1325
1553
  ok: isStringOrStringArray,
@@ -2018,31 +2246,148 @@ function createSourceCorpus(host, ingestor = createSourceIngestor(host)) {
2018
2246
  //#endregion
2019
2247
  //#region pipeline/asset-placeholders.ts
2020
2248
  const ASSET_PLACEHOLDER_PREFIX = "animus-asset:";
2021
- const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2022
- const PREFIX_RE = escapeRegExp(ASSET_PLACEHOLDER_PREFIX);
2023
- const QUOTED_PLACEHOLDER_RE = new RegExp(`(['"])${PREFIX_RE}([^'"]*?)\\1`, "g");
2024
- const BARE_PLACEHOLDER_RE = new RegExp(`${PREFIX_RE}([^'")\\s]+)`, "g");
2025
- function findAssetSpecifiers(css) {
2249
+ const URL_OPEN_RE = /url\(\s*(['"]?)/gi;
2250
+ const IMAGE_SET_OPEN_RE = /(?:-webkit-)?image-set\(/gi;
2251
+ const BARE_SPECIFIER_RE = /^[^'")\s]+/;
2252
+ /** The index of the `)` that closes the `(` at `open`, outside strings. */
2253
+ function closingParen$1(css, open) {
2254
+ let depth = 0;
2255
+ let quote = null;
2256
+ for (let i = open; i < css.length; i += 1) {
2257
+ const c = css[i];
2258
+ if (quote !== null) {
2259
+ if (c === quote) quote = null;
2260
+ } else if (c === "\"" || c === "'") quote = c;
2261
+ else if (c === "(") depth += 1;
2262
+ else if (c === ")") {
2263
+ depth -= 1;
2264
+ if (depth === 0) return i;
2265
+ }
2266
+ }
2267
+ return css.length;
2268
+ }
2269
+ function quotedReference(css, quoteAt) {
2270
+ const start = quoteAt + 1;
2271
+ if (!css.startsWith("animus-asset:", start)) return null;
2272
+ const end = css.indexOf(css[quoteAt], start);
2273
+ if (end === -1) return null;
2274
+ return {
2275
+ start,
2276
+ end,
2277
+ specifier: css.slice(start + 13, end)
2278
+ };
2279
+ }
2280
+ function assetReferences(css) {
2026
2281
  if (!css.includes("animus-asset:")) return [];
2027
- const seen = /* @__PURE__ */ new Set();
2028
- const remainder = css.replace(QUOTED_PLACEHOLDER_RE, (_match, _quote, specifier) => {
2029
- seen.add(specifier);
2030
- return "";
2031
- });
2032
- for (const match of remainder.matchAll(BARE_PLACEHOLDER_RE)) seen.add(match[1]);
2033
- return [...seen];
2282
+ const references = [];
2283
+ for (const open of css.matchAll(URL_OPEN_RE)) {
2284
+ const at = (open.index ?? 0) + open[0].length;
2285
+ if (open[1]) {
2286
+ const reference = quotedReference(css, at - 1);
2287
+ if (reference) references.push(reference);
2288
+ continue;
2289
+ }
2290
+ if (!css.startsWith("animus-asset:", at)) continue;
2291
+ const specifier = BARE_SPECIFIER_RE.exec(css.slice(at + 13))?.[0];
2292
+ if (specifier === void 0) continue;
2293
+ const end = at + 13 + specifier.length;
2294
+ references.push({
2295
+ start: at,
2296
+ end,
2297
+ specifier
2298
+ });
2299
+ }
2300
+ for (const open of css.matchAll(IMAGE_SET_OPEN_RE)) {
2301
+ const paren = (open.index ?? 0) + open[0].length - 1;
2302
+ const close = closingParen$1(css, paren);
2303
+ let depth = 0;
2304
+ let candidateStart = true;
2305
+ for (let i = paren + 1; i < close; i += 1) {
2306
+ const c = css[i];
2307
+ if (/\s/.test(c)) continue;
2308
+ if (c === "\"" || c === "'") {
2309
+ if (depth === 0 && candidateStart) {
2310
+ const reference = quotedReference(css, i);
2311
+ if (reference) references.push(reference);
2312
+ }
2313
+ const end = css.indexOf(c, i + 1);
2314
+ i = end === -1 ? close : end;
2315
+ } else if (c === "(") depth += 1;
2316
+ else if (c === ")") depth -= 1;
2317
+ else if (c === "," && depth === 0) {
2318
+ candidateStart = true;
2319
+ continue;
2320
+ }
2321
+ candidateStart = false;
2322
+ }
2323
+ }
2324
+ return references.sort((a, b) => a.start - b.start);
2325
+ }
2326
+ function findAssetSpecifiers(css) {
2327
+ return [...new Set(assetReferences(css).map((reference) => reference.specifier))];
2034
2328
  }
2035
2329
  function substituteAssetPlaceholders(css, urlBySpecifier) {
2036
2330
  if (urlBySpecifier.size === 0) return css;
2037
- if (!css.includes("animus-asset:")) return css;
2038
- const specifiers = [...urlBySpecifier.keys()].sort((a, b) => b.length - a.length);
2039
2331
  let out = css;
2040
- for (const specifier of specifiers) {
2041
- const placeholder = new RegExp(escapeRegExp(ASSET_PLACEHOLDER_PREFIX + specifier) + String.raw`(?=['")\s]|$)`, "g");
2042
- out = out.replace(placeholder, () => urlBySpecifier.get(specifier));
2332
+ for (const reference of assetReferences(css).reverse()) {
2333
+ const url = urlBySpecifier.get(reference.specifier);
2334
+ if (url === void 0) continue;
2335
+ out = out.slice(0, reference.start) + url + out.slice(reference.end);
2043
2336
  }
2044
2337
  return out;
2045
2338
  }
2339
+ /**
2340
+ * Specifiers of placeholder text anywhere inside a `url()` or `image-set()`
2341
+ * call, a reference or not: after substitution any of it is a broken load.
2342
+ */
2343
+ function placeholdersInAssetCalls(css) {
2344
+ if (!css.includes("animus-asset:")) return [];
2345
+ const calls = [...css.matchAll(URL_OPEN_RE), ...css.matchAll(IMAGE_SET_OPEN_RE)].map((open) => {
2346
+ const paren = css.indexOf("(", open.index ?? 0);
2347
+ return [paren, closingParen$1(css, paren)];
2348
+ });
2349
+ const found = /* @__PURE__ */ new Set();
2350
+ for (let at = css.indexOf(ASSET_PLACEHOLDER_PREFIX); at !== -1; at = css.indexOf(ASSET_PLACEHOLDER_PREFIX, at + 1)) {
2351
+ if (!calls.some(([open, close]) => open < at && at < close)) continue;
2352
+ found.add(/^[^'")\s,]*/.exec(css.slice(at + 13))[0]);
2353
+ }
2354
+ return [...found];
2355
+ }
2356
+ function findSheetAssetSpecifiers(sheets) {
2357
+ return [.../* @__PURE__ */ new Set([
2358
+ ...findAssetSpecifiers(sheets.variableCss),
2359
+ ...findAssetSpecifiers(sheets.globalCss),
2360
+ ...findAssetSpecifiers(sheets.componentCss)
2361
+ ])];
2362
+ }
2363
+ /** The one substitution step every emitted sheet passes through. */
2364
+ function substituteSheetAssets(sheets, urlBySpecifier) {
2365
+ return {
2366
+ variableCss: substituteAssetPlaceholders(sheets.variableCss, urlBySpecifier),
2367
+ globalCss: substituteAssetPlaceholders(sheets.globalCss, urlBySpecifier),
2368
+ componentCss: substituteAssetPlaceholders(sheets.componentCss, urlBySpecifier)
2369
+ };
2370
+ }
2371
+ const UNSUBSTITUTED_ASSET_CODE = "animus.asset.unsubstituted-placeholder";
2372
+ /** A placeholder in emitted CSS is a broken URL in the browser: it fails a
2373
+ * strict build and warns otherwise. */
2374
+ function reportSurvivingAssetPlaceholders(css, report) {
2375
+ const specifiers = placeholdersInAssetCalls(css);
2376
+ if (specifiers.length === 0) return;
2377
+ const message = `${report.prefix} asset() placeholders reached ${report.surface ?? "emitted CSS"} unsubstituted: ${specifiers.join(", ")} (${UNSUBSTITUTED_ASSET_CODE})`;
2378
+ if (report.strict) throw new Error(message);
2379
+ report.warn(message);
2380
+ }
2381
+ /**
2382
+ * The runtime code an analysis generates: each dynamic prop config, which the
2383
+ * shared prop map module carries, and each component's replacement. Extraction
2384
+ * lifts asset() out of them into root variables, so none should hold one.
2385
+ * Both embed values as JSON strings, so their quote and backslash escapes are
2386
+ * undone for scanning: the text is for `findAssetSpecifiers`, not to run.
2387
+ */
2388
+ function generatedModuleCode(manifest) {
2389
+ return [JSON.stringify(manifest.dynamic_props), ...Object.values(manifest.components).map((component) => component.replacement)].join("\n").replace(/\\(["\\])/g, "$1");
2390
+ }
2046
2391
  //#endregion
2047
2392
  //#region pipeline/resolve-asset.ts
2048
2393
  /**
@@ -2266,160 +2611,505 @@ function postProcessCss(css, opts) {
2266
2611
  }
2267
2612
  }
2268
2613
  //#endregion
2269
- //#region pipeline/manifest-diagnostics.ts
2270
- /** Stable code for selector forms with no substitutable subject; the
2271
- * extraction engine mints the same string. */
2272
- const SELECTOR_UNSUPPORTED_SUBJECT = "animus.selector.unsupported-subject";
2273
- /** The one matcher for the engine's unresolved-parent chain-drop message;
2274
- * consumers match through the exported helpers, never their own copy. */
2275
- const UNRESOLVED_PARENT_RE = /chain dropped: could not resolve parent component '([^']+)'/;
2276
- function isUnresolvedParentDrop(diagnostic) {
2277
- return UNRESOLVED_PARENT_RE.test(diagnostic.message);
2278
- }
2279
- function unresolvedParentName(diagnostic) {
2280
- return UNRESOLVED_PARENT_RE.exec(diagnostic.message)?.[1] ?? null;
2614
+ //#region pipeline/property-names.ts
2615
+ function createPropertyNames(declared, prefix, themeNames = []) {
2616
+ const finals = /* @__PURE__ */ new Map();
2617
+ for (const name of [...new Set(declared)].sort()) finals.set(name, `${prefix}-${name}`);
2618
+ const aliases = /* @__PURE__ */ new Map();
2619
+ for (const [name, final] of finals) if (!finals.has(final)) aliases.set(final, name);
2620
+ const others = /* @__PURE__ */ new Map();
2621
+ for (const name of [...new Set(themeNames)].sort()) if (!finals.has(name)) others.set(name, `${prefix}-${name}`);
2622
+ return {
2623
+ identity: (spelling) => finals.has(spelling) ? spelling : aliases.get(spelling) ?? (others.has(spelling) ? spelling : void 0),
2624
+ finalName: (name) => finals.get(name) ?? others.get(name),
2625
+ ambiguous: () => [...others.keys()].filter((name) => aliases.has(name)).map((name) => ({
2626
+ name,
2627
+ contextual: aliases.get(name) ?? name
2628
+ }))
2629
+ };
2281
2630
  }
2282
- /** True when the selector carries at least one substitutable `&` subject
2283
- * outside quoted text. */
2284
- function hasSelectorSubject(value) {
2285
- let quote = null;
2286
- let escaped = false;
2287
- for (const c of value) {
2288
- if (escaped) {
2289
- escaped = false;
2290
- continue;
2291
- }
2292
- if (c === "\\") {
2293
- escaped = true;
2631
+ const NAME_CHAR = /[\w-]/;
2632
+ /**
2633
+ * The name starting at `from`, read as CSS tokenizes an identifier: ASCII
2634
+ * letters, digits, `-` and `_`, every non-ASCII code point, and escapes.
2635
+ */
2636
+ function readCustomPropertyName(css, from) {
2637
+ let end = from;
2638
+ let name = "";
2639
+ while (end < css.length) {
2640
+ if (isNameChar(css[end])) {
2641
+ name += css[end];
2642
+ end += 1;
2294
2643
  continue;
2295
2644
  }
2296
- if (quote !== null) {
2297
- if (c === quote) quote = null;
2298
- } else if (c === "\"" || c === "'") quote = c;
2299
- else if (c === "&") return true;
2645
+ const escape = escapeAt(css, end);
2646
+ if (escape === null) break;
2647
+ name += escape.codePoint;
2648
+ end += escape.length;
2300
2649
  }
2301
- return false;
2650
+ return {
2651
+ end,
2652
+ name
2653
+ };
2654
+ }
2655
+ /** A valid escape at `at`: up to six hex digits and one following
2656
+ * whitespace, or any other code point but a newline. */
2657
+ function escapeAt(css, at) {
2658
+ if (css[at] !== "\\") return null;
2659
+ const next = css.codePointAt(at + 1);
2660
+ if (next === void 0 || next === 10 || next === 13 || next === 12) return null;
2661
+ const digits = /^[0-9a-fA-F]{1,6}/.exec(css.slice(at + 1, at + 7))?.[0];
2662
+ if (digits === void 0) return {
2663
+ codePoint: String.fromCodePoint(next),
2664
+ length: 1 + String.fromCodePoint(next).length
2665
+ };
2666
+ const after = css.slice(at + 1 + digits.length);
2667
+ const space = after.startsWith("\r\n") ? 2 : /^[ \t\n\r\f]/.test(after) ? 1 : 0;
2668
+ const value = parseInt(digits, 16);
2669
+ const valid = value !== 0 && value <= 1114111 && (value < 55296 || value > 57343);
2670
+ return {
2671
+ codePoint: String.fromCodePoint(valid ? value : 65533),
2672
+ length: 1 + digits.length + space
2673
+ };
2302
2674
  }
2303
2675
  /**
2304
- * Coded diagnostics for selector-shaped alias values with no substitutable
2305
- * subject; alias values never reach the evaluator's own key guard.
2676
+ * Gives every custom-property token that names a managed property its final
2677
+ * name, in one pass: declaration keys, `var()` reads at any depth (fallbacks
2678
+ * included), `@property` names, transition-property lists and style queries.
2679
+ * A name is read whole, escapes decoded, so an undeclared name that starts
2680
+ * with a managed one is never renamed in part. Quoted strings, `url()` and
2681
+ * comments are copied unchanged.
2306
2682
  */
2307
- function collectSelectorAliasDiagnostics(selectorAliasesJson) {
2308
- if (!selectorAliasesJson) return [];
2309
- const aliases = parseInternalWire(selectorAliasesJson, "selectorAliasesJson (the system loader's selector-alias registry)");
2310
- const diagnostics = [];
2311
- for (const [name, value] of Object.entries(aliases)) if (value.includes("&") && !hasSelectorSubject(value)) diagnostics.push({
2312
- file: "system",
2313
- component: name,
2314
- kind: "warn",
2315
- message: `selector alias '${name}' value '${value}' has no substitutable '&' subject outside quoted text (${SELECTOR_UNSUPPORTED_SUBJECT})`,
2316
- code: SELECTOR_UNSUPPORTED_SUBJECT,
2317
- severity: severityFor(SELECTOR_UNSUPPORTED_SUBJECT)
2318
- });
2319
- return diagnostics;
2683
+ function renameCustomProperties(css, names) {
2684
+ let out = "";
2685
+ let i = 0;
2686
+ while (i < css.length) {
2687
+ const c = css[i];
2688
+ if (c === "\"" || c === "'") {
2689
+ const end = closingQuote$1(css, i);
2690
+ out += css.slice(i, end);
2691
+ i = end;
2692
+ } else if (css.startsWith("/*", i)) {
2693
+ const close = css.indexOf("*/", i + 2);
2694
+ const end = close === -1 ? css.length : close + 2;
2695
+ out += css.slice(i, end);
2696
+ i = end;
2697
+ } else if (/^url\(/i.test(css.slice(i, i + 4)) && !isNameChar(css[i - 1])) {
2698
+ const end = closingUrl(css, i + 4);
2699
+ out += css.slice(i, end);
2700
+ i = end;
2701
+ } else if (css.startsWith("--", i) && !isNameChar(css[i - 1])) {
2702
+ const { end, name } = readCustomPropertyName(css, i + 2);
2703
+ const identity = names.identity(name);
2704
+ const final = identity === void 0 ? void 0 : names.finalName(identity);
2705
+ out += final === void 0 ? css.slice(i, end) : `--${final}`;
2706
+ i = end;
2707
+ } else {
2708
+ out += c;
2709
+ i += 1;
2710
+ }
2711
+ }
2712
+ return out;
2320
2713
  }
2321
- const UNREADABLE_SOURCE_FILE = "animus.ingestion.unreadable-source-file";
2714
+ /** A name code point, or a UTF-16 unit of a non-ASCII one. */
2715
+ function isNameChar(c) {
2716
+ return c !== void 0 && (NAME_CHAR.test(c) || c.charCodeAt(0) >= 128);
2717
+ }
2718
+ function closingQuote$1(css, open) {
2719
+ const quote = css[open];
2720
+ let i = open + 1;
2721
+ while (i < css.length && css[i] !== quote) i += css[i] === "\\" ? 2 : 1;
2722
+ return Math.min(i + 1, css.length);
2723
+ }
2724
+ /** The index after the `)` that closes an unquoted or quoted `url(`. */
2725
+ function closingUrl(css, from) {
2726
+ let i = from;
2727
+ while (i < css.length && css[i] !== ")") i = css[i] === "\"" || css[i] === "'" ? closingQuote$1(css, i) : i + 1;
2728
+ return Math.min(i + 1, css.length);
2729
+ }
2730
+ //#endregion
2731
+ //#region pipeline/property-registrations.ts
2732
+ const PROPERTY_RULE = /@property\s+(--[\w-]+)\s*\{[^}]*\}\n?/g;
2733
+ /** The data types a registration's `syntax` may name. */
2734
+ const SYNTAX_TYPES = /* @__PURE__ */ new Set([
2735
+ "angle",
2736
+ "color",
2737
+ "custom-ident",
2738
+ "image",
2739
+ "integer",
2740
+ "length",
2741
+ "length-percentage",
2742
+ "number",
2743
+ "percentage",
2744
+ "resolution",
2745
+ "string",
2746
+ "time",
2747
+ "transform-function",
2748
+ "transform-list",
2749
+ "url"
2750
+ ]);
2751
+ const SYNTAX_COMPONENT = /^(?:<([a-z-]+)>[+#]?|-?[a-zA-Z_][\w-]*)$/;
2752
+ const INITIAL_VALUE = /(initial-value\s*:\s*)([^;}]*)/;
2753
+ const MATH_FUNCTION = /\b(?:calc|min|max|clamp)\(/i;
2322
2754
  /**
2323
- * A configured source file that could not be read is a LOST INPUT: the
2324
- * emitted artifacts silently lack whatever that file declared.
2755
+ * Removes the `@property` rules browsers would ignore — an unknown syntax
2756
+ * component, a syntax other than `*` without an initial value, an initial
2757
+ * value that substitutes another value or depends on context, or one the
2758
+ * syntax does not accept. Minifying a stylesheet that contains such a rule
2759
+ * can also fail. An all-absolute math initial value is written as its
2760
+ * computed value, which the minifier reads.
2325
2761
  */
2326
- function unreadableSourceDiagnostic(relPath, error) {
2762
+ function splitInvalidPropertyRegistrations(css) {
2763
+ const invalid = [];
2327
2764
  return {
2328
- file: relPath,
2329
- component: "source",
2330
- kind: "warn",
2331
- message: `configured source file ${relPath} could not be read and was skipped: ${String(error)}`,
2332
- code: UNREADABLE_SOURCE_FILE,
2333
- severity: severityFor(UNREADABLE_SOURCE_FILE)
2765
+ css: css.replace(PROPERTY_RULE, (rule, name) => {
2766
+ const checked = checkRegistration(rule);
2767
+ if ("rule" in checked) return checked.rule;
2768
+ invalid.push({
2769
+ name,
2770
+ reason: checked.reason
2771
+ });
2772
+ return "";
2773
+ }),
2774
+ invalid
2334
2775
  };
2335
2776
  }
2336
- /** The collision entry code minted by the system package's merge. */
2337
- const VOCABULARY_COLLISION = "animus.vocabulary.collision";
2338
- /** A theme `@property` registration browsers would ignore; it is not emitted. */
2339
- const INVALID_PROPERTY_REGISTRATION = "animus.theme.invalid-property-registration";
2340
- /** Minted when a sealed kit with registered vocabulary is consumed through
2341
- * the deprecated `from()`/`includes:` verbs. */
2342
- const VOCABULARY_LEGACY_VERB = "animus.vocabulary.legacy-verb";
2343
- const DIAGNOSTIC_SEVERITY = /* @__PURE__ */ new Map([
2344
- [SELECTOR_UNSUPPORTED_SUBJECT, "error"],
2345
- [UNREADABLE_SOURCE_FILE, "error"],
2346
- [VOCABULARY_COLLISION, "warn"],
2347
- [VOCABULARY_LEGACY_VERB, "warn"],
2348
- [INVALID_PROPERTY_REGISTRATION, "error"]
2349
- ]);
2350
- /** An unlisted code is `warn`: a witness kind from a newer system package
2351
- * must not fail the strict build of a host that predates its writer. */
2352
- function severityFor(code) {
2353
- return (code === void 0 ? void 0 : DIAGNOSTIC_SEVERITY.get(code)) ?? "warn";
2777
+ /** The registrations in `css`, by custom-property name. */
2778
+ function propertyRegistrations(css) {
2779
+ const registrations = /* @__PURE__ */ new Map();
2780
+ for (const match of css.matchAll(PROPERTY_RULE)) registrations.set(match[1], readRegistration(match[0]));
2781
+ return registrations;
2354
2782
  }
2355
- /**
2356
- * The one mapper of a sealed system's witness entries for every host: the
2357
- * loader's evaluation host shims `console`, so the record is the channel.
2358
- */
2359
- function vocabularyWitnessDiagnostics(vocabularyWitnessesJson) {
2360
- if (!vocabularyWitnessesJson) return [];
2361
- const entries = parseInternalWire(vocabularyWitnessesJson, "vocabularyWitnessesJson (the sealed system's vocabulary witness record)");
2362
- const diagnostics = [];
2363
- for (const entry of entries) if (entry.code === "animus.vocabulary.collision") diagnostics.push({
2364
- file: "system",
2365
- component: entry.name ?? "keyframes",
2366
- kind: "warn",
2367
- message: `keyframes vocabulary "${entry.name}" is registered by both ${entry.loser} and ${entry.winner} — ${entry.winner} wins; rename one collection (${entry.code})`,
2368
- code: entry.code,
2369
- severity: severityFor(entry.code)
2370
- });
2371
- else if (entry.code === "animus.vocabulary.legacy-verb") diagnostics.push({
2372
- file: "system",
2373
- component: "keyframes",
2374
- kind: "warn",
2375
- 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})`,
2376
- code: entry.code,
2377
- severity: severityFor(entry.code)
2378
- });
2379
- else diagnostics.push({
2380
- file: "system",
2381
- component: "vocabulary",
2382
- kind: "warn",
2383
- 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})` : ""}`,
2384
- code: entry.code,
2385
- severity: severityFor(entry.code)
2386
- });
2387
- return diagnostics;
2783
+ function readRegistration(rule) {
2784
+ return {
2785
+ syntax: /syntax:\s*"([^"]*)"/.exec(rule)?.[1]?.trim() ?? "",
2786
+ initialValue: /initial-value\s*:\s*([^;}]*)/.exec(rule)?.[1]?.trim()
2787
+ };
2388
2788
  }
2389
- /** The diagnostics a loaded system's own records carry: vocabulary
2390
- * witnesses and invalid `@property` registrations. */
2391
- function systemLoadDiagnostics(system) {
2392
- const registrations = (system.invalidPropertyRegistrations ?? []).map(({ name, reason }) => ({
2393
- file: "system",
2394
- component: name,
2395
- kind: "warn",
2396
- 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})`,
2397
- code: INVALID_PROPERTY_REGISTRATION,
2398
- severity: severityFor(INVALID_PROPERTY_REGISTRATION)
2399
- }));
2400
- return [...vocabularyWitnessDiagnostics(system.vocabularyWitnessesJson), ...registrations];
2789
+ /** The rule to keep, with an all-absolute math initial value folded, or why
2790
+ * a browser would ignore it. */
2791
+ function checkRegistration(rule) {
2792
+ const { syntax, initialValue } = readRegistration(rule);
2793
+ if (initialValue !== void 0 && (0, _animus_ui_properties.substitutesValue)(initialValue)) return { reason: `initialValue "${initialValue}" substitutes another value; var(), env() and attr() are not allowed in an initial value` };
2794
+ let kept = rule;
2795
+ if (syntax !== "*") {
2796
+ const unknown = syntax.split("|").map((component) => ({
2797
+ text: component.trim(),
2798
+ match: SYNTAX_COMPONENT.exec(component.trim())
2799
+ })).find(({ match }) => match === null || match[1] !== void 0 && !SYNTAX_TYPES.has(match[1]));
2800
+ if (unknown !== void 0) return { reason: `syntax "${syntax}" has the unknown component "${unknown.text}"` };
2801
+ if (initialValue === void 0) return { reason: `syntax "${syntax}" needs an initialValue` };
2802
+ if ((0, _animus_ui_properties.dependsOnContext)(initialValue)) return { reason: `initialValue "${initialValue}" depends on context; font- and container-relative units are not allowed` };
2803
+ const folded = (0, _animus_ui_properties.foldInitialValue)(syntax, initialValue);
2804
+ if (folded !== void 0 && !folded.accepted) return { reason: `initialValue "${initialValue}" computes to the ${folded.type} ${folded.text}, which syntax "${syntax}" does not accept` };
2805
+ if (folded !== void 0) kept = rule.replace(INITIAL_VALUE, `$1${folded.text}`);
2806
+ }
2807
+ try {
2808
+ (0, lightningcss.transform)({
2809
+ filename: "animus-registrations.css",
2810
+ code: Buffer.from(kept)
2811
+ });
2812
+ return { rule: kept };
2813
+ } catch (error) {
2814
+ const detail = error instanceof Error ? error.message : String(error);
2815
+ 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` };
2816
+ return { reason: `the CSS parser rejects \`${kept.trim()}\` (${detail})` };
2817
+ }
2401
2818
  }
2819
+ //#endregion
2820
+ //#region pipeline/property-diagnostics.ts
2402
2821
  /**
2403
- * The single strict-escalation policy point for both extraction plugins:
2404
- * error-severity diagnostics throw together under `strict`, else warn.
2822
+ * Reports custom properties that will not behave as their CSS suggests: a
2823
+ * property that refers to itself, a fallback a registered initial value
2824
+ * applies ahead of, and an animated property that cannot interpolate. Names
2825
+ * are read as CSS tokenizes them, so escapes, function case, comments and
2826
+ * quoted text match the prefix pass. The CSS itself is never changed.
2405
2827
  */
2406
- function surfaceManifestDiagnostics(manifest, warn, policy = {}) {
2407
- const errors = [];
2408
- const diagnostics = policy.prepend?.length ? [...policy.prepend, ...manifest.diagnostics ?? []] : manifest.diagnostics ?? [];
2409
- for (const diagnostic of diagnostics) {
2410
- let line = null;
2411
- if (diagnostic.kind === "bail") line = `⚠ ${diagnostic.component} not extracted: ${diagnostic.message}`;
2412
- else if (diagnostic.kind === "skip") line = `⚠ ${diagnostic.component}: skipped ${diagnostic.message}`;
2413
- else if (diagnostic.kind === "warn") line = `⚠ ${diagnostic.file}: ${diagnostic.component}: ${diagnostic.message}`;
2414
- if (line === null) continue;
2415
- if (diagnostic.code && !diagnostic.message.includes(diagnostic.code)) line += ` [${diagnostic.code}]`;
2416
- if (policy.strict && diagnostic.severity === "error") {
2417
- errors.push(`${diagnostic.code ?? "error"} — ${diagnostic.component}: ${diagnostic.message}`);
2828
+ function checkCustomProperties(input) {
2829
+ const registered = propertyRegistrations(input.system.variableCss);
2830
+ const declared = new Set(input.system.contextualProperties ?? []);
2831
+ const findings = [];
2832
+ const sheets = [{
2833
+ css: input.componentCss,
2834
+ owner: componentOwner(input.manifest)
2835
+ }, {
2836
+ css: input.globalCss,
2837
+ owner: globalOwner
2838
+ }];
2839
+ for (const { css, owner } of sheets) {
2840
+ if (!/var\(|transition|@keyframes/i.test(css)) continue;
2841
+ const parsed = declarations(css);
2842
+ const discreteRules = new Set(parsed.filter((d) => d.property === "transition-behavior" && /\ballow-discrete\b/.test(d.value)).map((d) => d.rule));
2843
+ for (const declaration of parsed) {
2844
+ const found = (code, property, message) => findings.push({
2845
+ code,
2846
+ property,
2847
+ message,
2848
+ owner: owner(declaration)
2849
+ });
2850
+ if (isSelfReference(declaration)) {
2851
+ 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`);
2852
+ continue;
2853
+ }
2854
+ 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`);
2855
+ for (const read of varReads(declaration.value)) {
2856
+ const registration = registered.get(read.name);
2857
+ if (read.fallback === void 0 || registration?.initialValue === void 0) continue;
2858
+ const chain = read.fallback.startsWith("var(");
2859
+ 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`);
2860
+ }
2861
+ for (const animated of animatedProperties(declaration, discreteRules)) {
2862
+ if (animated.discrete || !declared.has(animated.name)) continue;
2863
+ const reason = interpolationGap(registered.get(animated.name));
2864
+ if (reason === null) continue;
2865
+ 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`);
2866
+ }
2867
+ }
2868
+ }
2869
+ return toDiagnostics(findings);
2870
+ }
2871
+ /** Declarations in engine-emitted CSS: one `property: value` per `;`. */
2872
+ function declarations(css) {
2873
+ const found = [];
2874
+ const stack = [];
2875
+ let rules = 0;
2876
+ let segment = 0;
2877
+ let parens = 0;
2878
+ let quote = null;
2879
+ const take = (end) => {
2880
+ const text = css.slice(segment, end);
2881
+ const colon = text.indexOf(":");
2882
+ const current = stack[stack.length - 1];
2883
+ if (current === void 0 || colon <= 0) return;
2884
+ found.push({
2885
+ property: text.slice(0, colon).trim(),
2886
+ value: text.slice(colon + 1).replace(/;$/, "").trim(),
2887
+ start: segment + (text.length - text.trimStart().length),
2888
+ end,
2889
+ rule: current.rule,
2890
+ context: stack.map((entry) => entry.prelude)
2891
+ });
2892
+ };
2893
+ for (let i = 0; i < css.length; i += 1) {
2894
+ const c = css[i];
2895
+ if (quote !== null) {
2896
+ if (c === "\\") i += 1;
2897
+ else if (c === quote) quote = null;
2418
2898
  continue;
2419
2899
  }
2420
- warn(line);
2900
+ if (c === "\"" || c === "'") quote = c;
2901
+ else if (c === "(") parens += 1;
2902
+ else if (c === ")") parens -= 1;
2903
+ else if (parens === 0 && c === "{") {
2904
+ stack.push({
2905
+ prelude: css.slice(segment, i).trim(),
2906
+ rule: rules
2907
+ });
2908
+ rules += 1;
2909
+ segment = i + 1;
2910
+ } else if (parens === 0 && c === ";") {
2911
+ take(i + 1);
2912
+ segment = i + 1;
2913
+ } else if (parens === 0 && c === "}") {
2914
+ take(i);
2915
+ stack.pop();
2916
+ segment = i + 1;
2917
+ }
2421
2918
  }
2422
- if (errors.length > 0) throw new Error(`[animus] strict: ${errors.length} error diagnostic(s):\n${errors.join("\n")}`);
2919
+ return found;
2920
+ }
2921
+ /** `text` as a whole custom-property name, escapes decoded; `undefined`
2922
+ * when it is not one. */
2923
+ function customPropertyName(text) {
2924
+ if (!text.startsWith("--")) return void 0;
2925
+ const { end, name } = readCustomPropertyName(text, 2);
2926
+ return end === text.length ? `--${name}` : void 0;
2927
+ }
2928
+ function isSelfReference(declaration) {
2929
+ const property = customPropertyName(declaration.property);
2930
+ const reads = varReads(declaration.value);
2931
+ 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();
2932
+ }
2933
+ /** Whether every read of the declared property sits inside another read's
2934
+ * fallback, as in `--a: var(--b, var(--a))`. */
2935
+ function readsItselfOnlyInFallback(declaration) {
2936
+ const property = customPropertyName(declaration.property);
2937
+ if (property === void 0) return false;
2938
+ const reads = varReads(declaration.value);
2939
+ const own = reads.filter((read) => read.name === property);
2940
+ return own.length > 0 && own.every((read) => reads.some((outer) => outer !== read && outer.start < read.start && read.end <= outer.end));
2941
+ }
2942
+ /** Every `var()` read in `value`, nested fallbacks included, in any case
2943
+ * and spacing. Quoted text and comments hold none. */
2944
+ function varReads(value) {
2945
+ const reads = [];
2946
+ for (let at = 0; at < value.length;) {
2947
+ const c = value[at];
2948
+ if (c === "\"" || c === "'") {
2949
+ at = closingQuote(value, at);
2950
+ continue;
2951
+ }
2952
+ if (value.startsWith("/*", at)) {
2953
+ at = skipSpace(value, at);
2954
+ continue;
2955
+ }
2956
+ if (value.slice(at, at + 4).toLowerCase() !== "var(" || /[\w-]/.test(value[at - 1] ?? "")) {
2957
+ at += 1;
2958
+ continue;
2959
+ }
2960
+ const nameAt = skipSpace(value, at + 4);
2961
+ if (!value.startsWith("--", nameAt)) {
2962
+ at += 4;
2963
+ continue;
2964
+ }
2965
+ const { end: nameEnd, name } = readCustomPropertyName(value, nameAt + 2);
2966
+ const after = skipSpace(value, nameEnd);
2967
+ const close = closingParen(value, at + 4);
2968
+ reads.push({
2969
+ name: `--${name}`,
2970
+ fallback: value[after] === "," ? value.slice(after + 1, close - 1).trim() : void 0,
2971
+ start: at,
2972
+ end: close
2973
+ });
2974
+ at = after;
2975
+ }
2976
+ return reads;
2977
+ }
2978
+ /** The index after the `)` that closes the call whose arguments start at
2979
+ * `from`. Quoted text and comments are skipped. */
2980
+ function closingParen(value, from) {
2981
+ let depth = 1;
2982
+ let at = from;
2983
+ while (at < value.length && depth > 0) {
2984
+ const c = value[at];
2985
+ if (c === "\"" || c === "'") at = closingQuote(value, at);
2986
+ else if (value.startsWith("/*", at)) at = skipSpace(value, at);
2987
+ else {
2988
+ if (c === "(") depth += 1;
2989
+ else if (c === ")") depth -= 1;
2990
+ at += 1;
2991
+ }
2992
+ }
2993
+ return at;
2994
+ }
2995
+ function closingQuote(value, open) {
2996
+ let at = open + 1;
2997
+ while (at < value.length && value[at] !== value[open]) at += value[at] === "\\" ? 2 : 1;
2998
+ return Math.min(at + 1, value.length);
2999
+ }
3000
+ /** The index after CSS whitespace and comments starting at `from`. */
3001
+ function skipSpace(value, from) {
3002
+ let at = from;
3003
+ for (;;) {
3004
+ while (/[ \t\n\r\f]/.test(value[at] ?? "")) at += 1;
3005
+ if (!value.startsWith("/*", at)) return at;
3006
+ const close = value.indexOf("*/", at + 2);
3007
+ at = close === -1 ? value.length : close + 2;
3008
+ }
3009
+ }
3010
+ function withoutComments(text) {
3011
+ return text.replace(/\/\*[\s\S]*?\*\//g, "");
3012
+ }
3013
+ /** Custom properties this declaration sets in a keyframe or names in a
3014
+ * transition. */
3015
+ function animatedProperties(declaration, discreteRules) {
3016
+ const keyframes = declaration.context.find((prelude) => prelude.startsWith("@keyframes"));
3017
+ const set = customPropertyName(declaration.property);
3018
+ if (keyframes !== void 0 && set !== void 0) return [{
3019
+ name: set,
3020
+ how: `set in ${keyframes}`,
3021
+ discrete: false
3022
+ }];
3023
+ if (declaration.property !== "transition" && declaration.property !== "transition-property") return [];
3024
+ return topLevelItems(declaration.value).flatMap((item) => {
3025
+ const property = topLevelTokens(item.replace(/\/\*[\s\S]*?\*\//g, " ")).map(customPropertyName).find((name) => name !== void 0);
3026
+ return property !== void 0 ? [{
3027
+ name: property,
3028
+ how: "transitioned",
3029
+ discrete: /\ballow-discrete\b/.test(item) || discreteRules.has(declaration.rule)
3030
+ }] : [];
3031
+ });
3032
+ }
3033
+ /** The whitespace-separated tokens of `item` outside any parentheses. */
3034
+ function topLevelTokens(item) {
3035
+ const tokens = [];
3036
+ let depth = 0;
3037
+ let token = "";
3038
+ for (const c of item) {
3039
+ if (c === "(") depth += 1;
3040
+ else if (c === ")") depth -= 1;
3041
+ if (depth === 0 && /\s/.test(c)) {
3042
+ if (token) tokens.push(token);
3043
+ token = "";
3044
+ } else token += c;
3045
+ }
3046
+ if (token) tokens.push(token);
3047
+ return tokens;
3048
+ }
3049
+ function topLevelItems(value) {
3050
+ const items = [];
3051
+ let depth = 0;
3052
+ let start = 0;
3053
+ for (let i = 0; i < value.length; i += 1) if (value[i] === "(") depth += 1;
3054
+ else if (value[i] === ")") depth -= 1;
3055
+ else if (value[i] === "," && depth === 0) {
3056
+ items.push(value.slice(start, i));
3057
+ start = i + 1;
3058
+ }
3059
+ items.push(value.slice(start));
3060
+ return items;
3061
+ }
3062
+ /** Why a property cannot interpolate, or `null` when it can. */
3063
+ function interpolationGap(registration) {
3064
+ if (registration === void 0) return "it is not registered";
3065
+ if (registration.syntax === "*") return "it is registered with the universal syntax \"*\"";
3066
+ return null;
3067
+ }
3068
+ function componentOwner(manifest) {
3069
+ const components = Object.values(manifest.components ?? {});
3070
+ return (declaration) => {
3071
+ const selector = [...declaration.context].reverse().find((prelude) => !prelude.startsWith("@"));
3072
+ const classes = (selector ?? "").match(/\.-?[_a-zA-Z][\w-]*/g) ?? [];
3073
+ for (const dotted of classes) {
3074
+ const name = dotted.slice(1);
3075
+ const owner = components.find((component) => name === component.class_name || name.startsWith(`${component.class_name}--`) || component.replacement.includes(`"${name}"`));
3076
+ if (owner) return {
3077
+ file: owner.file,
3078
+ component: owner.binding
3079
+ };
3080
+ }
3081
+ return {
3082
+ file: "styles",
3083
+ component: selector ?? "stylesheet"
3084
+ };
3085
+ };
3086
+ }
3087
+ function globalOwner(declaration) {
3088
+ const keyframes = declaration.context.find((prelude) => prelude.startsWith("@keyframes"));
3089
+ const selector = [...declaration.context].reverse().find((prelude) => !prelude.startsWith("@"));
3090
+ return {
3091
+ file: "system",
3092
+ component: keyframes ?? selector ?? "global styles"
3093
+ };
3094
+ }
3095
+ /** One diagnostic per code, property and owner. */
3096
+ function toDiagnostics(findings) {
3097
+ const seen = /* @__PURE__ */ new Set();
3098
+ const diagnostics = [];
3099
+ for (const finding of findings) {
3100
+ const key = `${finding.code}|${finding.property}|${finding.owner.file}|${finding.owner.component}`;
3101
+ if (seen.has(key)) continue;
3102
+ seen.add(key);
3103
+ diagnostics.push({
3104
+ file: finding.owner.file,
3105
+ component: finding.owner.component,
3106
+ kind: "warn",
3107
+ message: `${finding.message} (${finding.code})`,
3108
+ code: finding.code,
3109
+ severity: severityFor(finding.code)
3110
+ });
3111
+ }
3112
+ return diagnostics;
2423
3113
  }
2424
3114
  //#endregion
2425
3115
  //#region pipeline/unit-fallback.ts
@@ -2498,12 +3188,33 @@ function buildAnalysisInputs(opts) {
2498
3188
  externalDirsJson: opts.externalDirs?.length && hasSourceThemeManifests(opts.system) ? JSON.stringify(opts.externalDirs) : null
2499
3189
  };
2500
3190
  if (opts.system.declarationScalesJson) inputs.declarationScalesJson = opts.system.declarationScalesJson;
3191
+ const context = opts.analysisContext;
3192
+ if (context?.skippedSources?.length || context?.unbundledComputedImports || context?.packageDirs?.length) inputs.analysisContextJson = JSON.stringify({
3193
+ skippedSources: context.skippedSources ?? [],
3194
+ unbundledComputedImports: context.unbundledComputedImports ?? false,
3195
+ packageDirs: context.packageDirs ?? []
3196
+ });
2501
3197
  return inputs;
2502
3198
  }
2503
3199
  function hasSourceThemeManifests(system) {
2504
3200
  const json = system.sourceThemeManifestsJson ?? "";
2505
3201
  return json.length > 0 && json !== "{}";
2506
3202
  }
3203
+ /** Systems whose load diagnostics were surfaced: each loaded system reports
3204
+ * them once, not again on every analysis and hot update. The set is per
3205
+ * process. Next's webpack compilers share one analysis in one process, but
3206
+ * its build worker and Turbopack builds analyze in two processes, which
3207
+ * repeat every diagnostic alike, component ones included. */
3208
+ const surfacedSystems = /* @__PURE__ */ new WeakSet();
3209
+ /** The loaded system's own diagnostics, the first time it is analyzed. A
3210
+ * strict error keeps failing every analysis until the system changes. */
3211
+ function systemDiagnostics(system, strict) {
3212
+ const diagnostics = [...collectSelectorAliasDiagnostics(system.selectorAliasesJson), ...systemLoadDiagnostics(system)];
3213
+ const blocking = strict === true && diagnostics.some((d) => d.severity === "error");
3214
+ if (surfacedSystems.has(system) && !blocking) return [];
3215
+ surfacedSystems.add(system);
3216
+ return diagnostics;
3217
+ }
2507
3218
  /**
2508
3219
  * The one analysis invocation both plugins share. Error handling stays at
2509
3220
  * the call site (strict-mode throw vs warn).
@@ -2518,11 +3229,21 @@ function runProjectAnalysis(engineApi, opts) {
2518
3229
  const extractMs = Math.round(performance.now() - t);
2519
3230
  t = performance.now();
2520
3231
  const manifest = JSON.parse(manifestJson);
3232
+ const systemErrors = systemLoadDiagnostics(opts.system).filter((diagnostic) => diagnostic.kind === "error");
3233
+ if (systemErrors.length > 0) manifest.diagnostics = [...systemErrors, ...manifest.diagnostics ?? []];
3234
+ const componentCss = applyUnitFallback(manifest.css);
3235
+ const propertyDiagnostics = checkCustomProperties({
3236
+ system: opts.system,
3237
+ manifest,
3238
+ componentCss,
3239
+ globalCss: manifest.sheets.global
3240
+ });
2521
3241
  surfaceManifestDiagnostics(manifest, opts.warn, {
2522
3242
  strict: opts.strict,
3243
+ info: opts.info,
2523
3244
  prepend: [
2524
- ...collectSelectorAliasDiagnostics(opts.system.selectorAliasesJson),
2525
- ...systemLoadDiagnostics(opts.system),
3245
+ ...systemDiagnostics(opts.system, opts.strict),
3246
+ ...propertyDiagnostics,
2526
3247
  ...opts.extraDiagnostics ?? []
2527
3248
  ]
2528
3249
  });
@@ -2531,7 +3252,7 @@ function runProjectAnalysis(engineApi, opts) {
2531
3252
  manifest,
2532
3253
  manifestJson,
2533
3254
  globalCss: manifest.sheets.global,
2534
- componentCss: applyUnitFallback(manifest.css),
3255
+ componentCss,
2535
3256
  inputs,
2536
3257
  timings: {
2537
3258
  serializeMs,
@@ -2616,87 +3337,60 @@ function applyPrefix(prefix, variableMapJson, variableCss, themeJson, contextual
2616
3337
  }
2617
3338
  return result;
2618
3339
  }
2619
- //#endregion
2620
- //#region pipeline/property-registrations.ts
2621
- const PROPERTY_RULE = /@property\s+(--[\w-]+)\s*\{[^}]*\}\n?/g;
2622
- /** The data types a registration's `syntax` may name. */
2623
- const SYNTAX_TYPES = /* @__PURE__ */ new Set([
2624
- "angle",
2625
- "color",
2626
- "custom-ident",
2627
- "image",
2628
- "integer",
2629
- "length",
2630
- "length-percentage",
2631
- "number",
2632
- "percentage",
2633
- "resolution",
2634
- "string",
2635
- "time",
2636
- "transform-function",
2637
- "transform-list",
2638
- "url"
2639
- ]);
2640
- const SYNTAX_COMPONENT = /^(?:<([a-z-]+)>[+#]?|-?[a-zA-Z_][\w-]*)$/;
2641
- /** The syntax types whose values can carry a font- or container-relative
2642
- * unit, which makes a typed initial value depend on context. */
2643
- const DIMENSION_TYPES = /* @__PURE__ */ new Set([
2644
- "image",
2645
- "length",
2646
- "length-percentage",
2647
- "transform-function",
2648
- "transform-list"
2649
- ]);
2650
- 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;
2651
- const SUBSTITUTION = /\b(?:var|env|attr)\(/i;
2652
- /** Quoted strings and `url()` hold text, never units or substitutions. */
2653
- const LITERAL_TEXT = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|url\([^)]*\)/gi;
3340
+ /** The runtime's transport variables are `--animus-<prop>`. */
3341
+ const TRANSPORT_NAMESPACE = "animus-";
2654
3342
  /**
2655
- * Removes the `@property` rules browsers would ignore — an unknown syntax
2656
- * component, a syntax other than `*` without an initial value, an initial
2657
- * value that depends on context, or one the syntax does not accept.
2658
- * Minifying a stylesheet that contains such a rule can also fail.
3343
+ * The prefix applied through one map: every variable the theme defines and
3344
+ * every declared contextual variable takes `--${prefix}-${name}`, each
3345
+ * reference renamed once. Each contextual entry carries its declared and
3346
+ * final name, so lookups run on the declared identity and the extractor
3347
+ * emits the final name.
2659
3348
  */
2660
- function splitInvalidPropertyRegistrations(css) {
2661
- const invalid = [];
3349
+ function applyPropertyNames(prefix, artifacts) {
3350
+ const byScale = artifacts.contextualVarsJson ? parseInternalWire(artifacts.contextualVarsJson, "contextualVarsJson (the theme's contextual variable names)") : null;
3351
+ const declared = Object.values(byScale ?? {}).flat();
3352
+ const variableMap = parseInternalWire(artifacts.variableMapJson, "variableMapJson (the theme's token variables)");
3353
+ const managed = createPropertyNames(declared, prefix, [...Object.values(variableMap).map((name) => name.replace(/^--/, "")), ...[...artifacts.variableCss.matchAll(THEME_DEFINITION)].map((m) => m[1])]);
3354
+ const nameConflicts = [...[...new Set(declared)].flatMap((name) => {
3355
+ const final = managed.finalName(name) ?? name;
3356
+ return final.startsWith(TRANSPORT_NAMESPACE) ? [{
3357
+ name,
3358
+ final,
3359
+ reason: "transport"
3360
+ }] : [];
3361
+ }), ...managed.ambiguous().map(({ name, contextual }) => ({
3362
+ name,
3363
+ final: contextual,
3364
+ reason: "theme-variable"
3365
+ }))];
3366
+ const rename = (value) => renameCustomProperties(value, managed);
2662
3367
  return {
2663
- css: css.replace(PROPERTY_RULE, (rule, name) => {
2664
- const reason = registrationFailure(rule);
2665
- if (reason === null) return rule;
2666
- invalid.push({
2667
- name,
2668
- reason
2669
- });
2670
- return "";
2671
- }),
2672
- invalid
3368
+ variableMapJson: JSON.stringify(Object.fromEntries(Object.entries(variableMap).map(([key, name]) => [key, rename(name)]))),
3369
+ variableCss: rename(artifacts.variableCss),
3370
+ themeJson: renameTokenValues(artifacts.themeJson, rename),
3371
+ contextualVarsJson: byScale === null ? null : JSON.stringify(Object.fromEntries(Object.entries(byScale).map(([scale, names]) => [scale, names.map((name) => ({
3372
+ name,
3373
+ var: managed.finalName(name) ?? name
3374
+ }))]))),
3375
+ declarationScalesJson: artifacts.declarationScalesJson === null ? null : renameDeclarationRecords(artifacts.declarationScalesJson, rename),
3376
+ nameConflicts,
3377
+ contextualProperties: [...new Set(declared)].map((name) => `--${managed.finalName(name) ?? name}`)
2673
3378
  };
2674
3379
  }
2675
- function registrationFailure(rule) {
2676
- const syntax = /syntax:\s*"([^"]*)"/.exec(rule)?.[1]?.trim() ?? "";
2677
- const initialValue = /initial-value\s*:\s*([^;}]*)/.exec(rule)?.[1]?.trim();
2678
- if (syntax !== "*") {
2679
- const components = syntax.split("|").map((component) => ({
2680
- text: component.trim(),
2681
- match: SYNTAX_COMPONENT.exec(component.trim())
2682
- }));
2683
- const unknown = components.find(({ match }) => match === null || match[1] !== void 0 && !SYNTAX_TYPES.has(match[1]));
2684
- if (unknown !== void 0) return `syntax "${syntax}" has the unknown component "${unknown.text}"`;
2685
- if (initialValue === void 0) return `syntax "${syntax}" needs an initialValue`;
2686
- const dimensional = components.some(({ match }) => match?.[1] !== void 0 && DIMENSION_TYPES.has(match[1]));
2687
- const value = initialValue.replace(LITERAL_TEXT, "");
2688
- 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`;
2689
- }
2690
- try {
2691
- (0, lightningcss.transform)({
2692
- filename: "animus-registrations.css",
2693
- code: Buffer.from(rule)
2694
- });
2695
- return null;
2696
- } catch (error) {
2697
- const detail = error instanceof Error ? error.message : String(error);
2698
- return `the CSS parser rejects \`${rule.trim()}\` (${detail})`;
2699
- }
3380
+ /** A custom property the theme's CSS defines or registers. */
3381
+ const THEME_DEFINITION = /(?:@property\s+|(?:^|[\s;{]))--([\w-]+)(?=\s*[:{])/g;
3382
+ /** Renames inside the token map's values. */
3383
+ function renameTokenValues(themeJson, rename) {
3384
+ const tokens = parseInternalWire(themeJson, "themeJson (the theme's flattened token values)");
3385
+ return JSON.stringify(Object.fromEntries(Object.entries(tokens).map(([path, value]) => [path, rename(value)])));
3386
+ }
3387
+ /** Renames inside every declaration-scale record value. */
3388
+ function renameDeclarationRecords(declarationScalesJson, rename) {
3389
+ const scales = parseInternalWire(declarationScalesJson, "declarationScalesJson (the theme's declaration scales)");
3390
+ return JSON.stringify(Object.fromEntries(Object.entries(scales).map(([name, scale]) => [name, {
3391
+ ...scale,
3392
+ values: Object.fromEntries(Object.entries(scale.values).map(([key, record]) => [key, Object.fromEntries(Object.entries(record).map(([property, value]) => [property, rename(value)]))]))
3393
+ }])));
2700
3394
  }
2701
3395
  //#endregion
2702
3396
  //#region pipeline/system-config.ts
@@ -2713,7 +3407,28 @@ function loadSystemConfig(engineApi, opts) {
2713
3407
  let variableCss = registrations.css;
2714
3408
  let contextualVarsJson = config.contextualVarsJson || null;
2715
3409
  let declarationScalesJson = config.declarationScalesJson || null;
2716
- if (opts.prefix) {
3410
+ const declaredNames = Object.values(parseInternalWire(contextualVarsJson ?? "{}", "contextualVarsJson (the theme's contextual variable names)")).flat();
3411
+ let contextualProperties = declaredNames.map((name) => `--${name}`);
3412
+ let legacyPrefixedContextualVars = [];
3413
+ let prefixNameConflicts = [];
3414
+ if (opts.prefix && opts.prefixContextualVars) {
3415
+ const resolved = applyPropertyNames(opts.prefix, {
3416
+ variableMapJson,
3417
+ variableCss,
3418
+ themeJson: scalesJson,
3419
+ contextualVarsJson,
3420
+ declarationScalesJson
3421
+ });
3422
+ variableMapJson = resolved.variableMapJson;
3423
+ variableCss = resolved.variableCss;
3424
+ scalesJson = resolved.themeJson;
3425
+ contextualVarsJson = resolved.contextualVarsJson;
3426
+ contextualProperties = resolved.contextualProperties;
3427
+ prefixNameConflicts = resolved.nameConflicts;
3428
+ declarationScalesJson = resolved.declarationScalesJson;
3429
+ } else if (opts.prefix) {
3430
+ legacyPrefixedContextualVars = declaredNames;
3431
+ contextualProperties = declaredNames.map((name) => `--${opts.prefix}-${name}`);
2717
3432
  const prefixed = applyPrefix(opts.prefix, variableMapJson, variableCss, scalesJson, contextualVarsJson || void 0);
2718
3433
  variableMapJson = prefixed.variableMapJson;
2719
3434
  variableCss = prefixed.variableCss;
@@ -2728,6 +3443,7 @@ function loadSystemConfig(engineApi, opts) {
2728
3443
  variableMapJson,
2729
3444
  variableCss,
2730
3445
  contextualVarsJson,
3446
+ contextualProperties: [...new Set(contextualProperties)],
2731
3447
  selectorAliasesJson: config.selectorAliases || null,
2732
3448
  conditionAliasesJson: config.conditionAliases || null,
2733
3449
  transformSourcesJson: config.transformSources || null,
@@ -2739,7 +3455,10 @@ function loadSystemConfig(engineApi, opts) {
2739
3455
  sourceThemeManifestsJson: config.sourceThemeManifests || null
2740
3456
  };
2741
3457
  if (declarationScalesJson) system.declarationScalesJson = declarationScalesJson;
3458
+ if (config.propertyRecordsJson) system.propertyRecordsJson = config.propertyRecordsJson;
2742
3459
  if (registrations.invalid.length > 0) system.invalidPropertyRegistrations = registrations.invalid;
3460
+ if (prefixNameConflicts.length > 0) system.prefixNameConflicts = prefixNameConflicts;
3461
+ if (legacyPrefixedContextualVars.length > 0) system.legacyPrefixedContextualVars = [...new Set(legacyPrefixedContextualVars)];
2743
3462
  return system;
2744
3463
  }
2745
3464
  //#endregion
@@ -2793,6 +3512,7 @@ function buildDynamicPropConfig(dynamicProps) {
2793
3512
  if (meta.negative) entry.negative = true;
2794
3513
  if (meta.strict) entry.strict = true;
2795
3514
  if (meta.keywords?.length) entry.keywords = meta.keywords;
3515
+ if (meta.currentVar) entry.currentVar = meta.currentVar;
2796
3516
  configEntries[propName] = entry;
2797
3517
  }
2798
3518
  return configEntries;
@@ -2823,31 +3543,6 @@ function buildSystemPropsModule(opts) {
2823
3543
  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` : "");
2824
3544
  }
2825
3545
  //#endregion
2826
- //#region pipeline/timing-waterfall.ts
2827
- const RUST_TIMING_PHASES = [
2828
- ["parseAndWalk", "parse+walk"],
2829
- ["importResolution", "imports"],
2830
- ["extensionProvenance", "provenance"],
2831
- ["topologicalSort", "topo-sort"],
2832
- ["chainEvaluation", "chains"],
2833
- ["jsxScanning", "jsx-scan"],
2834
- ["systemPropAggregation", "sys-props"],
2835
- ["usageLedger", "usage"],
2836
- ["reconciliation", "reconcile"],
2837
- ["cssGeneration", "css-gen"],
2838
- ["manifestSerialization", "serialize"]
2839
- ];
2840
- function formatRustTimingWaterfall(timing, opts) {
2841
- const lines = [];
2842
- for (const [key, label] of RUST_TIMING_PHASES) {
2843
- const ms = timing[key] ?? 0;
2844
- const pad = " ".repeat(Math.max(0, opts.labelWidth - label.length));
2845
- const extra = key === "parseAndWalk" ? ` (${timing.fileCount ?? 0} files, ${timing.cacheHits ?? 0} cached)` : "";
2846
- lines.push(`${opts.indent}${label}${pad}${String(ms).padStart(5)}ms${extra}`);
2847
- }
2848
- return lines;
2849
- }
2850
- //#endregion
2851
3546
  //#region pipeline/tsconfig-paths.ts
2852
3547
  /**
2853
3548
  * tsconfig `paths` → alias pairs, for drivers with no live bundler config.
@@ -3360,16 +4055,22 @@ Object.defineProperty(exports, "findPackageRoot", {
3360
4055
  return findPackageRoot;
3361
4056
  }
3362
4057
  });
4058
+ Object.defineProperty(exports, "findSheetAssetSpecifiers", {
4059
+ enumerable: true,
4060
+ get: function() {
4061
+ return findSheetAssetSpecifiers;
4062
+ }
4063
+ });
3363
4064
  Object.defineProperty(exports, "firstOwners", {
3364
4065
  enumerable: true,
3365
4066
  get: function() {
3366
4067
  return firstOwners;
3367
4068
  }
3368
4069
  });
3369
- Object.defineProperty(exports, "formatRustTimingWaterfall", {
4070
+ Object.defineProperty(exports, "generatedModuleCode", {
3370
4071
  enumerable: true,
3371
4072
  get: function() {
3372
- return formatRustTimingWaterfall;
4073
+ return generatedModuleCode;
3373
4074
  }
3374
4075
  });
3375
4076
  Object.defineProperty(exports, "hashReplacementPlans", {
@@ -3390,6 +4091,12 @@ Object.defineProperty(exports, "isAdvisorySourceDiagnostic", {
3390
4091
  return isAdvisorySourceDiagnostic;
3391
4092
  }
3392
4093
  });
4094
+ Object.defineProperty(exports, "isDeletedSource", {
4095
+ enumerable: true,
4096
+ get: function() {
4097
+ return isDeletedSource;
4098
+ }
4099
+ });
3393
4100
  Object.defineProperty(exports, "isEngineTransformExtension", {
3394
4101
  enumerable: true,
3395
4102
  get: function() {
@@ -3450,6 +4157,12 @@ Object.defineProperty(exports, "readTsconfigAliasPairs", {
3450
4157
  return readTsconfigAliasPairs;
3451
4158
  }
3452
4159
  });
4160
+ Object.defineProperty(exports, "reportSurvivingAssetPlaceholders", {
4161
+ enumerable: true,
4162
+ get: function() {
4163
+ return reportSurvivingAssetPlaceholders;
4164
+ }
4165
+ });
3453
4166
  Object.defineProperty(exports, "resolveAbsolutePathSpecifier", {
3454
4167
  enumerable: true,
3455
4168
  get: function() {
@@ -3540,6 +4253,12 @@ Object.defineProperty(exports, "substituteAssetPlaceholders", {
3540
4253
  return substituteAssetPlaceholders;
3541
4254
  }
3542
4255
  });
4256
+ Object.defineProperty(exports, "substituteSheetAssets", {
4257
+ enumerable: true,
4258
+ get: function() {
4259
+ return substituteSheetAssets;
4260
+ }
4261
+ });
3543
4262
  Object.defineProperty(exports, "surfaceManifestDiagnostics", {
3544
4263
  enumerable: true,
3545
4264
  get: function() {