@animus-ui/extract 0.1.19 → 0.1.21

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 (57) hide show
  1. package/crates/extract-v2/animus-extract-v2.darwin-arm64.node +0 -0
  2. package/crates/extract-v2/animus-extract-v2.linux-arm64-gnu.node +0 -0
  3. package/crates/extract-v2/animus-extract-v2.linux-x64-gnu.node +0 -0
  4. package/crates/extract-v2/index.d.ts +13 -0
  5. package/dist/index.cjs +5 -1
  6. package/dist/index.mjs +2 -2
  7. package/dist/pipeline/analyze-project-args.d.ts +5 -1
  8. package/dist/pipeline/analyze-project-args.d.ts.map +1 -1
  9. package/dist/pipeline/assemble-stylesheet.d.ts +5 -0
  10. package/dist/pipeline/assemble-stylesheet.d.ts.map +1 -1
  11. package/dist/pipeline/asset-placeholders.d.ts +14 -0
  12. package/dist/pipeline/asset-placeholders.d.ts.map +1 -1
  13. package/dist/pipeline/core-options.d.ts +6 -0
  14. package/dist/pipeline/core-options.d.ts.map +1 -1
  15. package/dist/pipeline/discover-packages.d.ts +15 -1
  16. package/dist/pipeline/discover-packages.d.ts.map +1 -1
  17. package/dist/pipeline/dynamic-prop-config.d.ts +3 -0
  18. package/dist/pipeline/dynamic-prop-config.d.ts.map +1 -1
  19. package/dist/pipeline/engine-adapter.d.ts +1 -1
  20. package/dist/pipeline/engine-adapter.d.ts.map +1 -1
  21. package/dist/pipeline/index.d.ts +4 -4
  22. package/dist/pipeline/index.d.ts.map +1 -1
  23. package/dist/pipeline/manifest-diagnostics.d.ts +33 -3
  24. package/dist/pipeline/manifest-diagnostics.d.ts.map +1 -1
  25. package/dist/pipeline/manifest-schema.d.ts +6 -0
  26. package/dist/pipeline/manifest-schema.d.ts.map +1 -1
  27. package/dist/pipeline/post-process-css.d.ts.map +1 -1
  28. package/dist/pipeline/prefix.d.ts +35 -0
  29. package/dist/pipeline/prefix.d.ts.map +1 -1
  30. package/dist/pipeline/property-diagnostics.d.ts +6 -4
  31. package/dist/pipeline/property-diagnostics.d.ts.map +1 -1
  32. package/dist/pipeline/property-names.d.ts +32 -0
  33. package/dist/pipeline/property-names.d.ts.map +1 -0
  34. package/dist/pipeline/property-registrations.d.ts +5 -2
  35. package/dist/pipeline/property-registrations.d.ts.map +1 -1
  36. package/dist/pipeline/run-analysis.d.ts +16 -0
  37. package/dist/pipeline/run-analysis.d.ts.map +1 -1
  38. package/dist/pipeline/source-ingestion.d.ts +3 -0
  39. package/dist/pipeline/source-ingestion.d.ts.map +1 -1
  40. package/dist/pipeline/structural-self-check.d.ts +3 -0
  41. package/dist/pipeline/structural-self-check.d.ts.map +1 -1
  42. package/dist/pipeline/system-config.d.ts +9 -0
  43. package/dist/pipeline/system-config.d.ts.map +1 -1
  44. package/dist/pipeline/tsconfig-paths.d.ts +14 -0
  45. package/dist/pipeline/tsconfig-paths.d.ts.map +1 -1
  46. package/dist/{pipeline-BuiItKT-.mjs → pipeline-BXY7Pf_D.mjs} +1322 -567
  47. package/dist/{pipeline-CD2qmEEn.cjs → pipeline-D7B1z0-1.cjs} +1363 -566
  48. package/dist/session/extraction-session.d.ts +21 -1
  49. package/dist/session/extraction-session.d.ts.map +1 -1
  50. package/dist/session/index.d.ts +2 -2
  51. package/dist/session/index.d.ts.map +1 -1
  52. package/dist/session/resolve-packages.d.ts.map +1 -1
  53. package/dist/session/singleton.d.ts +5 -1
  54. package/dist/session/singleton.d.ts.map +1 -1
  55. package/dist/session.cjs +131 -11
  56. package/dist/session.mjs +132 -13
  57. package/package.json +4 -4
@@ -2,16 +2,21 @@ import { createRequire } from "node:module";
2
2
  import path, { basename, dirname, join, normalize, posix } from "node:path";
3
3
  import { createHash } from "crypto";
4
4
  import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "fs";
5
- import { dirname as dirname$1, extname, isAbsolute, join as join$1, relative, resolve } from "path";
5
+ import { dirname as dirname$1, extname, isAbsolute, join as join$1, relative, resolve, sep } from "path";
6
6
  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 { componentValues, dependsOnContext, foldInitialValue, identifierAt, isSpace, isUnitlessProperty, someValue, substitutesValue, tokenize, variableReads } from "@animus-ui/properties";
11
11
  //#region \0rolldown/runtime.js
12
12
  var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
13
13
  //#endregion
14
14
  //#region pipeline/assemble-stylesheet.ts
15
+ /**
16
+ * The cascade layer order, outermost first: its one definition. The engine
17
+ * reads it from `crates/extract-v2/src/layer_order.json`, which
18
+ * `scripts/layer-order.ts --write` writes from here.
19
+ */
15
20
  const ANIMUS_LAYERS = [
16
21
  "anm-global",
17
22
  "anm-base",
@@ -85,7 +90,8 @@ function buildAnalyzeProjectArgs(inputs) {
85
90
  inputs.transformSourcesJson,
86
91
  inputs.transformProvenanceJson ?? null
87
92
  ];
88
- if (inputs.declarationScalesJson) args.push(inputs.declarationScalesJson);
93
+ if (inputs.declarationScalesJson || inputs.analysisContextJson) args.push(inputs.declarationScalesJson);
94
+ if (inputs.analysisContextJson) args.push(inputs.analysisContextJson);
89
95
  return args;
90
96
  }
91
97
  //#endregion
@@ -105,6 +111,282 @@ function parseInternalWire(json, wire) {
105
111
  return parsed;
106
112
  }
107
113
  //#endregion
114
+ //#region pipeline/manifest-diagnostics.ts
115
+ /** Stable code for selector forms with no substitutable subject; the
116
+ * extraction engine mints the same string. */
117
+ const SELECTOR_UNSUPPORTED_SUBJECT = "animus.selector.unsupported-subject";
118
+ /** The one matcher for the engine's unresolved-parent chain-drop message;
119
+ * consumers match through the exported helpers, never their own copy. */
120
+ const UNRESOLVED_PARENT_RE = /chain dropped: could not resolve parent component '([^']+)'/;
121
+ function isUnresolvedParentDrop(diagnostic) {
122
+ return UNRESOLVED_PARENT_RE.test(diagnostic.message);
123
+ }
124
+ function unresolvedParentName(diagnostic) {
125
+ return UNRESOLVED_PARENT_RE.exec(diagnostic.message)?.[1] ?? null;
126
+ }
127
+ /** True when the selector carries at least one substitutable `&` subject
128
+ * outside quoted text. */
129
+ function hasSelectorSubject(value) {
130
+ let quote = null;
131
+ let escaped = false;
132
+ for (const c of value) {
133
+ if (escaped) {
134
+ escaped = false;
135
+ continue;
136
+ }
137
+ if (c === "\\") {
138
+ escaped = true;
139
+ continue;
140
+ }
141
+ if (quote !== null) {
142
+ if (c === quote) quote = null;
143
+ } else if (c === "\"" || c === "'") quote = c;
144
+ else if (c === "&") return true;
145
+ }
146
+ return false;
147
+ }
148
+ /**
149
+ * Coded diagnostics for selector-shaped alias values with no substitutable
150
+ * subject; alias values never reach the evaluator's own key guard.
151
+ */
152
+ function collectSelectorAliasDiagnostics(selectorAliasesJson) {
153
+ if (!selectorAliasesJson) return [];
154
+ const aliases = parseInternalWire(selectorAliasesJson, "selectorAliasesJson (the system loader's selector-alias registry)");
155
+ const diagnostics = [];
156
+ for (const [name, value] of Object.entries(aliases)) if (value.includes("&") && !hasSelectorSubject(value)) diagnostics.push({
157
+ file: "system",
158
+ component: name,
159
+ kind: "warn",
160
+ message: `selector alias '${name}' value '${value}' has no substitutable '&' subject outside quoted text (${SELECTOR_UNSUPPORTED_SUBJECT})`,
161
+ code: SELECTOR_UNSUPPORTED_SUBJECT,
162
+ severity: severityFor(SELECTOR_UNSUPPORTED_SUBJECT)
163
+ });
164
+ return diagnostics;
165
+ }
166
+ const UNREADABLE_SOURCE_FILE = "animus.ingestion.unreadable-source-file";
167
+ /** A kit the system extends resolved, yet discovery found none of its files. */
168
+ const NO_KIT_FILES = "animus.discovery.no-kit-files";
169
+ /** What a skipped source file costs: the analysis never sees what it
170
+ * renders. */
171
+ 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";
172
+ /**
173
+ * A configured source file that could not be read is a LOST INPUT: the
174
+ * emitted artifacts silently lack whatever that file declared.
175
+ */
176
+ /** Whether a failed source read means the file was deleted after discovery
177
+ * (an editor's delete-and-recreate, a branch switch): a deletion, not lost
178
+ * input. */
179
+ function isDeletedSource(error) {
180
+ return error?.code === "ENOENT";
181
+ }
182
+ function unreadableSourceDiagnostic(relPath, error) {
183
+ return {
184
+ file: relPath,
185
+ component: "source",
186
+ kind: "warn",
187
+ message: `configured source file ${relPath} could not be read: ${String(error)} (${SKIPPED_SOURCE_COST})`,
188
+ code: UNREADABLE_SOURCE_FILE,
189
+ severity: severityFor(UNREADABLE_SOURCE_FILE)
190
+ };
191
+ }
192
+ /** One warning per kit the system extends that resolved, yet yielded no
193
+ * files: none of its components are extracted. */
194
+ function noKitFilesDiagnostics(outcomes) {
195
+ return outcomes.filter((record) => record.outcome === "empty").map((record) => ({
196
+ file: record.specifier,
197
+ component: "kit",
198
+ kind: "warn",
199
+ message: "resolved, but discovery found none of its files, so none of its components are extracted — check that the package ships its src/ directory or a readable entry",
200
+ code: NO_KIT_FILES,
201
+ severity: severityFor(NO_KIT_FILES)
202
+ }));
203
+ }
204
+ /** The collision entry code minted by the system package's merge. */
205
+ const VOCABULARY_COLLISION = "animus.vocabulary.collision";
206
+ /** A theme `@property` registration browsers would ignore; it is not emitted. */
207
+ const INVALID_PROPERTY_REGISTRATION = "animus.theme.invalid-property-registration";
208
+ /** Minted when a sealed kit with registered vocabulary is consumed through
209
+ * the deprecated `from()`/`includes:` verbs. */
210
+ const VOCABULARY_LEGACY_VERB = "animus.vocabulary.legacy-verb";
211
+ /** A `var()` fallback that never applies: the property is registered with an
212
+ * initial value. */
213
+ const PROPERTY_FALLBACK_SUPPRESSED = "animus.property.fallback-suppressed";
214
+ /** The same, where the fallback heads a chain of further `var()` reads. */
215
+ const PROPERTY_FALLBACK_CHAIN_SUPPRESSED = "animus.property.fallback-chain-suppressed";
216
+ /** A declared property set in keyframes or transitioned without a typed
217
+ * registration, so it does not interpolate. */
218
+ const PROPERTY_UNREGISTERED_ANIMATION = "animus.property.unregistered-animation";
219
+ /** A custom property whose resolved value is `var()` of itself, which makes
220
+ * it invalid at computed-value time. */
221
+ const PROPERTY_SELF_REFERENCE = "animus.property.self-reference";
222
+ /** A custom property that reads itself only inside another `var()`'s
223
+ * fallback: cyclic wherever that fallback is used. */
224
+ const PROPERTY_FALLBACK_SELF_REFERENCE = "animus.property.fallback-self-reference";
225
+ /** Custom properties that `currentVar` writes in one rule, each reading
226
+ * another directly, so every property in the cycle is invalid at
227
+ * computed-value time. */
228
+ const PROPERTY_CURRENT_VAR_CYCLE = "animus.property.current-var-cycle";
229
+ /** A prefix renamed contextual variables without `prefixContextualVars`, so
230
+ * their declared names no longer resolve. */
231
+ const PREFIX_CONTEXTUAL_VARS_UNPREFIXED = "animus.prefix.contextual-vars-unprefixed";
232
+ /** Under `prefixContextualVars`, a final name that collides with a runtime
233
+ * transport variable or a theme variable of the same spelling. */
234
+ const PREFIX_NAME_CONFLICT = "animus.prefix.name-conflict";
235
+ /** A contextual variable declared on a scale that also holds a token of its
236
+ * name: scale reads resolve to the token. */
237
+ const PROPERTY_LEGACY_TOKEN_COLLISION = "animus.property.legacy-token-collision";
238
+ /** A theme property declared on a scale that also holds a token of its
239
+ * name. */
240
+ const PROPERTY_TOKEN_COLLISION = "animus.property.token-collision";
241
+ const DIAGNOSTIC_SEVERITY = /* @__PURE__ */ new Map([
242
+ [SELECTOR_UNSUPPORTED_SUBJECT, "error"],
243
+ [UNREADABLE_SOURCE_FILE, "error"],
244
+ [NO_KIT_FILES, "warn"],
245
+ [VOCABULARY_COLLISION, "warn"],
246
+ [VOCABULARY_LEGACY_VERB, "warn"],
247
+ [INVALID_PROPERTY_REGISTRATION, "error"],
248
+ [PROPERTY_FALLBACK_SUPPRESSED, "info"],
249
+ [PROPERTY_FALLBACK_CHAIN_SUPPRESSED, "warn"],
250
+ [PROPERTY_UNREGISTERED_ANIMATION, "info"],
251
+ [PROPERTY_SELF_REFERENCE, "warn"],
252
+ [PROPERTY_FALLBACK_SELF_REFERENCE, "warn"],
253
+ [PROPERTY_CURRENT_VAR_CYCLE, "warn"],
254
+ [PREFIX_CONTEXTUAL_VARS_UNPREFIXED, "warn"],
255
+ [PREFIX_NAME_CONFLICT, "error"],
256
+ [PROPERTY_LEGACY_TOKEN_COLLISION, "warn"],
257
+ [PROPERTY_TOKEN_COLLISION, "error"]
258
+ ]);
259
+ /** An unlisted code is `warn`: a witness kind from a newer system package
260
+ * must not fail the strict build of a host that predates its writer. */
261
+ function severityFor(code) {
262
+ return (code === void 0 ? void 0 : DIAGNOSTIC_SEVERITY.get(code)) ?? "warn";
263
+ }
264
+ /**
265
+ * The one mapper of a sealed system's witness entries for every host: the
266
+ * loader's evaluation host shims `console`, so the record is the channel.
267
+ */
268
+ function vocabularyWitnessDiagnostics(vocabularyWitnessesJson) {
269
+ if (!vocabularyWitnessesJson) return [];
270
+ const entries = parseInternalWire(vocabularyWitnessesJson, "vocabularyWitnessesJson (the sealed system's vocabulary witness record)");
271
+ const diagnostics = [];
272
+ for (const entry of entries) if (entry.code === "animus.vocabulary.collision") diagnostics.push({
273
+ file: "system",
274
+ component: entry.name ?? "keyframes",
275
+ kind: "warn",
276
+ message: `keyframes vocabulary "${entry.name}" is registered by both ${entry.loser} and ${entry.winner} — ${entry.winner} wins; rename one collection (${entry.code})`,
277
+ code: entry.code,
278
+ severity: severityFor(entry.code)
279
+ });
280
+ else if (entry.code === "animus.vocabulary.legacy-verb") diagnostics.push({
281
+ file: "system",
282
+ component: "keyframes",
283
+ kind: "warn",
284
+ 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})`,
285
+ code: entry.code,
286
+ severity: severityFor(entry.code)
287
+ });
288
+ else diagnostics.push({
289
+ file: "system",
290
+ component: "vocabulary",
291
+ kind: "warn",
292
+ 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})` : ""}`,
293
+ code: entry.code,
294
+ severity: severityFor(entry.code)
295
+ });
296
+ return diagnostics;
297
+ }
298
+ /** The diagnostics a loaded system's own records carry: vocabulary
299
+ * witnesses and invalid `@property` registrations. */
300
+ function systemLoadDiagnostics(system) {
301
+ const registrations = (system.invalidPropertyRegistrations ?? []).map(({ name, reason }) => ({
302
+ file: "system",
303
+ component: name,
304
+ kind: "warn",
305
+ 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})`,
306
+ code: INVALID_PROPERTY_REGISTRATION,
307
+ severity: severityFor(INVALID_PROPERTY_REGISTRATION)
308
+ }));
309
+ const prefixed = system.legacyPrefixedContextualVars ?? [];
310
+ const unprefixed = prefixed.length === 0 ? [] : [{
311
+ file: "system",
312
+ component: "prefix",
313
+ kind: "warn",
314
+ 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})`,
315
+ code: PREFIX_CONTEXTUAL_VARS_UNPREFIXED,
316
+ severity: severityFor(PREFIX_CONTEXTUAL_VARS_UNPREFIXED)
317
+ }];
318
+ const conflicts = (system.prefixNameConflicts ?? []).map(({ name, final, reason }) => ({
319
+ file: "system",
320
+ component: `--${name}`,
321
+ kind: "error",
322
+ 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})`,
323
+ code: PREFIX_NAME_CONFLICT,
324
+ severity: severityFor(PREFIX_NAME_CONFLICT)
325
+ }));
326
+ return [
327
+ ...vocabularyWitnessDiagnostics(system.vocabularyWitnessesJson),
328
+ ...registrations,
329
+ ...unprefixed,
330
+ ...conflicts,
331
+ ...tokenCollisionDiagnostics(system)
332
+ ];
333
+ }
334
+ /**
335
+ * Each declared property whose name is also a token on a scale it is
336
+ * declared on. A scale read of that key, a prop value or a `{scale.key}`
337
+ * reference, resolves to the token, so the property is unreachable through
338
+ * the scale. A legacy declaration warns and the token stays the winner; any
339
+ * other is an error in every mode.
340
+ */
341
+ function tokenCollisionDiagnostics(system) {
342
+ if (!system.propertyRecordsJson) return [];
343
+ const declared = parseInternalWire(system.propertyRecordsJson, "propertyRecordsJson (the theme's properties)").flatMap((record) => record.scales.map((scale) => ({
344
+ record,
345
+ scale,
346
+ path: `${scale}.${record.name}`
347
+ })));
348
+ if (declared.length === 0) return [];
349
+ const tokens = parseInternalWire(system.scalesJson, "scalesJson (the theme's scale tokens)");
350
+ return declared.filter(({ path }) => Object.hasOwn(tokens, path)).map(({ record, scale, path }) => {
351
+ const code = record.legacy ? PROPERTY_LEGACY_TOKEN_COLLISION : PROPERTY_TOKEN_COLLISION;
352
+ const declaration = record.legacy ? `the contextual variable ${record.name}, which declareContextualVars declares on ${scale},` : `the theme property ${record.name}, declared on ${scale},`;
353
+ return {
354
+ file: "system",
355
+ component: path,
356
+ kind: record.legacy ? "warn" : "error",
357
+ message: `the ${scale} token ${record.name} (${tokens[path]}) and ${declaration} share this key. A scale read of it, such as {${path}} or a prop on the ${scale} scale set to '${record.name}', resolves to the token, so the ${record.legacy ? "contextual variable" : "property"} is unreachable through ${scale}. Rename one of them (${code})`,
358
+ code,
359
+ severity: severityFor(code)
360
+ };
361
+ });
362
+ }
363
+ /**
364
+ * The single strict-escalation policy point for both extraction plugins:
365
+ * error-severity diagnostics throw together under `strict`, else warn.
366
+ */
367
+ function surfaceManifestDiagnostics(manifest, warn, policy = {}) {
368
+ const errors = [];
369
+ const diagnostics = policy.prepend?.length ? [...policy.prepend, ...manifest.diagnostics ?? []] : manifest.diagnostics ?? [];
370
+ for (const diagnostic of diagnostics) {
371
+ let line = null;
372
+ if (diagnostic.kind === "bail") line = `⚠ ${diagnostic.component} not extracted: ${diagnostic.message}`;
373
+ else if (diagnostic.kind === "skip") line = `⚠ ${diagnostic.component}: skipped ${diagnostic.message}`;
374
+ else if (diagnostic.kind === "warn") line = `${diagnostic.severity === "info" ? "ℹ" : "⚠"} ${diagnostic.file}: ${diagnostic.component}: ${diagnostic.message}`;
375
+ if (line === null) continue;
376
+ if (diagnostic.code && !diagnostic.message.includes(diagnostic.code)) line += ` [${diagnostic.code}]`;
377
+ if (diagnostic.severity === "info") {
378
+ policy.info?.(line);
379
+ continue;
380
+ }
381
+ if (policy.strict && diagnostic.severity === "error") {
382
+ errors.push(`${diagnostic.code ?? "error"} — ${diagnostic.component}: ${diagnostic.message}`);
383
+ continue;
384
+ }
385
+ warn(line);
386
+ }
387
+ if (errors.length > 0) throw new Error(`[animus] strict: ${errors.length} error diagnostic(s):\n${errors.join("\n")}`);
388
+ }
389
+ //#endregion
108
390
  //#region pipeline/mdx-preprocessor.ts
109
391
  /**
110
392
  * MDX compiled to scanner-consumable JSX, plus the extension sets every
@@ -1029,7 +1311,8 @@ function withoutInvalidOriginals(result, invalidOriginals) {
1029
1311
  ...result,
1030
1312
  originalEntries: result.originalEntries.filter((entry) => !invalidOriginals.has(entry.path)),
1031
1313
  analysisEntries: result.analysisEntries.filter((entry) => analysisPaths.has(entry.path)),
1032
- ownership
1314
+ ownership,
1315
+ skippedOriginals: [...invalidOriginals].sort()
1033
1316
  };
1034
1317
  }
1035
1318
  /**
@@ -1069,11 +1352,12 @@ function createSourceIngestor(host) {
1069
1352
  const invalidOriginals = new Set(fatal.map((diagnostic) => diagnostic.originalPath));
1070
1353
  if (diagnostics.length === 0) return invalidOriginals;
1071
1354
  if (host.strict() && fatal.length > 0) {
1072
- const lines = fatal.map((diagnostic) => `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}`);
1355
+ const lines = fatal.map((diagnostic) => `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message} (${SKIPPED_SOURCE_COST})`);
1073
1356
  throw new Error(`${host.prefix} ${lines.join(`\n${host.prefix} `)}`);
1074
1357
  }
1075
1358
  for (const diagnostic of diagnostics) {
1076
- const line = `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}`;
1359
+ const skipped = isAdvisorySourceDiagnostic(diagnostic) ? "" : ` (${SKIPPED_SOURCE_COST})`;
1360
+ const line = `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}${skipped}`;
1077
1361
  let warned = warnedByOriginal.get(diagnostic.originalPath);
1078
1362
  if (!warned) {
1079
1363
  warned = /* @__PURE__ */ new Set();
@@ -1108,7 +1392,7 @@ function createV2EngineApi(deps) {
1108
1392
  return {
1109
1393
  loadSystemModule: (...args) => native.loadSystemModule(...args),
1110
1394
  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) => {
1395
+ analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson, transformSourcesJson, transformProvenanceJson, declarationScalesJson, analysisContextJson) => {
1112
1396
  const filesJson = deps.rehydrateFilesJson ? deps.rehydrateFilesJson(filesJsonRaw) : filesJsonRaw;
1113
1397
  const sent = /* @__PURE__ */ new Map();
1114
1398
  for (const entry of parseFilesJson(filesJson, label)) sent.set(entry.path, entry.source);
@@ -1135,6 +1419,7 @@ function createV2EngineApi(deps) {
1135
1419
  transformSourcesJson: transformSourcesJson ?? void 0,
1136
1420
  transformProvenanceJson: transformProvenanceJson ?? void 0,
1137
1421
  declarationScalesJson,
1422
+ analysisContextJson,
1138
1423
  devMode
1139
1424
  };
1140
1425
  const engine = new native.ExtractEngine(config);
@@ -1207,6 +1492,7 @@ const CORE_OPTION_KEYS = /* @__PURE__ */ new Set([
1207
1492
  "strict",
1208
1493
  "verbose",
1209
1494
  "prefix",
1495
+ "prefixContextualVars",
1210
1496
  "targets",
1211
1497
  "minify",
1212
1498
  "staticCss",
@@ -1300,6 +1586,11 @@ const CORE_VALUE_GATES = [
1300
1586
  ok: isString,
1301
1587
  expected: "a string"
1302
1588
  },
1589
+ {
1590
+ key: "prefixContextualVars",
1591
+ ok: isBoolean,
1592
+ expected: "a boolean"
1593
+ },
1303
1594
  {
1304
1595
  key: "targets",
1305
1596
  ok: isStringOrStringArray,
@@ -1561,13 +1852,195 @@ function createSourceIdentity(rootDir) {
1561
1852
  };
1562
1853
  }
1563
1854
  //#endregion
1564
- //#region pipeline/discover-packages.ts
1565
- function findPackageRoot(absEntryPath) {
1566
- let pkgRoot = dirname$1(absEntryPath);
1567
- while (pkgRoot !== dirname$1(pkgRoot) && !existsSync(join$1(pkgRoot, "package.json"))) pkgRoot = dirname$1(pkgRoot);
1568
- return pkgRoot;
1855
+ //#region pipeline/tsconfig-paths.ts
1856
+ /**
1857
+ * tsconfig `paths` → alias pairs, for drivers with no live bundler config.
1858
+ * The nearest config declaring `paths` wins wholesale — TS never merges.
1859
+ */
1860
+ function stripJsonc(text) {
1861
+ let out = "";
1862
+ let inString = false;
1863
+ let inLine = false;
1864
+ let inBlock = false;
1865
+ for (let i = 0; i < text.length; i++) {
1866
+ const ch = text[i];
1867
+ const next = text[i + 1];
1868
+ if (inLine) {
1869
+ if (ch === "\n") {
1870
+ inLine = false;
1871
+ out += ch;
1872
+ }
1873
+ continue;
1874
+ }
1875
+ if (inBlock) {
1876
+ if (ch === "*" && next === "/") {
1877
+ inBlock = false;
1878
+ i++;
1879
+ }
1880
+ continue;
1881
+ }
1882
+ if (inString) {
1883
+ out += ch;
1884
+ if (ch === "\\") {
1885
+ out += next ?? "";
1886
+ i++;
1887
+ } else if (ch === "\"") inString = false;
1888
+ continue;
1889
+ }
1890
+ if (ch === "\"") {
1891
+ inString = true;
1892
+ out += ch;
1893
+ continue;
1894
+ }
1895
+ if (ch === "/" && next === "/") {
1896
+ inLine = true;
1897
+ i++;
1898
+ continue;
1899
+ }
1900
+ if (ch === "/" && next === "*") {
1901
+ inBlock = true;
1902
+ i++;
1903
+ continue;
1904
+ }
1905
+ out += ch;
1906
+ }
1907
+ return out.replace(/,(\s*[}\]])/g, "$1");
1569
1908
  }
1570
- const PACKAGE_SRC_EXCLUDES = [
1909
+ /** `Object(value) === value` holds for exactly the blocks and lists
1910
+ * `JSON.parse` produces; the `[object Object]` tag separates the two. */
1911
+ function isJsonBlock(value) {
1912
+ return Object(value) === value && Object.prototype.toString.call(value) === "[object Object]";
1913
+ }
1914
+ /** A block or a list. A declared `paths` OWNS the setting and blocks its
1915
+ * parents' even when it contributes nothing usable. */
1916
+ function isJsonKeyed(value) {
1917
+ return Object(value) === value;
1918
+ }
1919
+ /** A JSON string, excluding the boxed `String` object, which no path join
1920
+ * accepts. */
1921
+ function isJsonString(value) {
1922
+ return Object(value) !== value && Object.prototype.toString.call(value) === "[object String]";
1923
+ }
1924
+ function decodePaths(value) {
1925
+ if (!isJsonKeyed(value)) return null;
1926
+ const decoded = /* @__PURE__ */ new Map();
1927
+ for (const [pattern, targets] of Object.entries(value)) {
1928
+ const [first] = Array.isArray(targets) ? targets : [];
1929
+ if (isJsonString(first)) decoded.set(pattern, first);
1930
+ }
1931
+ return decoded;
1932
+ }
1933
+ /** TypeScript accepts one specifier or an array; a non-string member names
1934
+ * no config and is dropped here. */
1935
+ function decodeExtends(value) {
1936
+ return (Array.isArray(value) ? value : [value]).filter(isJsonString);
1937
+ }
1938
+ function readConfig(path) {
1939
+ let raw;
1940
+ try {
1941
+ raw = readFileSync(path, "utf-8");
1942
+ } catch {
1943
+ return null;
1944
+ }
1945
+ let parsed;
1946
+ try {
1947
+ parsed = JSON.parse(stripJsonc(raw));
1948
+ } catch {
1949
+ return null;
1950
+ }
1951
+ const root = isJsonBlock(parsed) ? parsed : {};
1952
+ const compilerOptions = isJsonBlock(root.compilerOptions) ? root.compilerOptions : {};
1953
+ return {
1954
+ dir: dirname$1(path),
1955
+ baseUrl: isJsonString(compilerOptions.baseUrl) ? compilerOptions.baseUrl : null,
1956
+ paths: decodePaths(compilerOptions.paths),
1957
+ extends: decodeExtends(root.extends)
1958
+ };
1959
+ }
1960
+ function resolveExtendsTarget(specifier, fromDir) {
1961
+ if (specifier.startsWith(".") || isAbsolute(specifier)) {
1962
+ const base = resolve(fromDir, specifier);
1963
+ for (const candidate of [
1964
+ base,
1965
+ `${base}.json`,
1966
+ join$1(base, "tsconfig.json")
1967
+ ]) if (existsSync(candidate)) return candidate;
1968
+ return null;
1969
+ }
1970
+ try {
1971
+ return __require.resolve(specifier.endsWith(".json") ? specifier : `${specifier}/tsconfig.json`, { paths: [fromDir] });
1972
+ } catch {
1973
+ try {
1974
+ return __require.resolve(specifier, { paths: [fromDir] });
1975
+ } catch {
1976
+ return null;
1977
+ }
1978
+ }
1979
+ }
1980
+ /** Child-first flattened extends chain starting at `entryPath`. */
1981
+ function loadChain(entryPath) {
1982
+ const chain = [];
1983
+ const visited = /* @__PURE__ */ new Set();
1984
+ const queue = [entryPath];
1985
+ while (queue.length > 0) {
1986
+ const path = queue.shift();
1987
+ const key = resolve(path);
1988
+ if (visited.has(key)) continue;
1989
+ visited.add(key);
1990
+ const node = readConfig(key);
1991
+ if (!node) continue;
1992
+ chain.push(node);
1993
+ for (const parent of node.extends) {
1994
+ const resolved = resolveExtendsTarget(parent, node.dir);
1995
+ if (resolved) queue.push(resolved);
1996
+ }
1997
+ }
1998
+ return chain;
1999
+ }
2000
+ /**
2001
+ * The project's tsconfig path aliases as `buildPathAliasesJson` pairs; empty
2002
+ * when no readable tsconfig, or no usable `paths`, exists.
2003
+ */
2004
+ function readTsconfigAliasPairs(rootDir) {
2005
+ const chain = loadChain(join$1(rootDir, "tsconfig.json"));
2006
+ if (chain.length === 0) return [];
2007
+ const pathsOwner = chain.find((node) => node.paths !== null);
2008
+ const paths = pathsOwner?.paths ?? null;
2009
+ if (pathsOwner === void 0 || paths === null) return [];
2010
+ const baseOwner = chain.find((node) => node.baseUrl !== null);
2011
+ const baseUrl = baseOwner?.baseUrl ?? null;
2012
+ const base = baseOwner !== void 0 && baseUrl !== null ? resolve(baseOwner.dir, baseUrl) : pathsOwner.dir;
2013
+ const pairs = [];
2014
+ for (const [pattern, target] of paths) {
2015
+ const patternStars = pattern.split("*").length - 1;
2016
+ const targetStars = target.split("*").length - 1;
2017
+ if (pattern === "*" || patternStars > 1 || targetStars > 1) continue;
2018
+ if (pattern.endsWith("/*")) {
2019
+ if (!target.endsWith("/*")) continue;
2020
+ pairs.push({
2021
+ pattern: pattern.slice(0, -2),
2022
+ target: resolve(base, target.slice(0, -2)),
2023
+ kind: "prefix"
2024
+ });
2025
+ continue;
2026
+ }
2027
+ if (patternStars > 0 || targetStars > 0) continue;
2028
+ pairs.push({
2029
+ pattern,
2030
+ target: resolve(base, target),
2031
+ kind: "exact"
2032
+ });
2033
+ }
2034
+ return pairs;
2035
+ }
2036
+ //#endregion
2037
+ //#region pipeline/discover-packages.ts
2038
+ function findPackageRoot(absEntryPath) {
2039
+ let pkgRoot = dirname$1(absEntryPath);
2040
+ while (pkgRoot !== dirname$1(pkgRoot) && !existsSync(join$1(pkgRoot, "package.json"))) pkgRoot = dirname$1(pkgRoot);
2041
+ return pkgRoot;
2042
+ }
2043
+ const PACKAGE_SRC_EXCLUDES = [
1571
2044
  "dist",
1572
2045
  "node_modules",
1573
2046
  ".test.",
@@ -1614,6 +2087,77 @@ function sourceEntryForSpecifier(specifier, srcDir, extensionsSet) {
1614
2087
  const subpath = specifier.slice(packageName.length + 1);
1615
2088
  return resolveAbsolutePathSpecifier(join$1(srcDir, subpath || "index"), extensionsSet);
1616
2089
  }
2090
+ /**
2091
+ * A package's `sideEffects` field, decoded the way Vite reads it: a boolean
2092
+ * as written, or the list's globs, one with no `/` matching the basename
2093
+ * anywhere and any other rooted at the package. Undefined without a readable
2094
+ * field, or with glob syntax this matcher lacks (braces, classes, negation,
2095
+ * extglobs), which leaves the bundler's side-effectful default.
2096
+ */
2097
+ function readPackageSideEffects(pkgRoot) {
2098
+ let manifest;
2099
+ try {
2100
+ manifest = JSON.parse(readFileSync(join$1(pkgRoot, "package.json"), "utf-8"));
2101
+ } catch {
2102
+ return;
2103
+ }
2104
+ const field = isJsonBlock(manifest) ? manifest.sideEffects : void 0;
2105
+ if (field === true || field === false) return field;
2106
+ if (!Array.isArray(field)) return void 0;
2107
+ const globs = field.filter(isJsonString);
2108
+ if (globs.some((glob) => /[{}[\]()!+@]/.test(glob))) return void 0;
2109
+ return globs.map((glob) => globToRegExp(glob.includes("/") ? glob.replace(/^\.?\//, "") : `**/${glob}`));
2110
+ }
2111
+ function matchesSideEffects(globs, pkgRoot, absFiles) {
2112
+ return absFiles.some((file) => {
2113
+ const relPath = relative(pkgRoot, file).split(sep).join("/");
2114
+ return globs.some((glob) => glob.test(relPath));
2115
+ });
2116
+ }
2117
+ /** An exports target under the `import` condition, then `default`, as the
2118
+ * system loader reads it; an array offers its targets in order. */
2119
+ function exportTarget(value) {
2120
+ if (isJsonString(value)) return value;
2121
+ if (Array.isArray(value)) {
2122
+ for (const entry of value) {
2123
+ const target = exportTarget(entry);
2124
+ if (target !== null) return target;
2125
+ }
2126
+ return null;
2127
+ }
2128
+ if (!isJsonBlock(value)) return null;
2129
+ return exportTarget(value.import) ?? exportTarget(value.default);
2130
+ }
2131
+ /** Each exact `exports` entry of the package at `pkgRoot`, as its subpath
2132
+ * and absolute target; `*` patterns name no single file and are skipped. */
2133
+ function packageExportEntries(pkgRoot) {
2134
+ let manifest;
2135
+ try {
2136
+ manifest = JSON.parse(readFileSync(join$1(pkgRoot, "package.json"), "utf-8"));
2137
+ } catch {
2138
+ return [];
2139
+ }
2140
+ const exports = isJsonBlock(manifest) ? manifest.exports : void 0;
2141
+ if (exports === void 0 || exports === null) return [];
2142
+ const subpaths = isJsonBlock(exports) && Object.keys(exports).some((k) => k.startsWith(".")) ? exports : { ".": exports };
2143
+ const entries = [];
2144
+ for (const [subpath, value] of Object.entries(subpaths)) {
2145
+ if (!subpath.startsWith(".") || subpath.includes("*")) continue;
2146
+ const target = exportTarget(value);
2147
+ if (target !== null) entries.push([subpath, resolve(pkgRoot, target)]);
2148
+ }
2149
+ return entries;
2150
+ }
2151
+ /** Installed content is release content, and unpacking can leave any file
2152
+ * times, so only a package whose real path is outside `node_modules`, such
2153
+ * as a workspace link, is judged stale by them. */
2154
+ function isInstalledPackage(pkgRoot) {
2155
+ let realRoot = pkgRoot;
2156
+ try {
2157
+ realRoot = realpathSync(pkgRoot);
2158
+ } catch {}
2159
+ return realRoot.split(/[\\/]/).includes("node_modules");
2160
+ }
1617
2161
  function distEntryIsStale(absEntry, srcDir, srcFiles) {
1618
2162
  if (isPathWithinRoot(srcDir, absEntry)) return false;
1619
2163
  let distMtime;
@@ -1634,6 +2178,20 @@ async function collectExternalPackageSources(opts) {
1634
2178
  const pushed = /* @__PURE__ */ new Set();
1635
2179
  const packageMap = {};
1636
2180
  const sourceEntries = /* @__PURE__ */ new Map();
2181
+ const sourceEntrySideEffects = /* @__PURE__ */ new Map();
2182
+ /** `replaced` names the entry the redirect stands in for; it is only
2183
+ * needed, and so only resolved, for a glob list. */
2184
+ const redirect = async (specifier, srcEntry, pkgRoot, replaced) => {
2185
+ sourceEntries.set(specifier, srcEntry);
2186
+ const field = readPackageSideEffects(pkgRoot);
2187
+ if (field === void 0) return;
2188
+ if (field === true || field === false) {
2189
+ sourceEntrySideEffects.set(specifier, field);
2190
+ return;
2191
+ }
2192
+ const replacedEntry = await replaced();
2193
+ sourceEntrySideEffects.set(specifier, replacedEntry === null || matchesSideEffects(field, pkgRoot, [srcEntry, replacedEntry]));
2194
+ };
1637
2195
  const packageDirs = [];
1638
2196
  const dirOwnerSets = {};
1639
2197
  const dirExtensions = {};
@@ -1671,7 +2229,8 @@ async function collectExternalPackageSources(opts) {
1671
2229
  const srcEntry = sourceEntryForSpecifier(specifier, srcDir, extensionsSet);
1672
2230
  if (srcEntry) {
1673
2231
  packageMap[specifier] = relative(rootDir, srcEntry);
1674
- sourceEntries.set(specifier, srcEntry);
2232
+ const replaced = absEntry;
2233
+ await redirect(specifier, srcEntry, pkgRoot, async () => replaced);
1675
2234
  } else packageMap[specifier] = relative(rootDir, absEntry);
1676
2235
  if (!isAbsolute(specifier)) {
1677
2236
  const packageName = bareSpecifierPackageName(specifier);
@@ -1679,12 +2238,18 @@ async function collectExternalPackageSources(opts) {
1679
2238
  const rootEntry = sourceEntryForSpecifier(packageName, srcDir, extensionsSet);
1680
2239
  if (rootEntry) {
1681
2240
  packageMap[packageName] = relative(rootDir, rootEntry);
1682
- sourceEntries.set(packageName, rootEntry);
2241
+ await redirect(packageName, rootEntry, pkgRoot, async () => {
2242
+ try {
2243
+ return await resolveSpecifier(packageName);
2244
+ } catch {
2245
+ return null;
2246
+ }
2247
+ });
1683
2248
  }
1684
2249
  }
1685
2250
  }
1686
2251
  const pkgFiles = walkPackageSources(srcDir, extensionsSet);
1687
- staleDist = distEntryIsStale(absEntry, srcDir, pkgFiles);
2252
+ staleDist = !isInstalledPackage(pkgRoot) && distEntryIsStale(absEntry, srcDir, pkgFiles);
1688
2253
  for (const pkgFile of pkgFiles) {
1689
2254
  const relPath = relative(rootDir, pkgFile);
1690
2255
  if (alreadyIngested(relPath)) {
@@ -1744,6 +2309,15 @@ async function collectExternalPackageSources(opts) {
1744
2309
  fileCount++;
1745
2310
  }
1746
2311
  }
2312
+ if (!isAbsolute(specifier)) {
2313
+ const packageName = bareSpecifierPackageName(specifier);
2314
+ for (const [subpath, target] of packageExportEntries(pkgRoot)) {
2315
+ const entrySpecifier = packageName + subpath.slice(1);
2316
+ const targetRelPath = relative(rootDir, target);
2317
+ if (entrySpecifier in packageMap || !alreadyIngested(targetRelPath)) continue;
2318
+ packageMap[entrySpecifier] = targetRelPath;
2319
+ }
2320
+ }
1747
2321
  outcomes.push({
1748
2322
  specifier,
1749
2323
  outcome: staleDist ? "stale-dist" : fileCount > 0 ? "resolved" : "empty",
@@ -1754,6 +2328,7 @@ async function collectExternalPackageSources(opts) {
1754
2328
  entries,
1755
2329
  packageMap,
1756
2330
  sourceEntries,
2331
+ sourceEntrySideEffects,
1757
2332
  packageDirs,
1758
2333
  dirOwnerSets,
1759
2334
  dirExtensions,
@@ -1777,9 +2352,12 @@ function excludeCollectedPackages(collected, rejectedSpecifiers, rootDir) {
1777
2352
  packageMap[specifier] = relTarget;
1778
2353
  }
1779
2354
  const sourceEntries = /* @__PURE__ */ new Map();
2355
+ const sourceEntrySideEffects = /* @__PURE__ */ new Map();
1780
2356
  for (const [specifier, absEntry] of collected.sourceEntries) {
1781
2357
  if (targetRejected(specifier, absEntry)) continue;
1782
2358
  sourceEntries.set(specifier, absEntry);
2359
+ const sideEffects = collected.sourceEntrySideEffects.get(specifier);
2360
+ if (sideEffects !== void 0) sourceEntrySideEffects.set(specifier, sideEffects);
1783
2361
  }
1784
2362
  const dirOwnerSets = {};
1785
2363
  for (const [dir, specs] of Object.entries(collected.dirOwnerSets)) {
@@ -1801,6 +2379,7 @@ function excludeCollectedPackages(collected, rejectedSpecifiers, rootDir) {
1801
2379
  entries: collected.entries.filter((entry) => !rejectedSpecifiers.has(collected.fileOwners[entry.path])),
1802
2380
  packageMap,
1803
2381
  sourceEntries,
2382
+ sourceEntrySideEffects,
1804
2383
  packageDirs: collected.packageDirs.filter((dir) => !rejectedDirs.includes(dir)),
1805
2384
  dirOwnerSets,
1806
2385
  dirExtensions,
@@ -1820,15 +2399,50 @@ function staleDistIncludesMessage(outcomes) {
1820
2399
  if (stale.length === 0) return null;
1821
2400
  return `[animus-extract] stale dist for include specifier(s): ${stale.join(", ")} — dist entry is older than the newest src/ file; rebuild the package(s) before extracting`;
1822
2401
  }
1823
- function extractSystemFilePackages(systemFilePath) {
2402
+ /** The engine's own parse of a module's imports, through its
2403
+ * `extractFacts`; undefined for an engine without one. A parse that
2404
+ * panicked reports no imports, so it counts as no parse. */
2405
+ function engineImportParser(engine) {
2406
+ const { extractFacts } = engine;
2407
+ if (!extractFacts) return void 0;
2408
+ return (source, path) => {
2409
+ try {
2410
+ const facts = parseInternalWire(extractFacts(JSON.stringify([{
2411
+ path,
2412
+ source
2413
+ }])), "extractFacts").files[path];
2414
+ return facts && !facts.parsePanicked ? facts.imports : null;
2415
+ } catch {
2416
+ return null;
2417
+ }
2418
+ };
2419
+ }
2420
+ /** A pattern matching a call of any of `callees` as a whole name, or null
2421
+ * when there is none to anchor on. */
2422
+ function rootCallPattern(callees) {
2423
+ if (callees.length === 0) return null;
2424
+ return `(?<![a-zA-Z0-9_$.])(?:${callees.map((callee) => callee.replace(/[.$]/g, "\\$&")).join("|")})`;
2425
+ }
2426
+ function extractSystemFilePackages(systemFilePath, parseImports) {
1824
2427
  let source;
1825
2428
  try {
1826
2429
  source = readFileSync(systemFilePath, "utf-8");
1827
2430
  } catch {
1828
2431
  return [];
1829
2432
  }
2433
+ const parsed = parseImports?.(source, systemFilePath) ?? null;
2434
+ const parsedImports = parsed && parsed.length > 0 ? parsed : null;
2435
+ let rootCall = "createSystem";
2436
+ if (parsedImports) {
2437
+ const shadowed = parsedImports.some((binding) => binding.local === "createSystem" && binding.imported !== "createSystem") || /\b(?:function\*?|class)\s+createSystem\b|\b(?:const|let|var)\s+createSystem\b/.test(source);
2438
+ rootCall = rootCallPattern([.../* @__PURE__ */ new Set([
2439
+ ...parsedImports.filter((binding) => binding.imported === "createSystem").map((binding) => binding.local),
2440
+ ...Array.from(source.matchAll(/\bimport\s+(?:[a-zA-Z_$][a-zA-Z0-9_$]*\s*,\s*)?\*\s*as\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s+from\s+['"]@animus-ui\/system(?:\/[^'"]*)?['"]/g), (match) => `${match[1]}.createSystem`),
2441
+ ...shadowed ? [] : ["createSystem"]
2442
+ ])]);
2443
+ }
1830
2444
  const identifiers = /* @__PURE__ */ new Set();
1831
- const constructorRegex = /createSystem\s*\(\s*\{[^}]*?\bincludes\s*:\s*\[([^\]]*)\]/gs;
2445
+ const constructorRegex = rootCall === null ? null : new RegExp(`${rootCall}\\s*\\(\\s*\\{[^}]*?\\bincludes\\s*:\\s*\\[([^\\]]*)\\]`, "gs");
1832
2446
  const chainRegex = /\.includes\s*\(\s*\[([^\]]*)\]\s*\)/gs;
1833
2447
  const collectIdentifiers = (regex) => {
1834
2448
  let match;
@@ -1840,7 +2454,7 @@ function extractSystemFilePackages(systemFilePath) {
1840
2454
  }
1841
2455
  }
1842
2456
  };
1843
- collectIdentifiers(constructorRegex);
2457
+ if (constructorRegex) collectIdentifiers(constructorRegex);
1844
2458
  collectIdentifiers(chainRegex);
1845
2459
  const IDENT_START_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*/;
1846
2460
  const skipTrivia = (from) => {
@@ -1860,6 +2474,38 @@ function extractSystemFilePackages(systemFilePath) {
1860
2474
  return pos;
1861
2475
  }
1862
2476
  };
2477
+ /** The index after the string literal opening at `from`. */
2478
+ const skipString = (from) => {
2479
+ const quote = source[from];
2480
+ let pos = from + 1;
2481
+ while (pos < source.length && source[pos] !== quote) pos += source[pos] === "\\" ? 2 : 1;
2482
+ return pos + 1;
2483
+ };
2484
+ /** The index after the bracketed span opening at `from`, balanced across
2485
+ * `<>`, `()`, `[]` and `{}`, or -1 when it never closes. An arrow type's
2486
+ * `=>` closes nothing. */
2487
+ const skipBalanced = (from) => {
2488
+ let depth = 0;
2489
+ let pos = from;
2490
+ while (pos < source.length) {
2491
+ const ch = source[pos];
2492
+ if (ch === "'" || ch === "\"" || ch === "`") {
2493
+ pos = skipString(pos);
2494
+ continue;
2495
+ }
2496
+ if (source.startsWith("//", pos) || source.startsWith("/*", pos)) {
2497
+ pos = skipTrivia(pos);
2498
+ continue;
2499
+ }
2500
+ if ("<([{".includes(ch)) depth++;
2501
+ else if (")]}".includes(ch) || ch === ">" && source[pos - 1] !== "=") {
2502
+ depth--;
2503
+ if (depth === 0) return pos + 1;
2504
+ }
2505
+ pos++;
2506
+ }
2507
+ return -1;
2508
+ };
1863
2509
  const consumeChainLinks = (from) => {
1864
2510
  let pos = from;
1865
2511
  for (;;) {
@@ -1869,6 +2515,11 @@ function extractSystemFilePackages(systemFilePath) {
1869
2515
  const method = IDENT_START_RE.exec(source.slice(cursor))?.[0];
1870
2516
  if (method !== "extend" && method !== "from") return pos;
1871
2517
  cursor = skipTrivia(cursor + method.length);
2518
+ if (source[cursor] === "<") {
2519
+ const afterTypeArguments = skipBalanced(cursor);
2520
+ if (afterTypeArguments === -1) return pos;
2521
+ cursor = skipTrivia(afterTypeArguments);
2522
+ }
1872
2523
  if (source[cursor] !== "(") return pos;
1873
2524
  cursor = skipTrivia(cursor + 1);
1874
2525
  const base = IDENT_START_RE.exec(source.slice(cursor))?.[0];
@@ -1886,14 +2537,47 @@ function extractSystemFilePackages(systemFilePath) {
1886
2537
  pos = cursor + 1;
1887
2538
  }
1888
2539
  };
2540
+ const bindingAt = /* @__PURE__ */ new Map();
2541
+ const aliasOf = /* @__PURE__ */ new Map();
2542
+ const declarationRe = /\b(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
2543
+ let declaration;
2544
+ while ((declaration = declarationRe.exec(source)) !== null) {
2545
+ const name = declaration[1];
2546
+ let cursor = skipTrivia(declaration.index + declaration[0].length);
2547
+ if (source[cursor] === ":") {
2548
+ cursor++;
2549
+ while (cursor < source.length) {
2550
+ const ch = source[cursor];
2551
+ if (ch === ";" || ch === ",") break;
2552
+ if (ch === "\n" && /^\s*(?:export\s+)?(?:const|let|var|function|class|type|interface|import)\b/.test(source.slice(cursor + 1, cursor + 80))) break;
2553
+ if (ch === "=" && source[cursor + 1] !== ">") break;
2554
+ if ("<([{".includes(ch) || ch === "'" || ch === "\"" || ch === "`") {
2555
+ const after = "<([{".includes(ch) ? skipBalanced(cursor) : skipString(cursor);
2556
+ if (after === -1) break;
2557
+ cursor = after;
2558
+ continue;
2559
+ }
2560
+ cursor = ch === "=" ? cursor + 2 : cursor + 1;
2561
+ }
2562
+ }
2563
+ if (source[cursor] !== "=" || source[cursor + 1] === "=") continue;
2564
+ const initializer = skipTrivia(cursor + 1);
2565
+ bindingAt.set(initializer, name);
2566
+ const target = IDENT_START_RE.exec(source.slice(initializer))?.[0];
2567
+ if (!target) continue;
2568
+ const rest = source.slice(initializer + target.length);
2569
+ if (/^[ \t]*(?:(?:as|satisfies)\s[^;\n]*)?(?:\/\/[^\n]*)?(?:;|\r?\n(?!\s*[.([?`])|$)/.test(rest)) aliasOf.set(name, target);
2570
+ }
1889
2571
  const boundIdentifierBefore = (index) => {
2572
+ const declared = bindingAt.get(index);
2573
+ if (declared) return declared;
1890
2574
  const match = /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*$/.exec(source.slice(0, index));
1891
2575
  return match ? match[1] : null;
1892
2576
  };
1893
2577
  const chainRootIdentifiers = /* @__PURE__ */ new Set();
1894
- const createSystemAnchor = /createSystem\s*\(/g;
2578
+ const createSystemAnchor = rootCall === null ? null : new RegExp(`${rootCall}\\s*\\(`, "g");
1895
2579
  let anchorMatch;
1896
- while ((anchorMatch = createSystemAnchor.exec(source)) !== null) {
2580
+ while ((anchorMatch = createSystemAnchor?.exec(source) ?? null) !== null) {
1897
2581
  let pos = anchorMatch.index + anchorMatch[0].length;
1898
2582
  let depth = 1;
1899
2583
  while (pos < source.length && depth > 0) {
@@ -1924,22 +2608,37 @@ function extractSystemFilePackages(systemFilePath) {
1924
2608
  }
1925
2609
  if (identifiers.size === 0) return [];
1926
2610
  const importMap = /* @__PURE__ */ new Map();
2611
+ for (const binding of parsedImports ?? []) importMap.set(binding.local, binding.source);
1927
2612
  const importRegex = /^\s*import\s+(?:([a-zA-Z_$][a-zA-Z0-9_$]*)\s*,\s*)?(?:\{([^}]*)\}|([a-zA-Z_$][a-zA-Z0-9_$]*))\s+from\s+['"]([^'"]+)['"]/gm;
1928
- let importMatch;
1929
- while ((importMatch = importRegex.exec(source)) !== null) {
1930
- const [, comboDefault, namedImports, defaultImport, specifier] = importMatch;
1931
- if (comboDefault) importMap.set(comboDefault, specifier);
1932
- if (defaultImport) importMap.set(defaultImport, specifier);
1933
- if (namedImports) for (const binding of namedImports.split(",")) {
1934
- const parts = binding.trim().split(/\s+as\s+/);
1935
- const localName = (parts[1] || parts[0]).trim();
1936
- if (localName) importMap.set(localName, specifier);
2613
+ if (!parsedImports) {
2614
+ let importMatch;
2615
+ while ((importMatch = importRegex.exec(source)) !== null) {
2616
+ const [, comboDefault, namedImports, defaultImport, specifier] = importMatch;
2617
+ if (comboDefault) importMap.set(comboDefault, specifier);
2618
+ if (defaultImport) importMap.set(defaultImport, specifier);
2619
+ if (namedImports) for (const binding of namedImports.split(",")) {
2620
+ const parts = binding.trim().split(/\s+as\s+/);
2621
+ const localName = (parts[1] || parts[0]).trim();
2622
+ if (localName) importMap.set(localName, specifier);
2623
+ }
1937
2624
  }
1938
2625
  }
1939
2626
  const systemFileDir = dirname$1(systemFilePath);
1940
2627
  const packages = /* @__PURE__ */ new Set();
2628
+ /** The import a local alias chain ends at, or the identifier itself. */
2629
+ const throughAliases = (id) => {
2630
+ const seen = /* @__PURE__ */ new Set();
2631
+ let current = id;
2632
+ while (!importMap.has(current) && !seen.has(current)) {
2633
+ seen.add(current);
2634
+ const target = aliasOf.get(current);
2635
+ if (!target) break;
2636
+ current = target;
2637
+ }
2638
+ return current;
2639
+ };
1941
2640
  for (const id of identifiers) {
1942
- const specifier = importMap.get(id);
2641
+ const specifier = importMap.get(throughAliases(id));
1943
2642
  if (!specifier) continue;
1944
2643
  if (specifier.startsWith(".")) packages.add(resolve(systemFileDir, specifier));
1945
2644
  else packages.add(specifier);
@@ -1998,31 +2697,113 @@ function createSourceCorpus(host, ingestor = createSourceIngestor(host)) {
1998
2697
  //#endregion
1999
2698
  //#region pipeline/asset-placeholders.ts
2000
2699
  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) {
2700
+ const URL_OPEN_RE = /url\(\s*(['"]?)/gi;
2701
+ const IMAGE_SET_OPEN_RE = /(?:-webkit-)?image-set\(/gi;
2702
+ const BARE_SPECIFIER_RE = /^[^'")\s]+/;
2703
+ /** The index of the `)` that closes the `(` at `open`, outside strings. */
2704
+ function closingParen(css, open) {
2705
+ let depth = 0;
2706
+ let quote = null;
2707
+ for (let i = open; i < css.length; i += 1) {
2708
+ const c = css[i];
2709
+ if (quote !== null) {
2710
+ if (c === quote) quote = null;
2711
+ } else if (c === "\"" || c === "'") quote = c;
2712
+ else if (c === "(") depth += 1;
2713
+ else if (c === ")") {
2714
+ depth -= 1;
2715
+ if (depth === 0) return i;
2716
+ }
2717
+ }
2718
+ return css.length;
2719
+ }
2720
+ function quotedReference(css, quoteAt) {
2721
+ const start = quoteAt + 1;
2722
+ if (!css.startsWith("animus-asset:", start)) return null;
2723
+ const end = css.indexOf(css[quoteAt], start);
2724
+ if (end === -1) return null;
2725
+ return {
2726
+ start,
2727
+ end,
2728
+ specifier: css.slice(start + 13, end)
2729
+ };
2730
+ }
2731
+ function assetReferences(css) {
2006
2732
  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];
2733
+ const references = [];
2734
+ for (const open of css.matchAll(URL_OPEN_RE)) {
2735
+ const at = (open.index ?? 0) + open[0].length;
2736
+ if (open[1]) {
2737
+ const reference = quotedReference(css, at - 1);
2738
+ if (reference) references.push(reference);
2739
+ continue;
2740
+ }
2741
+ if (!css.startsWith("animus-asset:", at)) continue;
2742
+ const specifier = BARE_SPECIFIER_RE.exec(css.slice(at + 13))?.[0];
2743
+ if (specifier === void 0) continue;
2744
+ const end = at + 13 + specifier.length;
2745
+ references.push({
2746
+ start: at,
2747
+ end,
2748
+ specifier
2749
+ });
2750
+ }
2751
+ for (const open of css.matchAll(IMAGE_SET_OPEN_RE)) {
2752
+ const paren = (open.index ?? 0) + open[0].length - 1;
2753
+ const close = closingParen(css, paren);
2754
+ let depth = 0;
2755
+ let candidateStart = true;
2756
+ for (let i = paren + 1; i < close; i += 1) {
2757
+ const c = css[i];
2758
+ if (/\s/.test(c)) continue;
2759
+ if (c === "\"" || c === "'") {
2760
+ if (depth === 0 && candidateStart) {
2761
+ const reference = quotedReference(css, i);
2762
+ if (reference) references.push(reference);
2763
+ }
2764
+ const end = css.indexOf(c, i + 1);
2765
+ i = end === -1 ? close : end;
2766
+ } else if (c === "(") depth += 1;
2767
+ else if (c === ")") depth -= 1;
2768
+ else if (c === "," && depth === 0) {
2769
+ candidateStart = true;
2770
+ continue;
2771
+ }
2772
+ candidateStart = false;
2773
+ }
2774
+ }
2775
+ return references.sort((a, b) => a.start - b.start);
2776
+ }
2777
+ function findAssetSpecifiers(css) {
2778
+ return [...new Set(assetReferences(css).map((reference) => reference.specifier))];
2014
2779
  }
2015
2780
  function substituteAssetPlaceholders(css, urlBySpecifier) {
2016
2781
  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
2782
  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));
2783
+ for (const reference of assetReferences(css).reverse()) {
2784
+ const url = urlBySpecifier.get(reference.specifier);
2785
+ if (url === void 0) continue;
2786
+ out = out.slice(0, reference.start) + url + out.slice(reference.end);
2023
2787
  }
2024
2788
  return out;
2025
2789
  }
2790
+ /**
2791
+ * Specifiers of placeholder text anywhere inside a `url()` or `image-set()`
2792
+ * call, a reference or not: after substitution any of it is a broken load.
2793
+ */
2794
+ function placeholdersInAssetCalls(css) {
2795
+ if (!css.includes("animus-asset:")) return [];
2796
+ const calls = [...css.matchAll(URL_OPEN_RE), ...css.matchAll(IMAGE_SET_OPEN_RE)].map((open) => {
2797
+ const paren = css.indexOf("(", open.index ?? 0);
2798
+ return [paren, closingParen(css, paren)];
2799
+ });
2800
+ const found = /* @__PURE__ */ new Set();
2801
+ for (let at = css.indexOf(ASSET_PLACEHOLDER_PREFIX); at !== -1; at = css.indexOf(ASSET_PLACEHOLDER_PREFIX, at + 1)) {
2802
+ if (!calls.some(([open, close]) => open < at && at < close)) continue;
2803
+ found.add(/^[^'")\s,]*/.exec(css.slice(at + 13))[0]);
2804
+ }
2805
+ return [...found];
2806
+ }
2026
2807
  function findSheetAssetSpecifiers(sheets) {
2027
2808
  return [.../* @__PURE__ */ new Set([
2028
2809
  ...findAssetSpecifiers(sheets.variableCss),
@@ -2042,12 +2823,22 @@ const UNSUBSTITUTED_ASSET_CODE = "animus.asset.unsubstituted-placeholder";
2042
2823
  /** A placeholder in emitted CSS is a broken URL in the browser: it fails a
2043
2824
  * strict build and warns otherwise. */
2044
2825
  function reportSurvivingAssetPlaceholders(css, report) {
2045
- const specifiers = findAssetSpecifiers(css);
2826
+ const specifiers = placeholdersInAssetCalls(css);
2046
2827
  if (specifiers.length === 0) return;
2047
- const message = `${report.prefix} asset() placeholders reached emitted CSS unsubstituted: ${specifiers.join(", ")} (${UNSUBSTITUTED_ASSET_CODE})`;
2828
+ const message = `${report.prefix} asset() placeholders reached ${report.surface ?? "emitted CSS"} unsubstituted: ${specifiers.join(", ")} (${UNSUBSTITUTED_ASSET_CODE})`;
2048
2829
  if (report.strict) throw new Error(message);
2049
2830
  report.warn(message);
2050
2831
  }
2832
+ /**
2833
+ * The runtime code an analysis generates: each dynamic prop config, which the
2834
+ * shared prop map module carries, and each component's replacement. Extraction
2835
+ * lifts asset() out of them into root variables, so none should hold one.
2836
+ * Both embed values as JSON strings, so their quote and backslash escapes are
2837
+ * undone for scanning: the text is for `findAssetSpecifiers`, not to run.
2838
+ */
2839
+ function generatedModuleCode(manifest) {
2840
+ return [JSON.stringify(manifest.dynamic_props), ...Object.values(manifest.components).map((component) => component.replacement)].join("\n").replace(/\\(["\\])/g, "$1");
2841
+ }
2051
2842
  //#endregion
2052
2843
  //#region pipeline/resolve-asset.ts
2053
2844
  /**
@@ -2246,14 +3037,9 @@ function buildPathAliasesJson(pairs, rootDir) {
2246
3037
  //#endregion
2247
3038
  //#region pipeline/post-process-css.ts
2248
3039
  function resolveLightningTargets(explicitTargets, rootDir) {
2249
- let queries;
2250
- if (explicitTargets) queries = Array.isArray(explicitTargets) ? explicitTargets : [explicitTargets];
2251
- else {
2252
- const detected = browserslist(void 0, { path: rootDir });
2253
- queries = detected.length > 0 ? detected : browserslist("defaults");
2254
- }
2255
- const [firstQuery] = queries;
2256
- return browserslistToTargets(firstQuery !== void 0 && firstQuery.includes(" ") ? browserslist(queries) : queries);
3040
+ if (explicitTargets) return browserslistToTargets(browserslist(explicitTargets));
3041
+ const detected = browserslist(void 0, { path: rootDir });
3042
+ return browserslistToTargets(detected.length > 0 ? detected : browserslist("defaults"));
2257
3043
  }
2258
3044
  function postProcessCss(css, opts) {
2259
3045
  if (!css) return css;
@@ -2271,186 +3057,28 @@ function postProcessCss(css, opts) {
2271
3057
  }
2272
3058
  }
2273
3059
  //#endregion
2274
- //#region pipeline/manifest-diagnostics.ts
2275
- /** Stable code for selector forms with no substitutable subject; the
2276
- * extraction engine mints the same string. */
2277
- const SELECTOR_UNSUPPORTED_SUBJECT = "animus.selector.unsupported-subject";
2278
- /** The one matcher for the engine's unresolved-parent chain-drop message;
2279
- * consumers match through the exported helpers, never their own copy. */
2280
- const UNRESOLVED_PARENT_RE = /chain dropped: could not resolve parent component '([^']+)'/;
2281
- function isUnresolvedParentDrop(diagnostic) {
2282
- return UNRESOLVED_PARENT_RE.test(diagnostic.message);
2283
- }
2284
- function unresolvedParentName(diagnostic) {
2285
- return UNRESOLVED_PARENT_RE.exec(diagnostic.message)?.[1] ?? null;
3060
+ //#region pipeline/property-registrations.ts
3061
+ const PROPERTY_AT_RULE = /@property\s+/gi;
3062
+ const RULE_BODY = /\s*\{[^}]*\}\n?/y;
3063
+ /** Each `@property` rule, through one newline after it, with its name read
3064
+ * as CSS tokenizes an identifier: escapes decoded, non-ASCII included. */
3065
+ function propertyRules(css) {
3066
+ const rules = [];
3067
+ for (const match of css.matchAll(PROPERTY_AT_RULE)) {
3068
+ if (match.index < (rules.at(-1)?.end ?? 0)) continue;
3069
+ const name = identifierAt(css, match.index + match[0].length);
3070
+ if (!name?.name.startsWith("--")) continue;
3071
+ RULE_BODY.lastIndex = name.end;
3072
+ const body = RULE_BODY.exec(css);
3073
+ if (body === null) continue;
3074
+ rules.push({
3075
+ start: match.index,
3076
+ end: name.end + body[0].length,
3077
+ name: name.name
3078
+ });
3079
+ }
3080
+ return rules;
2286
3081
  }
2287
- /** True when the selector carries at least one substitutable `&` subject
2288
- * outside quoted text. */
2289
- function hasSelectorSubject(value) {
2290
- let quote = null;
2291
- let escaped = false;
2292
- for (const c of value) {
2293
- if (escaped) {
2294
- escaped = false;
2295
- continue;
2296
- }
2297
- if (c === "\\") {
2298
- escaped = true;
2299
- continue;
2300
- }
2301
- if (quote !== null) {
2302
- if (c === quote) quote = null;
2303
- } else if (c === "\"" || c === "'") quote = c;
2304
- else if (c === "&") return true;
2305
- }
2306
- return false;
2307
- }
2308
- /**
2309
- * Coded diagnostics for selector-shaped alias values with no substitutable
2310
- * subject; alias values never reach the evaluator's own key guard.
2311
- */
2312
- function collectSelectorAliasDiagnostics(selectorAliasesJson) {
2313
- if (!selectorAliasesJson) return [];
2314
- const aliases = parseInternalWire(selectorAliasesJson, "selectorAliasesJson (the system loader's selector-alias registry)");
2315
- const diagnostics = [];
2316
- for (const [name, value] of Object.entries(aliases)) if (value.includes("&") && !hasSelectorSubject(value)) diagnostics.push({
2317
- file: "system",
2318
- component: name,
2319
- kind: "warn",
2320
- message: `selector alias '${name}' value '${value}' has no substitutable '&' subject outside quoted text (${SELECTOR_UNSUPPORTED_SUBJECT})`,
2321
- code: SELECTOR_UNSUPPORTED_SUBJECT,
2322
- severity: severityFor(SELECTOR_UNSUPPORTED_SUBJECT)
2323
- });
2324
- return diagnostics;
2325
- }
2326
- const UNREADABLE_SOURCE_FILE = "animus.ingestion.unreadable-source-file";
2327
- /**
2328
- * A configured source file that could not be read is a LOST INPUT: the
2329
- * emitted artifacts silently lack whatever that file declared.
2330
- */
2331
- function unreadableSourceDiagnostic(relPath, error) {
2332
- return {
2333
- file: relPath,
2334
- component: "source",
2335
- kind: "warn",
2336
- message: `configured source file ${relPath} could not be read and was skipped: ${String(error)}`,
2337
- code: UNREADABLE_SOURCE_FILE,
2338
- severity: severityFor(UNREADABLE_SOURCE_FILE)
2339
- };
2340
- }
2341
- /** The collision entry code minted by the system package's merge. */
2342
- const VOCABULARY_COLLISION = "animus.vocabulary.collision";
2343
- /** A theme `@property` registration browsers would ignore; it is not emitted. */
2344
- const INVALID_PROPERTY_REGISTRATION = "animus.theme.invalid-property-registration";
2345
- /** Minted when a sealed kit with registered vocabulary is consumed through
2346
- * the deprecated `from()`/`includes:` verbs. */
2347
- const VOCABULARY_LEGACY_VERB = "animus.vocabulary.legacy-verb";
2348
- /** A `var()` fallback that never applies: the property is registered with an
2349
- * initial value. */
2350
- const PROPERTY_FALLBACK_SUPPRESSED = "animus.property.fallback-suppressed";
2351
- /** The same, where the fallback heads a chain of further `var()` reads. */
2352
- const PROPERTY_FALLBACK_CHAIN_SUPPRESSED = "animus.property.fallback-chain-suppressed";
2353
- /** A declared property set in keyframes or transitioned without a typed
2354
- * registration, so it does not interpolate. */
2355
- const PROPERTY_UNREGISTERED_ANIMATION = "animus.property.unregistered-animation";
2356
- /** The same, where `allow-discrete` shows interpolation was intended. */
2357
- const PROPERTY_DISCRETE_ANIMATION = "animus.property.discrete-animation";
2358
- /** A custom property whose resolved value is `var()` of itself, which makes
2359
- * it invalid at computed-value time. */
2360
- const PROPERTY_SELF_REFERENCE = "animus.property.self-reference";
2361
- const DIAGNOSTIC_SEVERITY = /* @__PURE__ */ new Map([
2362
- [SELECTOR_UNSUPPORTED_SUBJECT, "error"],
2363
- [UNREADABLE_SOURCE_FILE, "error"],
2364
- [VOCABULARY_COLLISION, "warn"],
2365
- [VOCABULARY_LEGACY_VERB, "warn"],
2366
- [INVALID_PROPERTY_REGISTRATION, "error"],
2367
- [PROPERTY_FALLBACK_SUPPRESSED, "info"],
2368
- [PROPERTY_FALLBACK_CHAIN_SUPPRESSED, "warn"],
2369
- [PROPERTY_UNREGISTERED_ANIMATION, "info"],
2370
- [PROPERTY_DISCRETE_ANIMATION, "warn"],
2371
- [PROPERTY_SELF_REFERENCE, "warn"]
2372
- ]);
2373
- /** An unlisted code is `warn`: a witness kind from a newer system package
2374
- * must not fail the strict build of a host that predates its writer. */
2375
- function severityFor(code) {
2376
- return (code === void 0 ? void 0 : DIAGNOSTIC_SEVERITY.get(code)) ?? "warn";
2377
- }
2378
- /**
2379
- * The one mapper of a sealed system's witness entries for every host: the
2380
- * loader's evaluation host shims `console`, so the record is the channel.
2381
- */
2382
- function vocabularyWitnessDiagnostics(vocabularyWitnessesJson) {
2383
- if (!vocabularyWitnessesJson) return [];
2384
- const entries = parseInternalWire(vocabularyWitnessesJson, "vocabularyWitnessesJson (the sealed system's vocabulary witness record)");
2385
- const diagnostics = [];
2386
- for (const entry of entries) if (entry.code === "animus.vocabulary.collision") diagnostics.push({
2387
- file: "system",
2388
- component: entry.name ?? "keyframes",
2389
- kind: "warn",
2390
- message: `keyframes vocabulary "${entry.name}" is registered by both ${entry.loser} and ${entry.winner} — ${entry.winner} wins; rename one collection (${entry.code})`,
2391
- code: entry.code,
2392
- severity: severityFor(entry.code)
2393
- });
2394
- else if (entry.code === "animus.vocabulary.legacy-verb") diagnostics.push({
2395
- file: "system",
2396
- component: "keyframes",
2397
- kind: "warn",
2398
- 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})`,
2399
- code: entry.code,
2400
- severity: severityFor(entry.code)
2401
- });
2402
- else diagnostics.push({
2403
- file: "system",
2404
- component: "vocabulary",
2405
- kind: "warn",
2406
- 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})` : ""}`,
2407
- code: entry.code,
2408
- severity: severityFor(entry.code)
2409
- });
2410
- return diagnostics;
2411
- }
2412
- /** The diagnostics a loaded system's own records carry: vocabulary
2413
- * witnesses and invalid `@property` registrations. */
2414
- function systemLoadDiagnostics(system) {
2415
- const registrations = (system.invalidPropertyRegistrations ?? []).map(({ name, reason }) => ({
2416
- file: "system",
2417
- component: name,
2418
- kind: "warn",
2419
- 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})`,
2420
- code: INVALID_PROPERTY_REGISTRATION,
2421
- severity: severityFor(INVALID_PROPERTY_REGISTRATION)
2422
- }));
2423
- return [...vocabularyWitnessDiagnostics(system.vocabularyWitnessesJson), ...registrations];
2424
- }
2425
- /**
2426
- * The single strict-escalation policy point for both extraction plugins:
2427
- * error-severity diagnostics throw together under `strict`, else warn.
2428
- */
2429
- function surfaceManifestDiagnostics(manifest, warn, policy = {}) {
2430
- const errors = [];
2431
- const diagnostics = policy.prepend?.length ? [...policy.prepend, ...manifest.diagnostics ?? []] : manifest.diagnostics ?? [];
2432
- for (const diagnostic of diagnostics) {
2433
- let line = null;
2434
- if (diagnostic.kind === "bail") line = `⚠ ${diagnostic.component} not extracted: ${diagnostic.message}`;
2435
- else if (diagnostic.kind === "skip") line = `⚠ ${diagnostic.component}: skipped ${diagnostic.message}`;
2436
- else if (diagnostic.kind === "warn") line = `${diagnostic.severity === "info" ? "ℹ" : "⚠"} ${diagnostic.file}: ${diagnostic.component}: ${diagnostic.message}`;
2437
- if (line === null) continue;
2438
- if (diagnostic.code && !diagnostic.message.includes(diagnostic.code)) line += ` [${diagnostic.code}]`;
2439
- if (diagnostic.severity === "info") {
2440
- policy.info?.(line);
2441
- continue;
2442
- }
2443
- if (policy.strict && diagnostic.severity === "error") {
2444
- errors.push(`${diagnostic.code ?? "error"} — ${diagnostic.component}: ${diagnostic.message}`);
2445
- continue;
2446
- }
2447
- warn(line);
2448
- }
2449
- if (errors.length > 0) throw new Error(`[animus] strict: ${errors.length} error diagnostic(s):\n${errors.join("\n")}`);
2450
- }
2451
- //#endregion
2452
- //#region pipeline/property-registrations.ts
2453
- const PROPERTY_RULE = /@property\s+(--[\w-]+)\s*\{[^}]*\}\n?/g;
2454
3082
  /** The data types a registration's `syntax` may name. */
2455
3083
  const SYNTAX_TYPES = /* @__PURE__ */ new Set([
2456
3084
  "angle",
@@ -2470,44 +3098,40 @@ const SYNTAX_TYPES = /* @__PURE__ */ new Set([
2470
3098
  "url"
2471
3099
  ]);
2472
3100
  const SYNTAX_COMPONENT = /^(?:<([a-z-]+)>[+#]?|-?[a-zA-Z_][\w-]*)$/;
2473
- /** The syntax types whose values can carry a font- or container-relative
2474
- * unit, which makes a typed initial value depend on context. */
2475
- const DIMENSION_TYPES = /* @__PURE__ */ new Set([
2476
- "image",
2477
- "length",
2478
- "length-percentage",
2479
- "transform-function",
2480
- "transform-list"
2481
- ]);
2482
- 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;
2483
- const SUBSTITUTION = /\b(?:var|env|attr)\(/i;
2484
- /** Quoted strings and `url()` hold text, never units or substitutions. */
2485
- const LITERAL_TEXT = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|url\([^)]*\)/gi;
3101
+ const INITIAL_VALUE = /(initial-value\s*:\s*)([^;}]*)/;
3102
+ const MATH_FUNCTION = /\b(?:calc|min|max|clamp)\(/i;
2486
3103
  /**
2487
3104
  * Removes the `@property` rules browsers would ignore — an unknown syntax
2488
3105
  * component, a syntax other than `*` without an initial value, an initial
2489
- * value that depends on context, or one the syntax does not accept.
2490
- * Minifying a stylesheet that contains such a rule can also fail.
3106
+ * value that substitutes another value or depends on context, math that
3107
+ * breaks calculation syntax, or a value the syntax does not accept.
3108
+ * Minifying a stylesheet that contains such a rule can also fail. An
3109
+ * all-absolute math initial value is written as its computed value, which
3110
+ * the minifier reads.
2491
3111
  */
2492
3112
  function splitInvalidPropertyRegistrations(css) {
2493
3113
  const invalid = [];
3114
+ let kept = "";
3115
+ let copied = 0;
3116
+ for (const { start, end, name } of propertyRules(css)) {
3117
+ const checked = checkRegistration(css.slice(start, end));
3118
+ kept += css.slice(copied, start);
3119
+ if ("rule" in checked) kept += checked.rule;
3120
+ else invalid.push({
3121
+ name,
3122
+ reason: checked.reason
3123
+ });
3124
+ copied = end;
3125
+ }
2494
3126
  return {
2495
- css: css.replace(PROPERTY_RULE, (rule, name) => {
2496
- const reason = registrationFailure(rule);
2497
- if (reason === null) return rule;
2498
- invalid.push({
2499
- name,
2500
- reason
2501
- });
2502
- return "";
2503
- }),
3127
+ css: kept + css.slice(copied),
2504
3128
  invalid
2505
3129
  };
2506
3130
  }
2507
3131
  /** The registrations in `css`, by custom-property name. */
2508
3132
  function propertyRegistrations(css) {
2509
3133
  const registrations = /* @__PURE__ */ new Map();
2510
- for (const match of css.matchAll(PROPERTY_RULE)) registrations.set(match[1], readRegistration(match[0]));
3134
+ for (const { start, end, name } of propertyRules(css)) registrations.set(name, readRegistration(css.slice(start, end)));
2511
3135
  return registrations;
2512
3136
  }
2513
3137
  function readRegistration(rule) {
@@ -2516,42 +3140,56 @@ function readRegistration(rule) {
2516
3140
  initialValue: /initial-value\s*:\s*([^;}]*)/.exec(rule)?.[1]?.trim()
2517
3141
  };
2518
3142
  }
2519
- function registrationFailure(rule) {
3143
+ /** The rule to keep, with an all-absolute math initial value folded, or why
3144
+ * a browser would ignore it. */
3145
+ function checkRegistration(rule) {
2520
3146
  const { syntax, initialValue } = readRegistration(rule);
3147
+ if (initialValue !== void 0 && substitutesValue(initialValue)) return { reason: `initialValue "${initialValue}" substitutes another value; var(), env() and attr() are not allowed in an initial value` };
3148
+ let kept = rule;
3149
+ let folded;
2521
3150
  if (syntax !== "*") {
2522
- const components = syntax.split("|").map((component) => ({
3151
+ const unknown = syntax.split("|").map((component) => ({
2523
3152
  text: component.trim(),
2524
3153
  match: SYNTAX_COMPONENT.exec(component.trim())
2525
- }));
2526
- const unknown = components.find(({ match }) => match === null || match[1] !== void 0 && !SYNTAX_TYPES.has(match[1]));
2527
- if (unknown !== void 0) return `syntax "${syntax}" has the unknown component "${unknown.text}"`;
2528
- if (initialValue === void 0) return `syntax "${syntax}" needs an initialValue`;
2529
- const dimensional = components.some(({ match }) => match?.[1] !== void 0 && DIMENSION_TYPES.has(match[1]));
2530
- const value = initialValue.replace(LITERAL_TEXT, "");
2531
- 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`;
3154
+ })).find(({ match }) => match === null || match[1] !== void 0 && !SYNTAX_TYPES.has(match[1]));
3155
+ if (unknown !== void 0) return { reason: `syntax "${syntax}" has the unknown component "${unknown.text}"` };
3156
+ if (initialValue === void 0) return { reason: `syntax "${syntax}" needs an initialValue` };
3157
+ if (dependsOnContext(initialValue)) return { reason: `initialValue "${initialValue}" depends on context; font- and container-relative units are not allowed` };
3158
+ folded = foldInitialValue(syntax, initialValue);
3159
+ if (folded?.kind === "invalid") return { reason: `initialValue "${initialValue}" is not a valid calculation: ${folded.reason}` };
3160
+ if (folded?.kind === "rejected") return { reason: `initialValue "${initialValue}" computes to the ${folded.type} ${folded.text}, which syntax "${syntax}" does not accept` };
3161
+ if (folded?.kind === "folded") kept = rule.replace(INITIAL_VALUE, `$1${folded.text}`);
2532
3162
  }
2533
3163
  try {
2534
3164
  transform({
2535
3165
  filename: "animus-registrations.css",
2536
- code: Buffer.from(rule)
3166
+ code: Buffer.from(kept)
2537
3167
  });
2538
- return null;
3168
+ return { rule: kept };
2539
3169
  } catch (error) {
2540
3170
  const detail = error instanceof Error ? error.message : String(error);
2541
- return `the CSS parser rejects \`${rule.trim()}\` (${detail})`;
3171
+ if (folded?.kind === "not-finite") return { reason: `initialValue "${initialValue}" computes to an infinite or NaN value, which browsers clamp and which has no plain CSS value to write instead, and the CSS parser that builds the stylesheet cannot read the math (${detail})` };
3172
+ 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` };
3173
+ return { reason: `the CSS parser rejects \`${kept.trim()}\` (${detail})` };
2542
3174
  }
2543
3175
  }
2544
3176
  //#endregion
2545
3177
  //#region pipeline/property-diagnostics.ts
2546
3178
  /**
2547
3179
  * Reports custom properties that will not behave as their CSS suggests: a
2548
- * property that refers to itself and, once the system emits an `@property`
2549
- * rule, a fallback an initial value suppresses and an animated property that
2550
- * cannot interpolate. The CSS itself is never changed.
3180
+ * property that refers to itself, a fallback a registered initial value
3181
+ * applies ahead of, and an animated property that cannot interpolate. Sheets
3182
+ * are read through the shared CSS tokenizer, as the prefix pass reads them:
3183
+ * comments are trivia, quoted text holds no names, and escapes and function
3184
+ * case are decoded. The CSS itself is never changed.
2551
3185
  */
2552
3186
  function checkCustomProperties(input) {
2553
3187
  const registered = propertyRegistrations(input.system.variableCss);
2554
- const declared = declaredProperties(input.system.contextualVarsJson);
3188
+ const declaredNames = input.system.contextualProperties ?? [];
3189
+ const declared = new Set(declaredNames);
3190
+ const withInitialValue = [...registered].filter(([, registration]) => registration.initialValue !== void 0).map(([name]) => name);
3191
+ const writers = currentVarWriters(input.system.propConfigJson);
3192
+ const currentVars = new Set(writers.keys());
2555
3193
  const findings = [];
2556
3194
  const sheets = [{
2557
3195
  css: input.componentCss,
@@ -2561,8 +3199,20 @@ function checkCustomProperties(input) {
2561
3199
  owner: globalOwner
2562
3200
  }];
2563
3201
  for (const { css, owner } of sheets) {
3202
+ if (!mayHoldFinding(css, withInitialValue, declaredNames, currentVars)) continue;
2564
3203
  const parsed = declarations(css);
2565
- const discreteRules = new Set(parsed.filter((d) => d.property === "transition-behavior" && /\ballow-discrete\b/.test(d.value)).map((d) => d.rule));
3204
+ for (const cycle of currentVarCycles(parsed, currentVars)) {
3205
+ const [first] = cycle;
3206
+ const names = cycle.map((d) => d.property);
3207
+ const props = [...new Set(cycle.flatMap((d) => writers.get(d.property) ?? []))];
3208
+ findings.push({
3209
+ code: PROPERTY_CURRENT_VAR_CYCLE,
3210
+ property: first.property,
3211
+ message: `${cycle.map((d) => `${d.property}: ${d.value}`).join("; ")} are written by the currentVar of ${props.join(", ")} in one rule and read each other, a cycle, so ${names.join(", ")} are invalid at computed-value time wherever this rule applies. Give one of the props a value that does not read another's current variable`,
3212
+ owner: owner(first)
3213
+ });
3214
+ }
3215
+ const discreteRules = new Set(parsed.filter((d) => d.property === "transition-behavior" && topLevelItems(d.values).some(isDiscrete)).map((d) => d.rule));
2566
3216
  for (const declaration of parsed) {
2567
3217
  const found = (code, property, message) => findings.push({
2568
3218
  code,
@@ -2570,139 +3220,275 @@ function checkCustomProperties(input) {
2570
3220
  message,
2571
3221
  owner: owner(declaration)
2572
3222
  });
2573
- if (isSelfReference(declaration)) {
3223
+ const reads = variableReads(declaration.values);
3224
+ if (isSelfReference(declaration, reads)) {
2574
3225
  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`);
2575
3226
  continue;
2576
3227
  }
2577
- if (registered.size === 0) continue;
2578
- for (const read of varReads(declaration.value)) {
3228
+ if (readsItselfDirectly(declaration, reads)) {
3229
+ const { property, value } = declaration;
3230
+ found(PROPERTY_SELF_REFERENCE, property, `${property}: ${value} reads ${property} directly, a reference to itself, so it is invalid at computed-value time; a fallback inside that var() does not break the cycle. Give it a value that does not read its own name`);
3231
+ continue;
3232
+ }
3233
+ if (readsItselfOnlyInFallback(declaration, reads)) {
3234
+ const { property, value } = declaration;
3235
+ found(PROPERTY_FALLBACK_SELF_REFERENCE, property, `${property}: ${value} reads ${property} inside another var()'s fallback. Wherever that fallback is used, ${property} refers to itself, a cycle in every browser, so ${property} is invalid at computed-value time, or takes its registered initial value, and so is every property that reads it; where it is not used, browsers differ on whether the cycle still counts. Without this declaration, descendants read the inherited ${property} instead, also where the outer variable is set, which is what a prop with currentVar does for such a value`);
3236
+ }
3237
+ for (const read of reads) {
2579
3238
  const registration = registered.get(read.name);
2580
3239
  if (read.fallback === void 0 || registration?.initialValue === void 0) continue;
2581
- const chain = read.fallback.startsWith("var(");
2582
- found(chain ? PROPERTY_FALLBACK_CHAIN_SUPPRESSED : PROPERTY_FALLBACK_SUPPRESSED, read.name, chain ? `var(${read.name}, ${read.fallback}) never reads its fallback chain: ${read.name} is registered with initial-value ${registration.initialValue}, which applies whenever it is unset. Remove the initial value or reorder the chain` : `var(${read.name}, ${read.fallback}) never uses its fallback: ${read.name} is registered with initial-value ${registration.initialValue}, which applies whenever it is unset`);
3240
+ const { fallback } = read;
3241
+ const chain = reads.some((other) => other.start === fallback.first);
3242
+ found(chain ? PROPERTY_FALLBACK_CHAIN_SUPPRESSED : PROPERTY_FALLBACK_SUPPRESSED, read.name, `var(${read.name}, ${css.slice(fallback.start, fallback.end).trim()}): 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`);
2583
3243
  }
2584
3244
  for (const animated of animatedProperties(declaration, discreteRules)) {
2585
- if (!declared.has(animated.name)) continue;
3245
+ if (animated.discrete || !declared.has(animated.name)) continue;
2586
3246
  const reason = interpolationGap(registered.get(animated.name));
2587
3247
  if (reason === null) continue;
2588
- if (animated.discrete) found(PROPERTY_DISCRETE_ANIMATION, animated.name, `${animated.name} is transitioned with allow-discrete but ${reason}, so it flips at the midpoint instead of interpolating. Register it with a typed syntax`);
2589
- else 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`);
3248
+ 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`);
2590
3249
  }
2591
3250
  }
2592
3251
  }
2593
3252
  return toDiagnostics(findings);
2594
3253
  }
2595
- function declaredProperties(contextualVarsJson) {
2596
- if (!contextualVarsJson) return /* @__PURE__ */ new Set();
2597
- const byScale = parseInternalWire(contextualVarsJson, "contextualVarsJson (the theme's contextual variable names)");
2598
- return new Set(Object.values(byScale).flatMap((names) => names.map((name) => `--${name}`)));
3254
+ /**
3255
+ * Whether a sheet can hold a finding, shown by its text before anything is
3256
+ * parsed: a custom property reading its own name, a `var(` with a fallback
3257
+ * of a registration that has an initial value, a transition or keyframes
3258
+ * and a declared contextual variable's name, or a `var(` of a currentVar name
3259
+ * when two or more props write one. An escape or a comment can hide
3260
+ * a name or a function from the text, so a sheet with either is always
3261
+ * parsed.
3262
+ */
3263
+ function mayHoldFinding(css, withInitialValue, declared, currentVars) {
3264
+ return css.includes("\\") || css.includes("/*") || readsOwnName(css) || withInitialValue.some((name) => new RegExp(String.raw`var\(\s*${escapeRegExp(name)}\s*,`, "i").test(css)) || declared.some((name) => css.includes(name)) && /transition|@keyframes/i.test(css) || currentVars.size > 1 && [...currentVars].some((name) => new RegExp(String.raw`var\(\s*${escapeRegExp(name)}`, "i").test(css));
3265
+ }
3266
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3267
+ /** A custom-property declaration key, as the text shows one: anything but
3268
+ * CSS whitespace and the characters that end a name, so a non-ASCII space
3269
+ * stays part of the name, as CSS reads it. */
3270
+ const CUSTOM_PROPERTY_KEY = /(--[^\t\n\f\r :;{}()"'\\/]+)[\t\n\f\r ]*:/g;
3271
+ const VAR_OPEN = /var\(\s*/iy;
3272
+ const isNameCode = (c) => c >= 48 && c <= 57 || c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 45 || c === 95 || c >= 128;
3273
+ /**
3274
+ * Whether a custom-property declaration has `var(` of its own whole name in
3275
+ * its value, bounded as the declaration scanner bounds one: by a `;`, `{` or
3276
+ * `}` outside parentheses, brackets and quoted text. `var` matches in any
3277
+ * case, so this errs toward parsing.
3278
+ */
3279
+ function readsOwnName(css) {
3280
+ for (const match of css.matchAll(CUSTOM_PROPERTY_KEY)) {
3281
+ const name = match[1];
3282
+ let depth = 0;
3283
+ let quote = -1;
3284
+ for (let i = match.index + match[0].length; i < css.length; i += 1) {
3285
+ const c = css.charCodeAt(i);
3286
+ if (quote !== -1) {
3287
+ if (c === quote || c === 10 || c === 13 || c === 12) quote = -1;
3288
+ } else if (c === 34 || c === 39) quote = c;
3289
+ else if (c === 40 || c === 91) depth += 1;
3290
+ else if (c === 41 || c === 93) depth = Math.max(0, depth - 1);
3291
+ else if (depth === 0 && (c === 59 || c === 123 || c === 125)) break;
3292
+ else if (c === 118 || c === 86) {
3293
+ VAR_OPEN.lastIndex = i;
3294
+ const at = VAR_OPEN.test(css) ? VAR_OPEN.lastIndex : -1;
3295
+ if (at !== -1 && css.startsWith(name, at) && !isNameCode(css.charCodeAt(at + name.length))) return true;
3296
+ }
3297
+ }
3298
+ }
3299
+ return false;
2599
3300
  }
2600
- /** Declarations in engine-emitted CSS: one `property: value` per `;`. */
3301
+ /** `items` as text without comments, at-keywords decoded. */
3302
+ function textOf(css, items) {
3303
+ let text = "";
3304
+ const add = (token) => {
3305
+ if (token.type === "whitespace") text += " ";
3306
+ else if (token.type === "at-keyword") text += `@${token.value.toLowerCase()}`;
3307
+ else text += css.slice(token.start, token.end);
3308
+ };
3309
+ const walk = (list) => {
3310
+ for (const item of list) if (item.kind === "token") add(item.token);
3311
+ else {
3312
+ add(item.open);
3313
+ walk(item.children);
3314
+ if (item.close) add(item.close);
3315
+ }
3316
+ };
3317
+ walk(items);
3318
+ return text.trim();
3319
+ }
3320
+ /** Declarations in a sheet: one `property: value` per `;` inside a `{}`
3321
+ * block, read from component values, so comments separate nothing. */
2601
3322
  function declarations(css) {
2602
3323
  const found = [];
2603
- const stack = [];
2604
3324
  let rules = 0;
2605
- let segment = 0;
2606
- let parens = 0;
2607
- let quote = null;
2608
- const take = (end) => {
2609
- const text = css.slice(segment, end);
2610
- const colon = text.indexOf(":");
2611
- const current = stack[stack.length - 1];
2612
- if (current === void 0 || colon <= 0) return;
2613
- found.push({
2614
- property: text.slice(0, colon).trim(),
2615
- value: text.slice(colon + 1).replace(/;$/, "").trim(),
2616
- start: segment + (text.length - text.trimStart().length),
2617
- end,
2618
- rule: current.rule,
2619
- context: stack.map((entry) => entry.prelude)
2620
- });
2621
- };
2622
- for (let i = 0; i < css.length; i += 1) {
2623
- const c = css[i];
2624
- if (quote !== null) {
2625
- if (c === "\\") i += 1;
2626
- else if (c === quote) quote = null;
2627
- continue;
2628
- }
2629
- if (c === "\"" || c === "'") quote = c;
2630
- else if (c === "(") parens += 1;
2631
- else if (c === ")") parens -= 1;
2632
- else if (parens === 0 && c === "{") {
2633
- stack.push({
2634
- prelude: css.slice(segment, i).trim(),
2635
- rule: rules
3325
+ const visit = (items, context, rule) => {
3326
+ let segment = [];
3327
+ const take = () => {
3328
+ const declaration = declarationOf(css, segment);
3329
+ if (rule !== void 0 && declaration !== void 0) found.push({
3330
+ ...declaration,
3331
+ rule,
3332
+ context
2636
3333
  });
3334
+ segment = [];
3335
+ };
3336
+ for (const item of items) if (item.kind === "block" && item.open.type === "{") {
3337
+ const prelude = textOf(css, segment);
3338
+ segment = [];
2637
3339
  rules += 1;
2638
- segment = i + 1;
2639
- } else if (parens === 0 && c === ";") {
2640
- take(i + 1);
2641
- segment = i + 1;
2642
- } else if (parens === 0 && c === "}") {
2643
- take(i);
2644
- stack.pop();
2645
- segment = i + 1;
2646
- }
2647
- }
3340
+ visit(item.children, [...context, prelude], rules - 1);
3341
+ } else if (item.kind === "token" && item.token.type === ";") take();
3342
+ else segment.push(item);
3343
+ take();
3344
+ };
3345
+ visit(componentValues(tokenize(css)), [], void 0);
2648
3346
  return found;
2649
3347
  }
2650
- function isSelfReference(declaration) {
2651
- return declaration.property.startsWith("--") && declaration.value.replace(/\s+/g, "") === `var(${declaration.property})`;
2652
- }
2653
- /** Every `var()` read in `value`, nested fallbacks included. */
2654
- function varReads(value) {
2655
- const reads = [];
2656
- for (const match of value.matchAll(/var\(\s*(--[\w-]+)\s*/g)) {
2657
- const after = (match.index ?? 0) + match[0].length;
2658
- if (value[after] !== ",") {
2659
- reads.push({
2660
- name: match[1],
2661
- fallback: void 0
2662
- });
2663
- continue;
3348
+ /** The declaration a `;`-separated segment holds, if any. A bad string or
3349
+ * url makes a declaration invalid, so browsers drop it, and so does this. */
3350
+ function declarationOf(css, segment) {
3351
+ const colon = segment.findIndex((item) => item.kind === "token" && item.token.type === ":");
3352
+ const named = segment.slice(0, Math.max(colon, 0)).filter((i) => !isSpace(i));
3353
+ const values = trimSpace(segment.slice(colon + 1));
3354
+ if (colon === -1 || named.length === 0 || someValue(values, (v) => v.kind === "token" && (v.token.type === "bad-url" || v.token.type === "bad-string"))) return;
3355
+ const [only] = named;
3356
+ const ident = named.length === 1 && only.kind === "token" && only.token.type === "ident" ? only.token.value : void 0;
3357
+ const custom = ident?.startsWith("--") === true;
3358
+ return {
3359
+ property: custom && ident !== void 0 ? ident : (ident ?? textOf(css, named)).toLowerCase(),
3360
+ custom,
3361
+ value: values.length > 0 ? css.slice(values[0].start, values[values.length - 1].end) : "",
3362
+ values
3363
+ };
3364
+ }
3365
+ function trimSpace(items) {
3366
+ let start = 0;
3367
+ let end = items.length;
3368
+ while (start < end && isSpace(items[start])) start += 1;
3369
+ while (end > start && isSpace(items[end - 1])) end -= 1;
3370
+ return items.slice(start, end);
3371
+ }
3372
+ /** Whether the value is exactly `var()` of the declared property. */
3373
+ function isSelfReference(declaration, reads) {
3374
+ return declaration.custom && reads.length === 1 && reads[0].name === declaration.property && reads[0].fallback === void 0 && declaration.values.length === 1 && declaration.values[0].start === reads[0].start;
3375
+ }
3376
+ /** Whether a read of the declared property sits outside every other read's
3377
+ * fallback, as in `--a: var(--a, red)` or `--a: calc(var(--a) + 1px)`: cyclic
3378
+ * whatever the fallback, as the bare `var(--a)` is. */
3379
+ function readsItselfDirectly(declaration, reads) {
3380
+ return declaration.custom && reads.some((read) => read.name === declaration.property && isDirectRead(read, reads));
3381
+ }
3382
+ /** Whether `read` sits outside every other read's fallback, so it is taken
3383
+ * whenever the declaration is. */
3384
+ function isDirectRead(read, reads) {
3385
+ return !reads.some((outer) => outer !== read && outer.fallback !== void 0 && outer.fallback.start <= read.start && read.end <= outer.fallback.end);
3386
+ }
3387
+ /** Each custom property a `currentVar` writes, with the props writing it. */
3388
+ function currentVarWriters(propConfigJson) {
3389
+ const writers = /* @__PURE__ */ new Map();
3390
+ if (propConfigJson === void 0) return writers;
3391
+ const config = JSON.parse(propConfigJson);
3392
+ for (const [prop, entry] of Object.entries(config)) {
3393
+ if (!entry.currentVar) continue;
3394
+ writers.set(entry.currentVar, [...writers.get(entry.currentVar) ?? [], prop]);
3395
+ }
3396
+ return writers;
3397
+ }
3398
+ /**
3399
+ * Cycles among the `currentVar` writes of one rule, read directly. One rule
3400
+ * is one element under one set of conditions, so its writes are always
3401
+ * active together; writes in different rules, or a read only inside another
3402
+ * read's fallback, are left alone. Each cycle is listed once, in declaration
3403
+ * order.
3404
+ */
3405
+ function currentVarCycles(parsed, currentVars) {
3406
+ if (currentVars.size === 0) return [];
3407
+ const byRule = /* @__PURE__ */ new Map();
3408
+ for (const declaration of parsed) {
3409
+ if (!declaration.custom || !currentVars.has(declaration.property)) continue;
3410
+ const writes = byRule.get(declaration.rule) ?? /* @__PURE__ */ new Map();
3411
+ writes.set(declaration.property, declaration);
3412
+ byRule.set(declaration.rule, writes);
3413
+ }
3414
+ const cycles = [];
3415
+ for (const writes of byRule.values()) {
3416
+ const edges = /* @__PURE__ */ new Map();
3417
+ for (const [name, declaration] of writes) {
3418
+ const reads = variableReads(declaration.values);
3419
+ edges.set(name, reads.filter((read) => read.name !== name && writes.has(read.name) && isDirectRead(read, reads)).map((read) => read.name));
3420
+ }
3421
+ for (const component of stronglyConnected(edges)) {
3422
+ if (component.length < 2) continue;
3423
+ cycles.push([...writes.values()].filter((d) => component.includes(d.property)));
2664
3424
  }
2665
- let depth = 1;
2666
- let close = after + 1;
2667
- for (; close < value.length && depth > 0; close += 1) if (value[close] === "(") depth += 1;
2668
- else if (value[close] === ")") depth -= 1;
2669
- reads.push({
2670
- name: match[1],
2671
- fallback: value.slice(after + 1, close - 1).trim()
2672
- });
2673
3425
  }
2674
- return reads;
3426
+ return cycles;
3427
+ }
3428
+ /** Tarjan's strongly connected components of a small graph. */
3429
+ function stronglyConnected(edges) {
3430
+ const index = /* @__PURE__ */ new Map();
3431
+ const low = /* @__PURE__ */ new Map();
3432
+ const stack = [];
3433
+ const onStack = /* @__PURE__ */ new Set();
3434
+ const components = [];
3435
+ const visit = (node) => {
3436
+ index.set(node, index.size);
3437
+ low.set(node, index.get(node));
3438
+ stack.push(node);
3439
+ onStack.add(node);
3440
+ for (const next of edges.get(node) ?? []) if (!index.has(next)) {
3441
+ visit(next);
3442
+ low.set(node, Math.min(low.get(node), low.get(next)));
3443
+ } else if (onStack.has(next)) low.set(node, Math.min(low.get(node), index.get(next)));
3444
+ if (low.get(node) === index.get(node)) {
3445
+ const component = [];
3446
+ let member;
3447
+ do {
3448
+ member = stack.pop();
3449
+ onStack.delete(member);
3450
+ component.push(member);
3451
+ } while (member !== node);
3452
+ components.push(component);
3453
+ }
3454
+ };
3455
+ for (const node of edges.keys()) if (!index.has(node)) visit(node);
3456
+ return components;
3457
+ }
3458
+ /** Whether every read of the declared property sits inside another read's
3459
+ * fallback, as in `--a: var(--b, var(--a))`. */
3460
+ function readsItselfOnlyInFallback(declaration, reads) {
3461
+ if (!declaration.custom) return false;
3462
+ const own = reads.filter((read) => read.name === declaration.property);
3463
+ return own.length > 0 && own.every((read) => reads.some((outer) => outer.fallback !== void 0 && outer.fallback.start <= read.start && read.end <= outer.fallback.end));
2675
3464
  }
2676
3465
  /** Custom properties this declaration sets in a keyframe or names in a
2677
3466
  * transition. */
2678
3467
  function animatedProperties(declaration, discreteRules) {
2679
3468
  const keyframes = declaration.context.find((prelude) => prelude.startsWith("@keyframes"));
2680
- if (keyframes !== void 0 && declaration.property.startsWith("--")) return [{
3469
+ if (keyframes !== void 0 && declaration.custom) return [{
2681
3470
  name: declaration.property,
2682
3471
  how: `set in ${keyframes}`,
2683
3472
  discrete: false
2684
3473
  }];
2685
3474
  if (declaration.property !== "transition" && declaration.property !== "transition-property") return [];
2686
- return topLevelItems(declaration.value).flatMap((item) => {
2687
- const property = item.trim().split(/\s+/)[0] ?? "";
2688
- return property.startsWith("--") ? [{
2689
- name: property,
3475
+ return topLevelItems(declaration.values).flatMap((item) => {
3476
+ const named = item.find((value) => value.kind === "token" && value.token.type === "ident" && value.token.value.startsWith("--"));
3477
+ return named?.kind === "token" ? [{
3478
+ name: named.token.value,
2690
3479
  how: "transitioned",
2691
- discrete: /\ballow-discrete\b/.test(item) || discreteRules.has(declaration.rule)
3480
+ discrete: isDiscrete(item) || discreteRules.has(declaration.rule)
2692
3481
  }] : [];
2693
3482
  });
2694
3483
  }
2695
- function topLevelItems(value) {
2696
- const items = [];
2697
- let depth = 0;
2698
- let start = 0;
2699
- for (let i = 0; i < value.length; i += 1) if (value[i] === "(") depth += 1;
2700
- else if (value[i] === ")") depth -= 1;
2701
- else if (value[i] === "," && depth === 0) {
2702
- items.push(value.slice(start, i));
2703
- start = i + 1;
2704
- }
2705
- items.push(value.slice(start));
3484
+ function isDiscrete(item) {
3485
+ return item.some((value) => value.kind === "token" && value.token.type === "ident" && value.token.value.toLowerCase() === "allow-discrete");
3486
+ }
3487
+ /** The comma-separated items of a value, each its top-level values. */
3488
+ function topLevelItems(values) {
3489
+ const items = [[]];
3490
+ for (const value of values) if (value.kind === "token" && value.token.type === ",") items.push([]);
3491
+ else items[items.length - 1].push(value);
2706
3492
  return items;
2707
3493
  }
2708
3494
  /** Why a property cannot interpolate, or `null` when it can. */
@@ -2712,7 +3498,7 @@ function interpolationGap(registration) {
2712
3498
  return null;
2713
3499
  }
2714
3500
  function componentOwner(manifest) {
2715
- const components = Object.values(manifest.components);
3501
+ const components = Object.values(manifest.components ?? {});
2716
3502
  return (declaration) => {
2717
3503
  const selector = [...declaration.context].reverse().find((prelude) => !prelude.startsWith("@"));
2718
3504
  const classes = (selector ?? "").match(/\.-?[_a-zA-Z][\w-]*/g) ?? [];
@@ -2834,12 +3620,33 @@ function buildAnalysisInputs(opts) {
2834
3620
  externalDirsJson: opts.externalDirs?.length && hasSourceThemeManifests(opts.system) ? JSON.stringify(opts.externalDirs) : null
2835
3621
  };
2836
3622
  if (opts.system.declarationScalesJson) inputs.declarationScalesJson = opts.system.declarationScalesJson;
3623
+ const context = opts.analysisContext;
3624
+ if (context?.skippedSources?.length || context?.unbundledComputedImports || context?.packageDirs?.length) inputs.analysisContextJson = JSON.stringify({
3625
+ skippedSources: context.skippedSources ?? [],
3626
+ unbundledComputedImports: context.unbundledComputedImports ?? false,
3627
+ packageDirs: context.packageDirs ?? []
3628
+ });
2837
3629
  return inputs;
2838
3630
  }
2839
3631
  function hasSourceThemeManifests(system) {
2840
3632
  const json = system.sourceThemeManifestsJson ?? "";
2841
3633
  return json.length > 0 && json !== "{}";
2842
3634
  }
3635
+ /** Systems whose load diagnostics were surfaced: each loaded system reports
3636
+ * them once, not again on every analysis and hot update. The set is per
3637
+ * process. Next's webpack compilers share one analysis in one process, but
3638
+ * its build worker and Turbopack builds analyze in two processes, which
3639
+ * repeat every diagnostic alike, component ones included. */
3640
+ const surfacedSystems = /* @__PURE__ */ new WeakSet();
3641
+ /** The loaded system's own diagnostics, the first time it is analyzed. A
3642
+ * strict error keeps failing every analysis until the system changes. */
3643
+ function systemDiagnostics(system, strict) {
3644
+ const diagnostics = [...collectSelectorAliasDiagnostics(system.selectorAliasesJson), ...systemLoadDiagnostics(system)];
3645
+ const blocking = strict === true && diagnostics.some((d) => d.severity === "error");
3646
+ if (surfacedSystems.has(system) && !blocking) return [];
3647
+ surfacedSystems.add(system);
3648
+ return diagnostics;
3649
+ }
2843
3650
  /**
2844
3651
  * The one analysis invocation both plugins share. Error handling stays at
2845
3652
  * the call site (strict-mode throw vs warn).
@@ -2854,18 +3661,20 @@ function runProjectAnalysis(engineApi, opts) {
2854
3661
  const extractMs = Math.round(performance.now() - t);
2855
3662
  t = performance.now();
2856
3663
  const manifest = JSON.parse(manifestJson);
3664
+ const systemErrors = systemLoadDiagnostics(opts.system).filter((diagnostic) => diagnostic.kind === "error");
3665
+ if (systemErrors.length > 0) manifest.diagnostics = [...systemErrors, ...manifest.diagnostics ?? []];
3666
+ const componentCss = applyUnitFallback(manifest.css);
2857
3667
  const propertyDiagnostics = checkCustomProperties({
2858
3668
  system: opts.system,
2859
3669
  manifest,
2860
- componentCss: applyUnitFallback(manifest.css),
3670
+ componentCss,
2861
3671
  globalCss: manifest.sheets.global
2862
3672
  });
2863
3673
  surfaceManifestDiagnostics(manifest, opts.warn, {
2864
3674
  strict: opts.strict,
2865
3675
  info: opts.info,
2866
3676
  prepend: [
2867
- ...collectSelectorAliasDiagnostics(opts.system.selectorAliasesJson),
2868
- ...systemLoadDiagnostics(opts.system),
3677
+ ...systemDiagnostics(opts.system, opts.strict),
2869
3678
  ...propertyDiagnostics,
2870
3679
  ...opts.extraDiagnostics ?? []
2871
3680
  ]
@@ -2875,7 +3684,7 @@ function runProjectAnalysis(engineApi, opts) {
2875
3684
  manifest,
2876
3685
  manifestJson,
2877
3686
  globalCss: manifest.sheets.global,
2878
- componentCss: applyUnitFallback(manifest.css),
3687
+ componentCss,
2879
3688
  inputs,
2880
3689
  timings: {
2881
3690
  serializeMs,
@@ -2927,6 +3736,47 @@ function serializeStaticCss(config) {
2927
3736
  return json === "{}" ? null : json;
2928
3737
  }
2929
3738
  //#endregion
3739
+ //#region pipeline/property-names.ts
3740
+ function createPropertyNames(declared, prefix, themeNames = []) {
3741
+ const finals = /* @__PURE__ */ new Map();
3742
+ for (const name of [...new Set(declared)].sort()) finals.set(name, `${prefix}-${name}`);
3743
+ const aliases = /* @__PURE__ */ new Map();
3744
+ for (const [name, final] of finals) if (!finals.has(final)) aliases.set(final, name);
3745
+ const others = /* @__PURE__ */ new Map();
3746
+ for (const name of [...new Set(themeNames)].sort()) if (!finals.has(name)) others.set(name, `${prefix}-${name}`);
3747
+ return {
3748
+ identity: (spelling) => finals.has(spelling) ? spelling : aliases.get(spelling) ?? (others.has(spelling) ? spelling : void 0),
3749
+ finalName: (name) => finals.get(name) ?? others.get(name),
3750
+ ambiguous: () => [...others.keys()].filter((name) => aliases.has(name)).map((name) => ({
3751
+ name,
3752
+ contextual: aliases.get(name) ?? name
3753
+ }))
3754
+ };
3755
+ }
3756
+ /**
3757
+ * Gives every custom-property token that names a managed property its final
3758
+ * name, in one pass: declaration keys, `var()` reads at any depth (fallbacks
3759
+ * included), `@property` names, transition-property lists and style queries.
3760
+ * Names are read through the shared CSS tokenizer, whole and with escapes
3761
+ * decoded, escaped leading dashes included, so an undeclared identifier that
3762
+ * contains or starts with a managed name is never renamed in part. Quoted
3763
+ * strings, `url()` and comments are copied unchanged.
3764
+ */
3765
+ function renameCustomProperties(css, names) {
3766
+ let out = "";
3767
+ let copied = 0;
3768
+ for (const token of tokenize(css)) {
3769
+ if (token.type !== "ident" || !token.value.startsWith("--")) continue;
3770
+ const spelling = token.value.slice(2);
3771
+ const identity = names.identity(spelling);
3772
+ const final = identity === void 0 ? void 0 : names.finalName(identity);
3773
+ if (final === void 0 || final === spelling) continue;
3774
+ out += `${css.slice(copied, token.start)}--${final}`;
3775
+ copied = token.end;
3776
+ }
3777
+ return out + css.slice(copied);
3778
+ }
3779
+ //#endregion
2930
3780
  //#region pipeline/prefix.ts
2931
3781
  /** Prefix variable references without changing their matching rules. */
2932
3782
  function prefixVariableReferences(prefix, value) {
@@ -2960,6 +3810,61 @@ function applyPrefix(prefix, variableMapJson, variableCss, themeJson, contextual
2960
3810
  }
2961
3811
  return result;
2962
3812
  }
3813
+ /** The runtime's transport variables are `--animus-<prop>`. */
3814
+ const TRANSPORT_NAMESPACE = "animus-";
3815
+ /**
3816
+ * The prefix applied through one map: every variable the theme defines and
3817
+ * every declared contextual variable takes `--${prefix}-${name}`, each
3818
+ * reference renamed once. Each contextual entry carries its declared and
3819
+ * final name, so lookups run on the declared identity and the extractor
3820
+ * emits the final name.
3821
+ */
3822
+ function applyPropertyNames(prefix, artifacts) {
3823
+ const byScale = artifacts.contextualVarsJson ? parseInternalWire(artifacts.contextualVarsJson, "contextualVarsJson (the theme's contextual variable names)") : null;
3824
+ const declared = Object.values(byScale ?? {}).flat();
3825
+ const variableMap = parseInternalWire(artifacts.variableMapJson, "variableMapJson (the theme's token variables)");
3826
+ const managed = createPropertyNames(declared, prefix, [...Object.values(variableMap).map((name) => name.replace(/^--/, "")), ...[...artifacts.variableCss.matchAll(THEME_DEFINITION)].map((m) => m[1])]);
3827
+ const nameConflicts = [...[...new Set(declared)].flatMap((name) => {
3828
+ const final = managed.finalName(name) ?? name;
3829
+ return final.startsWith(TRANSPORT_NAMESPACE) ? [{
3830
+ name,
3831
+ final,
3832
+ reason: "transport"
3833
+ }] : [];
3834
+ }), ...managed.ambiguous().map(({ name, contextual }) => ({
3835
+ name,
3836
+ final: contextual,
3837
+ reason: "theme-variable"
3838
+ }))];
3839
+ const rename = (value) => renameCustomProperties(value, managed);
3840
+ return {
3841
+ variableMapJson: JSON.stringify(Object.fromEntries(Object.entries(variableMap).map(([key, name]) => [key, rename(name)]))),
3842
+ variableCss: rename(artifacts.variableCss),
3843
+ themeJson: renameTokenValues(artifacts.themeJson, rename),
3844
+ contextualVarsJson: byScale === null ? null : JSON.stringify(Object.fromEntries(Object.entries(byScale).map(([scale, names]) => [scale, names.map((name) => ({
3845
+ name,
3846
+ var: managed.finalName(name) ?? name
3847
+ }))]))),
3848
+ declarationScalesJson: artifacts.declarationScalesJson === null ? null : renameDeclarationRecords(artifacts.declarationScalesJson, rename),
3849
+ nameConflicts,
3850
+ contextualProperties: [...new Set(declared)].map((name) => `--${managed.finalName(name) ?? name}`)
3851
+ };
3852
+ }
3853
+ /** A custom property the theme's CSS defines or registers. */
3854
+ const THEME_DEFINITION = /(?:@property\s+|(?:^|[\s;{]))--([\w-]+)(?=\s*[:{])/g;
3855
+ /** Renames inside the token map's values. */
3856
+ function renameTokenValues(themeJson, rename) {
3857
+ const tokens = parseInternalWire(themeJson, "themeJson (the theme's flattened token values)");
3858
+ return JSON.stringify(Object.fromEntries(Object.entries(tokens).map(([path, value]) => [path, rename(value)])));
3859
+ }
3860
+ /** Renames inside every declaration-scale record value. */
3861
+ function renameDeclarationRecords(declarationScalesJson, rename) {
3862
+ const scales = parseInternalWire(declarationScalesJson, "declarationScalesJson (the theme's declaration scales)");
3863
+ return JSON.stringify(Object.fromEntries(Object.entries(scales).map(([name, scale]) => [name, {
3864
+ ...scale,
3865
+ values: Object.fromEntries(Object.entries(scale.values).map(([key, record]) => [key, Object.fromEntries(Object.entries(record).map(([property, value]) => [property, rename(value)]))]))
3866
+ }])));
3867
+ }
2963
3868
  //#endregion
2964
3869
  //#region pipeline/system-config.ts
2965
3870
  /**
@@ -2975,7 +3880,28 @@ function loadSystemConfig(engineApi, opts) {
2975
3880
  let variableCss = registrations.css;
2976
3881
  let contextualVarsJson = config.contextualVarsJson || null;
2977
3882
  let declarationScalesJson = config.declarationScalesJson || null;
2978
- if (opts.prefix) {
3883
+ const declaredNames = Object.values(parseInternalWire(contextualVarsJson ?? "{}", "contextualVarsJson (the theme's contextual variable names)")).flat();
3884
+ let contextualProperties = declaredNames.map((name) => `--${name}`);
3885
+ let legacyPrefixedContextualVars = [];
3886
+ let prefixNameConflicts = [];
3887
+ if (opts.prefix && opts.prefixContextualVars) {
3888
+ const resolved = applyPropertyNames(opts.prefix, {
3889
+ variableMapJson,
3890
+ variableCss,
3891
+ themeJson: scalesJson,
3892
+ contextualVarsJson,
3893
+ declarationScalesJson
3894
+ });
3895
+ variableMapJson = resolved.variableMapJson;
3896
+ variableCss = resolved.variableCss;
3897
+ scalesJson = resolved.themeJson;
3898
+ contextualVarsJson = resolved.contextualVarsJson;
3899
+ contextualProperties = resolved.contextualProperties;
3900
+ prefixNameConflicts = resolved.nameConflicts;
3901
+ declarationScalesJson = resolved.declarationScalesJson;
3902
+ } else if (opts.prefix) {
3903
+ legacyPrefixedContextualVars = declaredNames;
3904
+ contextualProperties = declaredNames.map((name) => `--${opts.prefix}-${name}`);
2979
3905
  const prefixed = applyPrefix(opts.prefix, variableMapJson, variableCss, scalesJson, contextualVarsJson || void 0);
2980
3906
  variableMapJson = prefixed.variableMapJson;
2981
3907
  variableCss = prefixed.variableCss;
@@ -2990,6 +3916,7 @@ function loadSystemConfig(engineApi, opts) {
2990
3916
  variableMapJson,
2991
3917
  variableCss,
2992
3918
  contextualVarsJson,
3919
+ contextualProperties: [...new Set(contextualProperties)],
2993
3920
  selectorAliasesJson: config.selectorAliases || null,
2994
3921
  conditionAliasesJson: config.conditionAliases || null,
2995
3922
  transformSourcesJson: config.transformSources || null,
@@ -3003,6 +3930,8 @@ function loadSystemConfig(engineApi, opts) {
3003
3930
  if (declarationScalesJson) system.declarationScalesJson = declarationScalesJson;
3004
3931
  if (config.propertyRecordsJson) system.propertyRecordsJson = config.propertyRecordsJson;
3005
3932
  if (registrations.invalid.length > 0) system.invalidPropertyRegistrations = registrations.invalid;
3933
+ if (prefixNameConflicts.length > 0) system.prefixNameConflicts = prefixNameConflicts;
3934
+ if (legacyPrefixedContextualVars.length > 0) system.legacyPrefixedContextualVars = [...new Set(legacyPrefixedContextualVars)];
3006
3935
  return system;
3007
3936
  }
3008
3937
  //#endregion
@@ -3013,7 +3942,7 @@ function runStructuralSelfCheck(input) {
3013
3942
  else if (input.assembledCss !== void 0 ? !/@layer\s+anm-base\s*\{/.test(input.assembledCss) : input.componentCss.trim().length === 0) failures.push(`${input.componentCount} component(s) discovered but the emitted component CSS is empty`);
3014
3943
  for (const { specifier, outcome } of input.externalOutcomes ?? []) if (outcome === "empty") failures.push(`include '${specifier}' resolved but discovered no component sources`);
3015
3944
  else if (outcome === "unresolvable") failures.push(`include '${specifier}' could not be resolved`);
3016
- if (!input.variableCss.includes(":root")) failures.push("No :root variable block found in variable CSS");
3945
+ if ((input.variableMapJson == null || Object.keys(parseInternalWire(input.variableMapJson, "variableMapJson (the theme's token → variable map)")).length > 0) && !input.variableCss.includes(":root")) failures.push("No :root variable block found in variable CSS");
3017
3946
  if ((input.assembledCss ?? `${input.variableCss}\n${input.globalCss}\n${input.componentCss}`).includes("__TRANSFORM__")) failures.push("Unresolved __TRANSFORM__ placeholders found in CSS output");
3018
3947
  if (input.componentCount > 0 && (input.componentCss.length > 0 || input.assembledCss !== void 0)) {
3019
3948
  const assembled = input.assembledCss ?? assembleStylesheet({
@@ -3022,9 +3951,16 @@ function runStructuralSelfCheck(input) {
3022
3951
  globalCss: input.globalCss,
3023
3952
  componentCss: input.componentCss
3024
3953
  });
3025
- const baseIdx = assembled.search(/@layer\s+anm-base\s*\{/);
3026
- const variantsIdx = assembled.search(/@layer\s+anm-variants\s*\{/);
3027
- if (baseIdx !== -1 && variantsIdx !== -1 && baseIdx >= variantsIdx) failures.push(`CSS layer ordering violated — @layer anm-base (offset ${baseIdx}) must precede @layer anm-variants (offset ${variantsIdx})`);
3954
+ const blocks = ANIMUS_LAYERS.map((layer) => ({
3955
+ layer,
3956
+ offset: assembled.search(new RegExp(`@layer\\s+${layer}\\s*\\{`))
3957
+ })).filter(({ offset }) => offset !== -1);
3958
+ const misplaced = blocks.findIndex(({ offset }, index) => index > 0 && offset <= blocks[index - 1].offset);
3959
+ if (misplaced !== -1) {
3960
+ const earlier = blocks[misplaced - 1];
3961
+ const later = blocks[misplaced];
3962
+ failures.push(`CSS layer ordering violated — @layer ${earlier.layer} (offset ${earlier.offset}) must precede @layer ${later.layer} (offset ${later.offset})`);
3963
+ }
3028
3964
  }
3029
3965
  return failures;
3030
3966
  }
@@ -3056,6 +3992,7 @@ function buildDynamicPropConfig(dynamicProps) {
3056
3992
  if (meta.negative) entry.negative = true;
3057
3993
  if (meta.strict) entry.strict = true;
3058
3994
  if (meta.keywords?.length) entry.keywords = meta.keywords;
3995
+ if (meta.currentVar) entry.currentVar = meta.currentVar;
3059
3996
  configEntries[propName] = entry;
3060
3997
  }
3061
3998
  return configEntries;
@@ -3086,188 +4023,6 @@ function buildSystemPropsModule(opts) {
3086
4023
  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` : "");
3087
4024
  }
3088
4025
  //#endregion
3089
- //#region pipeline/tsconfig-paths.ts
3090
- /**
3091
- * tsconfig `paths` → alias pairs, for drivers with no live bundler config.
3092
- * The nearest config declaring `paths` wins wholesale — TS never merges.
3093
- */
3094
- function stripJsonc(text) {
3095
- let out = "";
3096
- let inString = false;
3097
- let inLine = false;
3098
- let inBlock = false;
3099
- for (let i = 0; i < text.length; i++) {
3100
- const ch = text[i];
3101
- const next = text[i + 1];
3102
- if (inLine) {
3103
- if (ch === "\n") {
3104
- inLine = false;
3105
- out += ch;
3106
- }
3107
- continue;
3108
- }
3109
- if (inBlock) {
3110
- if (ch === "*" && next === "/") {
3111
- inBlock = false;
3112
- i++;
3113
- }
3114
- continue;
3115
- }
3116
- if (inString) {
3117
- out += ch;
3118
- if (ch === "\\") {
3119
- out += next ?? "";
3120
- i++;
3121
- } else if (ch === "\"") inString = false;
3122
- continue;
3123
- }
3124
- if (ch === "\"") {
3125
- inString = true;
3126
- out += ch;
3127
- continue;
3128
- }
3129
- if (ch === "/" && next === "/") {
3130
- inLine = true;
3131
- i++;
3132
- continue;
3133
- }
3134
- if (ch === "/" && next === "*") {
3135
- inBlock = true;
3136
- i++;
3137
- continue;
3138
- }
3139
- out += ch;
3140
- }
3141
- return out.replace(/,(\s*[}\]])/g, "$1");
3142
- }
3143
- /** `Object(value) === value` holds for exactly the blocks and lists
3144
- * `JSON.parse` produces; the `[object Object]` tag separates the two. */
3145
- function isJsonBlock(value) {
3146
- return Object(value) === value && Object.prototype.toString.call(value) === "[object Object]";
3147
- }
3148
- /** A block or a list. A declared `paths` OWNS the setting and blocks its
3149
- * parents' even when it contributes nothing usable. */
3150
- function isJsonKeyed(value) {
3151
- return Object(value) === value;
3152
- }
3153
- /** A JSON string, excluding the boxed `String` object, which no path join
3154
- * accepts. */
3155
- function isJsonString(value) {
3156
- return Object(value) !== value && Object.prototype.toString.call(value) === "[object String]";
3157
- }
3158
- function decodePaths(value) {
3159
- if (!isJsonKeyed(value)) return null;
3160
- const decoded = /* @__PURE__ */ new Map();
3161
- for (const [pattern, targets] of Object.entries(value)) {
3162
- const [first] = Array.isArray(targets) ? targets : [];
3163
- if (isJsonString(first)) decoded.set(pattern, first);
3164
- }
3165
- return decoded;
3166
- }
3167
- /** TypeScript accepts one specifier or an array; a non-string member names
3168
- * no config and is dropped here. */
3169
- function decodeExtends(value) {
3170
- return (Array.isArray(value) ? value : [value]).filter(isJsonString);
3171
- }
3172
- function readConfig(path) {
3173
- let raw;
3174
- try {
3175
- raw = readFileSync(path, "utf-8");
3176
- } catch {
3177
- return null;
3178
- }
3179
- let parsed;
3180
- try {
3181
- parsed = JSON.parse(stripJsonc(raw));
3182
- } catch {
3183
- return null;
3184
- }
3185
- const root = isJsonBlock(parsed) ? parsed : {};
3186
- const compilerOptions = isJsonBlock(root.compilerOptions) ? root.compilerOptions : {};
3187
- return {
3188
- dir: dirname$1(path),
3189
- baseUrl: isJsonString(compilerOptions.baseUrl) ? compilerOptions.baseUrl : null,
3190
- paths: decodePaths(compilerOptions.paths),
3191
- extends: decodeExtends(root.extends)
3192
- };
3193
- }
3194
- function resolveExtendsTarget(specifier, fromDir) {
3195
- if (specifier.startsWith(".") || isAbsolute(specifier)) {
3196
- const base = resolve(fromDir, specifier);
3197
- for (const candidate of [
3198
- base,
3199
- `${base}.json`,
3200
- join$1(base, "tsconfig.json")
3201
- ]) if (existsSync(candidate)) return candidate;
3202
- return null;
3203
- }
3204
- try {
3205
- return __require.resolve(specifier.endsWith(".json") ? specifier : `${specifier}/tsconfig.json`, { paths: [fromDir] });
3206
- } catch {
3207
- try {
3208
- return __require.resolve(specifier, { paths: [fromDir] });
3209
- } catch {
3210
- return null;
3211
- }
3212
- }
3213
- }
3214
- /** Child-first flattened extends chain starting at `entryPath`. */
3215
- function loadChain(entryPath) {
3216
- const chain = [];
3217
- const visited = /* @__PURE__ */ new Set();
3218
- const queue = [entryPath];
3219
- while (queue.length > 0) {
3220
- const path = queue.shift();
3221
- const key = resolve(path);
3222
- if (visited.has(key)) continue;
3223
- visited.add(key);
3224
- const node = readConfig(key);
3225
- if (!node) continue;
3226
- chain.push(node);
3227
- for (const parent of node.extends) {
3228
- const resolved = resolveExtendsTarget(parent, node.dir);
3229
- if (resolved) queue.push(resolved);
3230
- }
3231
- }
3232
- return chain;
3233
- }
3234
- /**
3235
- * The project's tsconfig path aliases as `buildPathAliasesJson` pairs; empty
3236
- * when no readable tsconfig, or no usable `paths`, exists.
3237
- */
3238
- function readTsconfigAliasPairs(rootDir) {
3239
- const chain = loadChain(join$1(rootDir, "tsconfig.json"));
3240
- if (chain.length === 0) return [];
3241
- const pathsOwner = chain.find((node) => node.paths !== null);
3242
- const paths = pathsOwner?.paths ?? null;
3243
- if (pathsOwner === void 0 || paths === null) return [];
3244
- const baseOwner = chain.find((node) => node.baseUrl !== null);
3245
- const baseUrl = baseOwner?.baseUrl ?? null;
3246
- const base = baseOwner !== void 0 && baseUrl !== null ? resolve(baseOwner.dir, baseUrl) : pathsOwner.dir;
3247
- const pairs = [];
3248
- for (const [pattern, target] of paths) {
3249
- const patternStars = pattern.split("*").length - 1;
3250
- const targetStars = target.split("*").length - 1;
3251
- if (pattern === "*" || patternStars > 1 || targetStars > 1) continue;
3252
- if (pattern.endsWith("/*")) {
3253
- if (!target.endsWith("/*")) continue;
3254
- pairs.push({
3255
- pattern: pattern.slice(0, -2),
3256
- target: resolve(base, target.slice(0, -2)),
3257
- kind: "prefix"
3258
- });
3259
- continue;
3260
- }
3261
- if (patternStars > 0 || targetStars > 0) continue;
3262
- pairs.push({
3263
- pattern,
3264
- target: resolve(base, target),
3265
- kind: "exact"
3266
- });
3267
- }
3268
- return pairs;
3269
- }
3270
- //#endregion
3271
4026
  //#region pipeline/error-diagnostics.ts
3272
4027
  function assertNoErrorDiagnostics(diagnostics) {
3273
4028
  const errors = (diagnostics ?? []).filter((diagnostic) => diagnostic.kind === "error");
@@ -3352,4 +4107,4 @@ function realpathThroughNearestExistingAncestor(path) {
3352
4107
  }
3353
4108
  }
3354
4109
  //#endregion
3355
- export { compareDiscoveryOrder as $, buildPathAliasesJson as A, collectExternalPackageSources as B, surfaceManifestDiagnostics as C, contentHash as Ct, vocabularyWitnessDiagnostics as D, stripLeadingLayerDeclaration as Dt, unresolvedParentName as E, assembleStylesheet as Et, findSheetAssetSpecifiers as F, isExcludedPackageRelativePath as G, extractSystemFilePackages as H, reportSurvivingAssetPlaceholders as I, unresolvableIncludesMessage as J, resolveAbsolutePathSpecifier as K, substituteAssetPlaceholders as L, resolveAssetFile as M, resolveThroughPathAliases as N, postProcessCss as O, validateLayerOrder as Ot, findAssetSpecifiers as P, sharesVolumeRoot as Q, substituteSheetAssets as R, isUnresolvedParentDrop as S, preprocessMdx as St, unreadableSourceDiagnostic as T, ANIMUS_LAYERS as Tt, findPackageRoot as U, excludeCollectedPackages as V, firstOwners as W, createSourceIdentity as X, walkPackageSources as Y, isPathWithinRoot as Z, runProjectAnalysis as _, projectExternalFileOwners as _t, assertNoErrorDiagnostics as a, REPLACEABLE_DEFAULT_EXCLUDE as at, VOCABULARY_COLLISION as b, ENGINE_TRANSFORM_EXTENSIONS as bt, buildDynamicPropConfig as c, createExcludeMatcher as ct, applyPrefix as d, RETIRED_ENGINE_MESSAGE as dt, discoverFiles as et, serializeStaticCss as f, assertNoRetiredEngineSelection as ft, clearEngineCache as g, parseFilesJson as gt, buildAnalysisInputs as h, isAdvisorySourceDiagnostic as ht, snapshotFilePlans as i, DRIVER_NAMESPACES as it, enforceExternalTokenContracts as j, resolveLightningTargets as k, __require as kt, runStructuralSelfCheck as l, resolveMode as lt, stableStringify as m, ingestSourceEntries as mt, diffFilePlans as n, CORE_OPTION_KEYS as nt, readTsconfigAliasPairs as o, STRUCTURAL_EXCLUDE as ot, camelToKebab as p, createV2EngineApi as pt, staleDistIncludesMessage as q, hashReplacementPlans as r, DEFAULT_EXCLUDE as rt, buildSystemPropsModule as s, assertKnownOptionKeys as st, toWatchKeys as t, AnimusConfigError as tt, loadSystemConfig as u, resolveVerbosity as ut, applyUnitFallback as v, adaptSvelteSource as vt, systemLoadDiagnostics as w, buildAnalyzeProjectArgs as wt, VOCABULARY_LEGACY_VERB as x, isEngineTransformExtension as xt, INVALID_PROPERTY_REGISTRATION as y, DEFAULT_EXTENSIONS as yt, createSourceCorpus as z };
4110
+ export { DRIVER_NAMESPACES as $, createSourceCorpus as A, contentHash as At, unresolvableIncludesMessage as B, resolveThroughPathAliases as C, noKitFilesDiagnostics as Ct, reportSurvivingAssetPlaceholders as D, unresolvedParentName as Dt, generatedModuleCode as E, unreadableSourceDiagnostic as Et, findPackageRoot as F, validateLayerOrder as Ft, createSourceIdentity as G, isJsonBlock as H, firstOwners as I, __require as It, compareDiscoveryOrder as J, isPathWithinRoot as K, isExcludedPackageRelativePath as L, engineImportParser as M, ANIMUS_LAYERS as Mt, excludeCollectedPackages as N, assembleStylesheet as Nt, substituteAssetPlaceholders as O, vocabularyWitnessDiagnostics as Ot, extractSystemFilePackages as P, stripLeadingLayerDeclaration as Pt, DEFAULT_EXCLUDE as Q, resolveAbsolutePathSpecifier as R, resolveAssetFile as S, isUnresolvedParentDrop as St, findSheetAssetSpecifiers as T, systemLoadDiagnostics as Tt, isJsonString as U, walkPackageSources as V, readTsconfigAliasPairs as W, AnimusConfigError as X, discoverFiles as Y, CORE_OPTION_KEYS as Z, applyUnitFallback as _, preprocessMdx as _t, assertNoErrorDiagnostics as a, resolveVerbosity as at, buildPathAliasesJson as b, VOCABULARY_LEGACY_VERB as bt, runStructuralSelfCheck as c, createV2EngineApi as ct, serializeStaticCss as d, parseFilesJson as dt, REPLACEABLE_DEFAULT_EXCLUDE as et, camelToKebab as f, projectExternalFileOwners as ft, runProjectAnalysis as g, isEngineTransformExtension as gt, clearEngineCache as h, ENGINE_TRANSFORM_EXTENSIONS as ht, snapshotFilePlans as i, resolveMode as it, collectExternalPackageSources as j, buildAnalyzeProjectArgs as jt, substituteSheetAssets as k, parseInternalWire as kt, loadSystemConfig as l, ingestSourceEntries as lt, buildAnalysisInputs as m, DEFAULT_EXTENSIONS as mt, diffFilePlans as n, assertKnownOptionKeys as nt, buildSystemPropsModule as o, RETIRED_ENGINE_MESSAGE as ot, stableStringify as p, adaptSvelteSource as pt, sharesVolumeRoot as q, hashReplacementPlans as r, createExcludeMatcher as rt, buildDynamicPropConfig as s, assertNoRetiredEngineSelection as st, toWatchKeys as t, STRUCTURAL_EXCLUDE as tt, applyPrefix as u, isAdvisorySourceDiagnostic as ut, postProcessCss as v, INVALID_PROPERTY_REGISTRATION as vt, findAssetSpecifiers as w, surfaceManifestDiagnostics as wt, enforceExternalTokenContracts as x, isDeletedSource as xt, resolveLightningTargets as y, VOCABULARY_COLLISION as yt, staleDistIncludesMessage as z };