@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
@@ -32,6 +32,11 @@ browserslist = __toESM(browserslist);
32
32
  let lightningcss = require("lightningcss");
33
33
  let _animus_ui_properties = require("@animus-ui/properties");
34
34
  //#region pipeline/assemble-stylesheet.ts
35
+ /**
36
+ * The cascade layer order, outermost first: its one definition. The engine
37
+ * reads it from `crates/extract-v2/src/layer_order.json`, which
38
+ * `scripts/layer-order.ts --write` writes from here.
39
+ */
35
40
  const ANIMUS_LAYERS = [
36
41
  "anm-global",
37
42
  "anm-base",
@@ -105,7 +110,8 @@ function buildAnalyzeProjectArgs(inputs) {
105
110
  inputs.transformSourcesJson,
106
111
  inputs.transformProvenanceJson ?? null
107
112
  ];
108
- if (inputs.declarationScalesJson) args.push(inputs.declarationScalesJson);
113
+ if (inputs.declarationScalesJson || inputs.analysisContextJson) args.push(inputs.declarationScalesJson);
114
+ if (inputs.analysisContextJson) args.push(inputs.analysisContextJson);
109
115
  return args;
110
116
  }
111
117
  //#endregion
@@ -125,6 +131,282 @@ function parseInternalWire(json, wire) {
125
131
  return parsed;
126
132
  }
127
133
  //#endregion
134
+ //#region pipeline/manifest-diagnostics.ts
135
+ /** Stable code for selector forms with no substitutable subject; the
136
+ * extraction engine mints the same string. */
137
+ const SELECTOR_UNSUPPORTED_SUBJECT = "animus.selector.unsupported-subject";
138
+ /** The one matcher for the engine's unresolved-parent chain-drop message;
139
+ * consumers match through the exported helpers, never their own copy. */
140
+ const UNRESOLVED_PARENT_RE = /chain dropped: could not resolve parent component '([^']+)'/;
141
+ function isUnresolvedParentDrop(diagnostic) {
142
+ return UNRESOLVED_PARENT_RE.test(diagnostic.message);
143
+ }
144
+ function unresolvedParentName(diagnostic) {
145
+ return UNRESOLVED_PARENT_RE.exec(diagnostic.message)?.[1] ?? null;
146
+ }
147
+ /** True when the selector carries at least one substitutable `&` subject
148
+ * outside quoted text. */
149
+ function hasSelectorSubject(value) {
150
+ let quote = null;
151
+ let escaped = false;
152
+ for (const c of value) {
153
+ if (escaped) {
154
+ escaped = false;
155
+ continue;
156
+ }
157
+ if (c === "\\") {
158
+ escaped = true;
159
+ continue;
160
+ }
161
+ if (quote !== null) {
162
+ if (c === quote) quote = null;
163
+ } else if (c === "\"" || c === "'") quote = c;
164
+ else if (c === "&") return true;
165
+ }
166
+ return false;
167
+ }
168
+ /**
169
+ * Coded diagnostics for selector-shaped alias values with no substitutable
170
+ * subject; alias values never reach the evaluator's own key guard.
171
+ */
172
+ function collectSelectorAliasDiagnostics(selectorAliasesJson) {
173
+ if (!selectorAliasesJson) return [];
174
+ const aliases = parseInternalWire(selectorAliasesJson, "selectorAliasesJson (the system loader's selector-alias registry)");
175
+ const diagnostics = [];
176
+ for (const [name, value] of Object.entries(aliases)) if (value.includes("&") && !hasSelectorSubject(value)) diagnostics.push({
177
+ file: "system",
178
+ component: name,
179
+ kind: "warn",
180
+ message: `selector alias '${name}' value '${value}' has no substitutable '&' subject outside quoted text (${SELECTOR_UNSUPPORTED_SUBJECT})`,
181
+ code: SELECTOR_UNSUPPORTED_SUBJECT,
182
+ severity: severityFor(SELECTOR_UNSUPPORTED_SUBJECT)
183
+ });
184
+ return diagnostics;
185
+ }
186
+ const UNREADABLE_SOURCE_FILE = "animus.ingestion.unreadable-source-file";
187
+ /** A kit the system extends resolved, yet discovery found none of its files. */
188
+ const NO_KIT_FILES = "animus.discovery.no-kit-files";
189
+ /** What a skipped source file costs: the analysis never sees what it
190
+ * renders. */
191
+ 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";
192
+ /**
193
+ * A configured source file that could not be read is a LOST INPUT: the
194
+ * emitted artifacts silently lack whatever that file declared.
195
+ */
196
+ /** Whether a failed source read means the file was deleted after discovery
197
+ * (an editor's delete-and-recreate, a branch switch): a deletion, not lost
198
+ * input. */
199
+ function isDeletedSource(error) {
200
+ return error?.code === "ENOENT";
201
+ }
202
+ function unreadableSourceDiagnostic(relPath, error) {
203
+ return {
204
+ file: relPath,
205
+ component: "source",
206
+ kind: "warn",
207
+ message: `configured source file ${relPath} could not be read: ${String(error)} (${SKIPPED_SOURCE_COST})`,
208
+ code: UNREADABLE_SOURCE_FILE,
209
+ severity: severityFor(UNREADABLE_SOURCE_FILE)
210
+ };
211
+ }
212
+ /** One warning per kit the system extends that resolved, yet yielded no
213
+ * files: none of its components are extracted. */
214
+ function noKitFilesDiagnostics(outcomes) {
215
+ return outcomes.filter((record) => record.outcome === "empty").map((record) => ({
216
+ file: record.specifier,
217
+ component: "kit",
218
+ kind: "warn",
219
+ 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",
220
+ code: NO_KIT_FILES,
221
+ severity: severityFor(NO_KIT_FILES)
222
+ }));
223
+ }
224
+ /** The collision entry code minted by the system package's merge. */
225
+ const VOCABULARY_COLLISION = "animus.vocabulary.collision";
226
+ /** A theme `@property` registration browsers would ignore; it is not emitted. */
227
+ const INVALID_PROPERTY_REGISTRATION = "animus.theme.invalid-property-registration";
228
+ /** Minted when a sealed kit with registered vocabulary is consumed through
229
+ * the deprecated `from()`/`includes:` verbs. */
230
+ const VOCABULARY_LEGACY_VERB = "animus.vocabulary.legacy-verb";
231
+ /** A `var()` fallback that never applies: the property is registered with an
232
+ * initial value. */
233
+ const PROPERTY_FALLBACK_SUPPRESSED = "animus.property.fallback-suppressed";
234
+ /** The same, where the fallback heads a chain of further `var()` reads. */
235
+ const PROPERTY_FALLBACK_CHAIN_SUPPRESSED = "animus.property.fallback-chain-suppressed";
236
+ /** A declared property set in keyframes or transitioned without a typed
237
+ * registration, so it does not interpolate. */
238
+ const PROPERTY_UNREGISTERED_ANIMATION = "animus.property.unregistered-animation";
239
+ /** A custom property whose resolved value is `var()` of itself, which makes
240
+ * it invalid at computed-value time. */
241
+ const PROPERTY_SELF_REFERENCE = "animus.property.self-reference";
242
+ /** A custom property that reads itself only inside another `var()`'s
243
+ * fallback: cyclic wherever that fallback is used. */
244
+ const PROPERTY_FALLBACK_SELF_REFERENCE = "animus.property.fallback-self-reference";
245
+ /** Custom properties that `currentVar` writes in one rule, each reading
246
+ * another directly, so every property in the cycle is invalid at
247
+ * computed-value time. */
248
+ const PROPERTY_CURRENT_VAR_CYCLE = "animus.property.current-var-cycle";
249
+ /** A prefix renamed contextual variables without `prefixContextualVars`, so
250
+ * their declared names no longer resolve. */
251
+ const PREFIX_CONTEXTUAL_VARS_UNPREFIXED = "animus.prefix.contextual-vars-unprefixed";
252
+ /** Under `prefixContextualVars`, a final name that collides with a runtime
253
+ * transport variable or a theme variable of the same spelling. */
254
+ const PREFIX_NAME_CONFLICT = "animus.prefix.name-conflict";
255
+ /** A contextual variable declared on a scale that also holds a token of its
256
+ * name: scale reads resolve to the token. */
257
+ const PROPERTY_LEGACY_TOKEN_COLLISION = "animus.property.legacy-token-collision";
258
+ /** A theme property declared on a scale that also holds a token of its
259
+ * name. */
260
+ const PROPERTY_TOKEN_COLLISION = "animus.property.token-collision";
261
+ const DIAGNOSTIC_SEVERITY = /* @__PURE__ */ new Map([
262
+ [SELECTOR_UNSUPPORTED_SUBJECT, "error"],
263
+ [UNREADABLE_SOURCE_FILE, "error"],
264
+ [NO_KIT_FILES, "warn"],
265
+ [VOCABULARY_COLLISION, "warn"],
266
+ [VOCABULARY_LEGACY_VERB, "warn"],
267
+ [INVALID_PROPERTY_REGISTRATION, "error"],
268
+ [PROPERTY_FALLBACK_SUPPRESSED, "info"],
269
+ [PROPERTY_FALLBACK_CHAIN_SUPPRESSED, "warn"],
270
+ [PROPERTY_UNREGISTERED_ANIMATION, "info"],
271
+ [PROPERTY_SELF_REFERENCE, "warn"],
272
+ [PROPERTY_FALLBACK_SELF_REFERENCE, "warn"],
273
+ [PROPERTY_CURRENT_VAR_CYCLE, "warn"],
274
+ [PREFIX_CONTEXTUAL_VARS_UNPREFIXED, "warn"],
275
+ [PREFIX_NAME_CONFLICT, "error"],
276
+ [PROPERTY_LEGACY_TOKEN_COLLISION, "warn"],
277
+ [PROPERTY_TOKEN_COLLISION, "error"]
278
+ ]);
279
+ /** An unlisted code is `warn`: a witness kind from a newer system package
280
+ * must not fail the strict build of a host that predates its writer. */
281
+ function severityFor(code) {
282
+ return (code === void 0 ? void 0 : DIAGNOSTIC_SEVERITY.get(code)) ?? "warn";
283
+ }
284
+ /**
285
+ * The one mapper of a sealed system's witness entries for every host: the
286
+ * loader's evaluation host shims `console`, so the record is the channel.
287
+ */
288
+ function vocabularyWitnessDiagnostics(vocabularyWitnessesJson) {
289
+ if (!vocabularyWitnessesJson) return [];
290
+ const entries = parseInternalWire(vocabularyWitnessesJson, "vocabularyWitnessesJson (the sealed system's vocabulary witness record)");
291
+ const diagnostics = [];
292
+ for (const entry of entries) if (entry.code === "animus.vocabulary.collision") diagnostics.push({
293
+ file: "system",
294
+ component: entry.name ?? "keyframes",
295
+ kind: "warn",
296
+ message: `keyframes vocabulary "${entry.name}" is registered by both ${entry.loser} and ${entry.winner} — ${entry.winner} wins; rename one collection (${entry.code})`,
297
+ code: entry.code,
298
+ severity: severityFor(entry.code)
299
+ });
300
+ else if (entry.code === "animus.vocabulary.legacy-verb") diagnostics.push({
301
+ file: "system",
302
+ component: "keyframes",
303
+ kind: "warn",
304
+ 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})`,
305
+ code: entry.code,
306
+ severity: severityFor(entry.code)
307
+ });
308
+ else diagnostics.push({
309
+ file: "system",
310
+ component: "vocabulary",
311
+ kind: "warn",
312
+ 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})` : ""}`,
313
+ code: entry.code,
314
+ severity: severityFor(entry.code)
315
+ });
316
+ return diagnostics;
317
+ }
318
+ /** The diagnostics a loaded system's own records carry: vocabulary
319
+ * witnesses and invalid `@property` registrations. */
320
+ function systemLoadDiagnostics(system) {
321
+ const registrations = (system.invalidPropertyRegistrations ?? []).map(({ name, reason }) => ({
322
+ file: "system",
323
+ component: name,
324
+ kind: "warn",
325
+ 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})`,
326
+ code: INVALID_PROPERTY_REGISTRATION,
327
+ severity: severityFor(INVALID_PROPERTY_REGISTRATION)
328
+ }));
329
+ const prefixed = system.legacyPrefixedContextualVars ?? [];
330
+ const unprefixed = prefixed.length === 0 ? [] : [{
331
+ file: "system",
332
+ component: "prefix",
333
+ kind: "warn",
334
+ 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})`,
335
+ code: PREFIX_CONTEXTUAL_VARS_UNPREFIXED,
336
+ severity: severityFor(PREFIX_CONTEXTUAL_VARS_UNPREFIXED)
337
+ }];
338
+ const conflicts = (system.prefixNameConflicts ?? []).map(({ name, final, reason }) => ({
339
+ file: "system",
340
+ component: `--${name}`,
341
+ kind: "error",
342
+ 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})`,
343
+ code: PREFIX_NAME_CONFLICT,
344
+ severity: severityFor(PREFIX_NAME_CONFLICT)
345
+ }));
346
+ return [
347
+ ...vocabularyWitnessDiagnostics(system.vocabularyWitnessesJson),
348
+ ...registrations,
349
+ ...unprefixed,
350
+ ...conflicts,
351
+ ...tokenCollisionDiagnostics(system)
352
+ ];
353
+ }
354
+ /**
355
+ * Each declared property whose name is also a token on a scale it is
356
+ * declared on. A scale read of that key, a prop value or a `{scale.key}`
357
+ * reference, resolves to the token, so the property is unreachable through
358
+ * the scale. A legacy declaration warns and the token stays the winner; any
359
+ * other is an error in every mode.
360
+ */
361
+ function tokenCollisionDiagnostics(system) {
362
+ if (!system.propertyRecordsJson) return [];
363
+ const declared = parseInternalWire(system.propertyRecordsJson, "propertyRecordsJson (the theme's properties)").flatMap((record) => record.scales.map((scale) => ({
364
+ record,
365
+ scale,
366
+ path: `${scale}.${record.name}`
367
+ })));
368
+ if (declared.length === 0) return [];
369
+ const tokens = parseInternalWire(system.scalesJson, "scalesJson (the theme's scale tokens)");
370
+ return declared.filter(({ path }) => Object.hasOwn(tokens, path)).map(({ record, scale, path }) => {
371
+ const code = record.legacy ? PROPERTY_LEGACY_TOKEN_COLLISION : PROPERTY_TOKEN_COLLISION;
372
+ const declaration = record.legacy ? `the contextual variable ${record.name}, which declareContextualVars declares on ${scale},` : `the theme property ${record.name}, declared on ${scale},`;
373
+ return {
374
+ file: "system",
375
+ component: path,
376
+ kind: record.legacy ? "warn" : "error",
377
+ 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})`,
378
+ code,
379
+ severity: severityFor(code)
380
+ };
381
+ });
382
+ }
383
+ /**
384
+ * The single strict-escalation policy point for both extraction plugins:
385
+ * error-severity diagnostics throw together under `strict`, else warn.
386
+ */
387
+ function surfaceManifestDiagnostics(manifest, warn, policy = {}) {
388
+ const errors = [];
389
+ const diagnostics = policy.prepend?.length ? [...policy.prepend, ...manifest.diagnostics ?? []] : manifest.diagnostics ?? [];
390
+ for (const diagnostic of diagnostics) {
391
+ let line = null;
392
+ if (diagnostic.kind === "bail") line = `⚠ ${diagnostic.component} not extracted: ${diagnostic.message}`;
393
+ else if (diagnostic.kind === "skip") line = `⚠ ${diagnostic.component}: skipped ${diagnostic.message}`;
394
+ else if (diagnostic.kind === "warn") line = `${diagnostic.severity === "info" ? "ℹ" : "⚠"} ${diagnostic.file}: ${diagnostic.component}: ${diagnostic.message}`;
395
+ if (line === null) continue;
396
+ if (diagnostic.code && !diagnostic.message.includes(diagnostic.code)) line += ` [${diagnostic.code}]`;
397
+ if (diagnostic.severity === "info") {
398
+ policy.info?.(line);
399
+ continue;
400
+ }
401
+ if (policy.strict && diagnostic.severity === "error") {
402
+ errors.push(`${diagnostic.code ?? "error"} — ${diagnostic.component}: ${diagnostic.message}`);
403
+ continue;
404
+ }
405
+ warn(line);
406
+ }
407
+ if (errors.length > 0) throw new Error(`[animus] strict: ${errors.length} error diagnostic(s):\n${errors.join("\n")}`);
408
+ }
409
+ //#endregion
128
410
  //#region pipeline/mdx-preprocessor.ts
129
411
  /**
130
412
  * MDX compiled to scanner-consumable JSX, plus the extension sets every
@@ -1049,7 +1331,8 @@ function withoutInvalidOriginals(result, invalidOriginals) {
1049
1331
  ...result,
1050
1332
  originalEntries: result.originalEntries.filter((entry) => !invalidOriginals.has(entry.path)),
1051
1333
  analysisEntries: result.analysisEntries.filter((entry) => analysisPaths.has(entry.path)),
1052
- ownership
1334
+ ownership,
1335
+ skippedOriginals: [...invalidOriginals].sort()
1053
1336
  };
1054
1337
  }
1055
1338
  /**
@@ -1089,11 +1372,12 @@ function createSourceIngestor(host) {
1089
1372
  const invalidOriginals = new Set(fatal.map((diagnostic) => diagnostic.originalPath));
1090
1373
  if (diagnostics.length === 0) return invalidOriginals;
1091
1374
  if (host.strict() && fatal.length > 0) {
1092
- const lines = fatal.map((diagnostic) => `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}`);
1375
+ const lines = fatal.map((diagnostic) => `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message} (${SKIPPED_SOURCE_COST})`);
1093
1376
  throw new Error(`${host.prefix} ${lines.join(`\n${host.prefix} `)}`);
1094
1377
  }
1095
1378
  for (const diagnostic of diagnostics) {
1096
- const line = `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}`;
1379
+ const skipped = isAdvisorySourceDiagnostic(diagnostic) ? "" : ` (${SKIPPED_SOURCE_COST})`;
1380
+ const line = `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}${skipped}`;
1097
1381
  let warned = warnedByOriginal.get(diagnostic.originalPath);
1098
1382
  if (!warned) {
1099
1383
  warned = /* @__PURE__ */ new Set();
@@ -1128,7 +1412,7 @@ function createV2EngineApi(deps) {
1128
1412
  return {
1129
1413
  loadSystemModule: (...args) => native.loadSystemModule(...args),
1130
1414
  extractFacts: (filesJson) => native.extractFacts(filesJson),
1131
- analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson, transformSourcesJson, transformProvenanceJson, declarationScalesJson) => {
1415
+ analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson, transformSourcesJson, transformProvenanceJson, declarationScalesJson, analysisContextJson) => {
1132
1416
  const filesJson = deps.rehydrateFilesJson ? deps.rehydrateFilesJson(filesJsonRaw) : filesJsonRaw;
1133
1417
  const sent = /* @__PURE__ */ new Map();
1134
1418
  for (const entry of parseFilesJson(filesJson, label)) sent.set(entry.path, entry.source);
@@ -1155,6 +1439,7 @@ function createV2EngineApi(deps) {
1155
1439
  transformSourcesJson: transformSourcesJson ?? void 0,
1156
1440
  transformProvenanceJson: transformProvenanceJson ?? void 0,
1157
1441
  declarationScalesJson,
1442
+ analysisContextJson,
1158
1443
  devMode
1159
1444
  };
1160
1445
  const engine = new native.ExtractEngine(config);
@@ -1227,6 +1512,7 @@ const CORE_OPTION_KEYS = /* @__PURE__ */ new Set([
1227
1512
  "strict",
1228
1513
  "verbose",
1229
1514
  "prefix",
1515
+ "prefixContextualVars",
1230
1516
  "targets",
1231
1517
  "minify",
1232
1518
  "staticCss",
@@ -1320,6 +1606,11 @@ const CORE_VALUE_GATES = [
1320
1606
  ok: isString,
1321
1607
  expected: "a string"
1322
1608
  },
1609
+ {
1610
+ key: "prefixContextualVars",
1611
+ ok: isBoolean,
1612
+ expected: "a boolean"
1613
+ },
1323
1614
  {
1324
1615
  key: "targets",
1325
1616
  ok: isStringOrStringArray,
@@ -1581,13 +1872,195 @@ function createSourceIdentity(rootDir) {
1581
1872
  };
1582
1873
  }
1583
1874
  //#endregion
1584
- //#region pipeline/discover-packages.ts
1585
- function findPackageRoot(absEntryPath) {
1586
- let pkgRoot = (0, path.dirname)(absEntryPath);
1587
- while (pkgRoot !== (0, path.dirname)(pkgRoot) && !(0, fs.existsSync)((0, path.join)(pkgRoot, "package.json"))) pkgRoot = (0, path.dirname)(pkgRoot);
1588
- return pkgRoot;
1875
+ //#region pipeline/tsconfig-paths.ts
1876
+ /**
1877
+ * tsconfig `paths` → alias pairs, for drivers with no live bundler config.
1878
+ * The nearest config declaring `paths` wins wholesale — TS never merges.
1879
+ */
1880
+ function stripJsonc(text) {
1881
+ let out = "";
1882
+ let inString = false;
1883
+ let inLine = false;
1884
+ let inBlock = false;
1885
+ for (let i = 0; i < text.length; i++) {
1886
+ const ch = text[i];
1887
+ const next = text[i + 1];
1888
+ if (inLine) {
1889
+ if (ch === "\n") {
1890
+ inLine = false;
1891
+ out += ch;
1892
+ }
1893
+ continue;
1894
+ }
1895
+ if (inBlock) {
1896
+ if (ch === "*" && next === "/") {
1897
+ inBlock = false;
1898
+ i++;
1899
+ }
1900
+ continue;
1901
+ }
1902
+ if (inString) {
1903
+ out += ch;
1904
+ if (ch === "\\") {
1905
+ out += next ?? "";
1906
+ i++;
1907
+ } else if (ch === "\"") inString = false;
1908
+ continue;
1909
+ }
1910
+ if (ch === "\"") {
1911
+ inString = true;
1912
+ out += ch;
1913
+ continue;
1914
+ }
1915
+ if (ch === "/" && next === "/") {
1916
+ inLine = true;
1917
+ i++;
1918
+ continue;
1919
+ }
1920
+ if (ch === "/" && next === "*") {
1921
+ inBlock = true;
1922
+ i++;
1923
+ continue;
1924
+ }
1925
+ out += ch;
1926
+ }
1927
+ return out.replace(/,(\s*[}\]])/g, "$1");
1589
1928
  }
1590
- const PACKAGE_SRC_EXCLUDES = [
1929
+ /** `Object(value) === value` holds for exactly the blocks and lists
1930
+ * `JSON.parse` produces; the `[object Object]` tag separates the two. */
1931
+ function isJsonBlock(value) {
1932
+ return Object(value) === value && Object.prototype.toString.call(value) === "[object Object]";
1933
+ }
1934
+ /** A block or a list. A declared `paths` OWNS the setting and blocks its
1935
+ * parents' even when it contributes nothing usable. */
1936
+ function isJsonKeyed(value) {
1937
+ return Object(value) === value;
1938
+ }
1939
+ /** A JSON string, excluding the boxed `String` object, which no path join
1940
+ * accepts. */
1941
+ function isJsonString(value) {
1942
+ return Object(value) !== value && Object.prototype.toString.call(value) === "[object String]";
1943
+ }
1944
+ function decodePaths(value) {
1945
+ if (!isJsonKeyed(value)) return null;
1946
+ const decoded = /* @__PURE__ */ new Map();
1947
+ for (const [pattern, targets] of Object.entries(value)) {
1948
+ const [first] = Array.isArray(targets) ? targets : [];
1949
+ if (isJsonString(first)) decoded.set(pattern, first);
1950
+ }
1951
+ return decoded;
1952
+ }
1953
+ /** TypeScript accepts one specifier or an array; a non-string member names
1954
+ * no config and is dropped here. */
1955
+ function decodeExtends(value) {
1956
+ return (Array.isArray(value) ? value : [value]).filter(isJsonString);
1957
+ }
1958
+ function readConfig(path$4) {
1959
+ let raw;
1960
+ try {
1961
+ raw = (0, fs.readFileSync)(path$4, "utf-8");
1962
+ } catch {
1963
+ return null;
1964
+ }
1965
+ let parsed;
1966
+ try {
1967
+ parsed = JSON.parse(stripJsonc(raw));
1968
+ } catch {
1969
+ return null;
1970
+ }
1971
+ const root = isJsonBlock(parsed) ? parsed : {};
1972
+ const compilerOptions = isJsonBlock(root.compilerOptions) ? root.compilerOptions : {};
1973
+ return {
1974
+ dir: (0, path.dirname)(path$4),
1975
+ baseUrl: isJsonString(compilerOptions.baseUrl) ? compilerOptions.baseUrl : null,
1976
+ paths: decodePaths(compilerOptions.paths),
1977
+ extends: decodeExtends(root.extends)
1978
+ };
1979
+ }
1980
+ function resolveExtendsTarget(specifier, fromDir) {
1981
+ if (specifier.startsWith(".") || (0, path.isAbsolute)(specifier)) {
1982
+ const base = (0, path.resolve)(fromDir, specifier);
1983
+ for (const candidate of [
1984
+ base,
1985
+ `${base}.json`,
1986
+ (0, path.join)(base, "tsconfig.json")
1987
+ ]) if ((0, fs.existsSync)(candidate)) return candidate;
1988
+ return null;
1989
+ }
1990
+ try {
1991
+ return require.resolve(specifier.endsWith(".json") ? specifier : `${specifier}/tsconfig.json`, { paths: [fromDir] });
1992
+ } catch {
1993
+ try {
1994
+ return require.resolve(specifier, { paths: [fromDir] });
1995
+ } catch {
1996
+ return null;
1997
+ }
1998
+ }
1999
+ }
2000
+ /** Child-first flattened extends chain starting at `entryPath`. */
2001
+ function loadChain(entryPath) {
2002
+ const chain = [];
2003
+ const visited = /* @__PURE__ */ new Set();
2004
+ const queue = [entryPath];
2005
+ while (queue.length > 0) {
2006
+ const path$5 = queue.shift();
2007
+ const key = (0, path.resolve)(path$5);
2008
+ if (visited.has(key)) continue;
2009
+ visited.add(key);
2010
+ const node = readConfig(key);
2011
+ if (!node) continue;
2012
+ chain.push(node);
2013
+ for (const parent of node.extends) {
2014
+ const resolved = resolveExtendsTarget(parent, node.dir);
2015
+ if (resolved) queue.push(resolved);
2016
+ }
2017
+ }
2018
+ return chain;
2019
+ }
2020
+ /**
2021
+ * The project's tsconfig path aliases as `buildPathAliasesJson` pairs; empty
2022
+ * when no readable tsconfig, or no usable `paths`, exists.
2023
+ */
2024
+ function readTsconfigAliasPairs(rootDir) {
2025
+ const chain = loadChain((0, path.join)(rootDir, "tsconfig.json"));
2026
+ if (chain.length === 0) return [];
2027
+ const pathsOwner = chain.find((node) => node.paths !== null);
2028
+ const paths = pathsOwner?.paths ?? null;
2029
+ if (pathsOwner === void 0 || paths === null) return [];
2030
+ const baseOwner = chain.find((node) => node.baseUrl !== null);
2031
+ const baseUrl = baseOwner?.baseUrl ?? null;
2032
+ const base = baseOwner !== void 0 && baseUrl !== null ? (0, path.resolve)(baseOwner.dir, baseUrl) : pathsOwner.dir;
2033
+ const pairs = [];
2034
+ for (const [pattern, target] of paths) {
2035
+ const patternStars = pattern.split("*").length - 1;
2036
+ const targetStars = target.split("*").length - 1;
2037
+ if (pattern === "*" || patternStars > 1 || targetStars > 1) continue;
2038
+ if (pattern.endsWith("/*")) {
2039
+ if (!target.endsWith("/*")) continue;
2040
+ pairs.push({
2041
+ pattern: pattern.slice(0, -2),
2042
+ target: (0, path.resolve)(base, target.slice(0, -2)),
2043
+ kind: "prefix"
2044
+ });
2045
+ continue;
2046
+ }
2047
+ if (patternStars > 0 || targetStars > 0) continue;
2048
+ pairs.push({
2049
+ pattern,
2050
+ target: (0, path.resolve)(base, target),
2051
+ kind: "exact"
2052
+ });
2053
+ }
2054
+ return pairs;
2055
+ }
2056
+ //#endregion
2057
+ //#region pipeline/discover-packages.ts
2058
+ function findPackageRoot(absEntryPath) {
2059
+ let pkgRoot = (0, path.dirname)(absEntryPath);
2060
+ while (pkgRoot !== (0, path.dirname)(pkgRoot) && !(0, fs.existsSync)((0, path.join)(pkgRoot, "package.json"))) pkgRoot = (0, path.dirname)(pkgRoot);
2061
+ return pkgRoot;
2062
+ }
2063
+ const PACKAGE_SRC_EXCLUDES = [
1591
2064
  "dist",
1592
2065
  "node_modules",
1593
2066
  ".test.",
@@ -1606,9 +2079,9 @@ const PACKAGE_OUTPUT_EXCLUDES = [
1606
2079
  ".d.ts",
1607
2080
  ".map"
1608
2081
  ];
1609
- const isFile = (path$4) => {
2082
+ const isFile = (path$2) => {
1610
2083
  try {
1611
- return (0, fs.statSync)(path$4).isFile();
2084
+ return (0, fs.statSync)(path$2).isFile();
1612
2085
  } catch {
1613
2086
  return false;
1614
2087
  }
@@ -1634,6 +2107,77 @@ function sourceEntryForSpecifier(specifier, srcDir, extensionsSet) {
1634
2107
  const subpath = specifier.slice(packageName.length + 1);
1635
2108
  return resolveAbsolutePathSpecifier((0, path.join)(srcDir, subpath || "index"), extensionsSet);
1636
2109
  }
2110
+ /**
2111
+ * A package's `sideEffects` field, decoded the way Vite reads it: a boolean
2112
+ * as written, or the list's globs, one with no `/` matching the basename
2113
+ * anywhere and any other rooted at the package. Undefined without a readable
2114
+ * field, or with glob syntax this matcher lacks (braces, classes, negation,
2115
+ * extglobs), which leaves the bundler's side-effectful default.
2116
+ */
2117
+ function readPackageSideEffects(pkgRoot) {
2118
+ let manifest;
2119
+ try {
2120
+ manifest = JSON.parse((0, fs.readFileSync)((0, path.join)(pkgRoot, "package.json"), "utf-8"));
2121
+ } catch {
2122
+ return;
2123
+ }
2124
+ const field = isJsonBlock(manifest) ? manifest.sideEffects : void 0;
2125
+ if (field === true || field === false) return field;
2126
+ if (!Array.isArray(field)) return void 0;
2127
+ const globs = field.filter(isJsonString);
2128
+ if (globs.some((glob) => /[{}[\]()!+@]/.test(glob))) return void 0;
2129
+ return globs.map((glob) => globToRegExp(glob.includes("/") ? glob.replace(/^\.?\//, "") : `**/${glob}`));
2130
+ }
2131
+ function matchesSideEffects(globs, pkgRoot, absFiles) {
2132
+ return absFiles.some((file) => {
2133
+ const relPath = (0, path.relative)(pkgRoot, file).split(path.sep).join("/");
2134
+ return globs.some((glob) => glob.test(relPath));
2135
+ });
2136
+ }
2137
+ /** An exports target under the `import` condition, then `default`, as the
2138
+ * system loader reads it; an array offers its targets in order. */
2139
+ function exportTarget(value) {
2140
+ if (isJsonString(value)) return value;
2141
+ if (Array.isArray(value)) {
2142
+ for (const entry of value) {
2143
+ const target = exportTarget(entry);
2144
+ if (target !== null) return target;
2145
+ }
2146
+ return null;
2147
+ }
2148
+ if (!isJsonBlock(value)) return null;
2149
+ return exportTarget(value.import) ?? exportTarget(value.default);
2150
+ }
2151
+ /** Each exact `exports` entry of the package at `pkgRoot`, as its subpath
2152
+ * and absolute target; `*` patterns name no single file and are skipped. */
2153
+ function packageExportEntries(pkgRoot) {
2154
+ let manifest;
2155
+ try {
2156
+ manifest = JSON.parse((0, fs.readFileSync)((0, path.join)(pkgRoot, "package.json"), "utf-8"));
2157
+ } catch {
2158
+ return [];
2159
+ }
2160
+ const exports = isJsonBlock(manifest) ? manifest.exports : void 0;
2161
+ if (exports === void 0 || exports === null) return [];
2162
+ const subpaths = isJsonBlock(exports) && Object.keys(exports).some((k) => k.startsWith(".")) ? exports : { ".": exports };
2163
+ const entries = [];
2164
+ for (const [subpath, value] of Object.entries(subpaths)) {
2165
+ if (!subpath.startsWith(".") || subpath.includes("*")) continue;
2166
+ const target = exportTarget(value);
2167
+ if (target !== null) entries.push([subpath, (0, path.resolve)(pkgRoot, target)]);
2168
+ }
2169
+ return entries;
2170
+ }
2171
+ /** Installed content is release content, and unpacking can leave any file
2172
+ * times, so only a package whose real path is outside `node_modules`, such
2173
+ * as a workspace link, is judged stale by them. */
2174
+ function isInstalledPackage(pkgRoot) {
2175
+ let realRoot = pkgRoot;
2176
+ try {
2177
+ realRoot = (0, fs.realpathSync)(pkgRoot);
2178
+ } catch {}
2179
+ return realRoot.split(/[\\/]/).includes("node_modules");
2180
+ }
1637
2181
  function distEntryIsStale(absEntry, srcDir, srcFiles) {
1638
2182
  if (isPathWithinRoot(srcDir, absEntry)) return false;
1639
2183
  let distMtime;
@@ -1654,6 +2198,20 @@ async function collectExternalPackageSources(opts) {
1654
2198
  const pushed = /* @__PURE__ */ new Set();
1655
2199
  const packageMap = {};
1656
2200
  const sourceEntries = /* @__PURE__ */ new Map();
2201
+ const sourceEntrySideEffects = /* @__PURE__ */ new Map();
2202
+ /** `replaced` names the entry the redirect stands in for; it is only
2203
+ * needed, and so only resolved, for a glob list. */
2204
+ const redirect = async (specifier, srcEntry, pkgRoot, replaced) => {
2205
+ sourceEntries.set(specifier, srcEntry);
2206
+ const field = readPackageSideEffects(pkgRoot);
2207
+ if (field === void 0) return;
2208
+ if (field === true || field === false) {
2209
+ sourceEntrySideEffects.set(specifier, field);
2210
+ return;
2211
+ }
2212
+ const replacedEntry = await replaced();
2213
+ sourceEntrySideEffects.set(specifier, replacedEntry === null || matchesSideEffects(field, pkgRoot, [srcEntry, replacedEntry]));
2214
+ };
1657
2215
  const packageDirs = [];
1658
2216
  const dirOwnerSets = {};
1659
2217
  const dirExtensions = {};
@@ -1691,7 +2249,8 @@ async function collectExternalPackageSources(opts) {
1691
2249
  const srcEntry = sourceEntryForSpecifier(specifier, srcDir, extensionsSet);
1692
2250
  if (srcEntry) {
1693
2251
  packageMap[specifier] = (0, path.relative)(rootDir, srcEntry);
1694
- sourceEntries.set(specifier, srcEntry);
2252
+ const replaced = absEntry;
2253
+ await redirect(specifier, srcEntry, pkgRoot, async () => replaced);
1695
2254
  } else packageMap[specifier] = (0, path.relative)(rootDir, absEntry);
1696
2255
  if (!(0, path.isAbsolute)(specifier)) {
1697
2256
  const packageName = bareSpecifierPackageName(specifier);
@@ -1699,12 +2258,18 @@ async function collectExternalPackageSources(opts) {
1699
2258
  const rootEntry = sourceEntryForSpecifier(packageName, srcDir, extensionsSet);
1700
2259
  if (rootEntry) {
1701
2260
  packageMap[packageName] = (0, path.relative)(rootDir, rootEntry);
1702
- sourceEntries.set(packageName, rootEntry);
2261
+ await redirect(packageName, rootEntry, pkgRoot, async () => {
2262
+ try {
2263
+ return await resolveSpecifier(packageName);
2264
+ } catch {
2265
+ return null;
2266
+ }
2267
+ });
1703
2268
  }
1704
2269
  }
1705
2270
  }
1706
2271
  const pkgFiles = walkPackageSources(srcDir, extensionsSet);
1707
- staleDist = distEntryIsStale(absEntry, srcDir, pkgFiles);
2272
+ staleDist = !isInstalledPackage(pkgRoot) && distEntryIsStale(absEntry, srcDir, pkgFiles);
1708
2273
  for (const pkgFile of pkgFiles) {
1709
2274
  const relPath = (0, path.relative)(rootDir, pkgFile);
1710
2275
  if (alreadyIngested(relPath)) {
@@ -1764,6 +2329,15 @@ async function collectExternalPackageSources(opts) {
1764
2329
  fileCount++;
1765
2330
  }
1766
2331
  }
2332
+ if (!(0, path.isAbsolute)(specifier)) {
2333
+ const packageName = bareSpecifierPackageName(specifier);
2334
+ for (const [subpath, target] of packageExportEntries(pkgRoot)) {
2335
+ const entrySpecifier = packageName + subpath.slice(1);
2336
+ const targetRelPath = (0, path.relative)(rootDir, target);
2337
+ if (entrySpecifier in packageMap || !alreadyIngested(targetRelPath)) continue;
2338
+ packageMap[entrySpecifier] = targetRelPath;
2339
+ }
2340
+ }
1767
2341
  outcomes.push({
1768
2342
  specifier,
1769
2343
  outcome: staleDist ? "stale-dist" : fileCount > 0 ? "resolved" : "empty",
@@ -1774,6 +2348,7 @@ async function collectExternalPackageSources(opts) {
1774
2348
  entries,
1775
2349
  packageMap,
1776
2350
  sourceEntries,
2351
+ sourceEntrySideEffects,
1777
2352
  packageDirs,
1778
2353
  dirOwnerSets,
1779
2354
  dirExtensions,
@@ -1797,9 +2372,12 @@ function excludeCollectedPackages(collected, rejectedSpecifiers, rootDir) {
1797
2372
  packageMap[specifier] = relTarget;
1798
2373
  }
1799
2374
  const sourceEntries = /* @__PURE__ */ new Map();
2375
+ const sourceEntrySideEffects = /* @__PURE__ */ new Map();
1800
2376
  for (const [specifier, absEntry] of collected.sourceEntries) {
1801
2377
  if (targetRejected(specifier, absEntry)) continue;
1802
2378
  sourceEntries.set(specifier, absEntry);
2379
+ const sideEffects = collected.sourceEntrySideEffects.get(specifier);
2380
+ if (sideEffects !== void 0) sourceEntrySideEffects.set(specifier, sideEffects);
1803
2381
  }
1804
2382
  const dirOwnerSets = {};
1805
2383
  for (const [dir, specs] of Object.entries(collected.dirOwnerSets)) {
@@ -1821,6 +2399,7 @@ function excludeCollectedPackages(collected, rejectedSpecifiers, rootDir) {
1821
2399
  entries: collected.entries.filter((entry) => !rejectedSpecifiers.has(collected.fileOwners[entry.path])),
1822
2400
  packageMap,
1823
2401
  sourceEntries,
2402
+ sourceEntrySideEffects,
1824
2403
  packageDirs: collected.packageDirs.filter((dir) => !rejectedDirs.includes(dir)),
1825
2404
  dirOwnerSets,
1826
2405
  dirExtensions,
@@ -1840,15 +2419,50 @@ function staleDistIncludesMessage(outcomes) {
1840
2419
  if (stale.length === 0) return null;
1841
2420
  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`;
1842
2421
  }
1843
- function extractSystemFilePackages(systemFilePath) {
2422
+ /** The engine's own parse of a module's imports, through its
2423
+ * `extractFacts`; undefined for an engine without one. A parse that
2424
+ * panicked reports no imports, so it counts as no parse. */
2425
+ function engineImportParser(engine) {
2426
+ const { extractFacts } = engine;
2427
+ if (!extractFacts) return void 0;
2428
+ return (source, path$3) => {
2429
+ try {
2430
+ const facts = parseInternalWire(extractFacts(JSON.stringify([{
2431
+ path: path$3,
2432
+ source
2433
+ }])), "extractFacts").files[path$3];
2434
+ return facts && !facts.parsePanicked ? facts.imports : null;
2435
+ } catch {
2436
+ return null;
2437
+ }
2438
+ };
2439
+ }
2440
+ /** A pattern matching a call of any of `callees` as a whole name, or null
2441
+ * when there is none to anchor on. */
2442
+ function rootCallPattern(callees) {
2443
+ if (callees.length === 0) return null;
2444
+ return `(?<![a-zA-Z0-9_$.])(?:${callees.map((callee) => callee.replace(/[.$]/g, "\\$&")).join("|")})`;
2445
+ }
2446
+ function extractSystemFilePackages(systemFilePath, parseImports) {
1844
2447
  let source;
1845
2448
  try {
1846
2449
  source = (0, fs.readFileSync)(systemFilePath, "utf-8");
1847
2450
  } catch {
1848
2451
  return [];
1849
2452
  }
2453
+ const parsed = parseImports?.(source, systemFilePath) ?? null;
2454
+ const parsedImports = parsed && parsed.length > 0 ? parsed : null;
2455
+ let rootCall = "createSystem";
2456
+ if (parsedImports) {
2457
+ const shadowed = parsedImports.some((binding) => binding.local === "createSystem" && binding.imported !== "createSystem") || /\b(?:function\*?|class)\s+createSystem\b|\b(?:const|let|var)\s+createSystem\b/.test(source);
2458
+ rootCall = rootCallPattern([.../* @__PURE__ */ new Set([
2459
+ ...parsedImports.filter((binding) => binding.imported === "createSystem").map((binding) => binding.local),
2460
+ ...Array.from(source.matchAll(/\bimport\s+(?:[a-zA-Z_$][a-zA-Z0-9_$]*\s*,\s*)?\*\s*as\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s+from\s+['"]@animus-ui\/system(?:\/[^'"]*)?['"]/g), (match) => `${match[1]}.createSystem`),
2461
+ ...shadowed ? [] : ["createSystem"]
2462
+ ])]);
2463
+ }
1850
2464
  const identifiers = /* @__PURE__ */ new Set();
1851
- const constructorRegex = /createSystem\s*\(\s*\{[^}]*?\bincludes\s*:\s*\[([^\]]*)\]/gs;
2465
+ const constructorRegex = rootCall === null ? null : new RegExp(`${rootCall}\\s*\\(\\s*\\{[^}]*?\\bincludes\\s*:\\s*\\[([^\\]]*)\\]`, "gs");
1852
2466
  const chainRegex = /\.includes\s*\(\s*\[([^\]]*)\]\s*\)/gs;
1853
2467
  const collectIdentifiers = (regex) => {
1854
2468
  let match;
@@ -1860,7 +2474,7 @@ function extractSystemFilePackages(systemFilePath) {
1860
2474
  }
1861
2475
  }
1862
2476
  };
1863
- collectIdentifiers(constructorRegex);
2477
+ if (constructorRegex) collectIdentifiers(constructorRegex);
1864
2478
  collectIdentifiers(chainRegex);
1865
2479
  const IDENT_START_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*/;
1866
2480
  const skipTrivia = (from) => {
@@ -1880,6 +2494,38 @@ function extractSystemFilePackages(systemFilePath) {
1880
2494
  return pos;
1881
2495
  }
1882
2496
  };
2497
+ /** The index after the string literal opening at `from`. */
2498
+ const skipString = (from) => {
2499
+ const quote = source[from];
2500
+ let pos = from + 1;
2501
+ while (pos < source.length && source[pos] !== quote) pos += source[pos] === "\\" ? 2 : 1;
2502
+ return pos + 1;
2503
+ };
2504
+ /** The index after the bracketed span opening at `from`, balanced across
2505
+ * `<>`, `()`, `[]` and `{}`, or -1 when it never closes. An arrow type's
2506
+ * `=>` closes nothing. */
2507
+ const skipBalanced = (from) => {
2508
+ let depth = 0;
2509
+ let pos = from;
2510
+ while (pos < source.length) {
2511
+ const ch = source[pos];
2512
+ if (ch === "'" || ch === "\"" || ch === "`") {
2513
+ pos = skipString(pos);
2514
+ continue;
2515
+ }
2516
+ if (source.startsWith("//", pos) || source.startsWith("/*", pos)) {
2517
+ pos = skipTrivia(pos);
2518
+ continue;
2519
+ }
2520
+ if ("<([{".includes(ch)) depth++;
2521
+ else if (")]}".includes(ch) || ch === ">" && source[pos - 1] !== "=") {
2522
+ depth--;
2523
+ if (depth === 0) return pos + 1;
2524
+ }
2525
+ pos++;
2526
+ }
2527
+ return -1;
2528
+ };
1883
2529
  const consumeChainLinks = (from) => {
1884
2530
  let pos = from;
1885
2531
  for (;;) {
@@ -1889,6 +2535,11 @@ function extractSystemFilePackages(systemFilePath) {
1889
2535
  const method = IDENT_START_RE.exec(source.slice(cursor))?.[0];
1890
2536
  if (method !== "extend" && method !== "from") return pos;
1891
2537
  cursor = skipTrivia(cursor + method.length);
2538
+ if (source[cursor] === "<") {
2539
+ const afterTypeArguments = skipBalanced(cursor);
2540
+ if (afterTypeArguments === -1) return pos;
2541
+ cursor = skipTrivia(afterTypeArguments);
2542
+ }
1892
2543
  if (source[cursor] !== "(") return pos;
1893
2544
  cursor = skipTrivia(cursor + 1);
1894
2545
  const base = IDENT_START_RE.exec(source.slice(cursor))?.[0];
@@ -1906,14 +2557,47 @@ function extractSystemFilePackages(systemFilePath) {
1906
2557
  pos = cursor + 1;
1907
2558
  }
1908
2559
  };
2560
+ const bindingAt = /* @__PURE__ */ new Map();
2561
+ const aliasOf = /* @__PURE__ */ new Map();
2562
+ const declarationRe = /\b(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
2563
+ let declaration;
2564
+ while ((declaration = declarationRe.exec(source)) !== null) {
2565
+ const name = declaration[1];
2566
+ let cursor = skipTrivia(declaration.index + declaration[0].length);
2567
+ if (source[cursor] === ":") {
2568
+ cursor++;
2569
+ while (cursor < source.length) {
2570
+ const ch = source[cursor];
2571
+ if (ch === ";" || ch === ",") break;
2572
+ if (ch === "\n" && /^\s*(?:export\s+)?(?:const|let|var|function|class|type|interface|import)\b/.test(source.slice(cursor + 1, cursor + 80))) break;
2573
+ if (ch === "=" && source[cursor + 1] !== ">") break;
2574
+ if ("<([{".includes(ch) || ch === "'" || ch === "\"" || ch === "`") {
2575
+ const after = "<([{".includes(ch) ? skipBalanced(cursor) : skipString(cursor);
2576
+ if (after === -1) break;
2577
+ cursor = after;
2578
+ continue;
2579
+ }
2580
+ cursor = ch === "=" ? cursor + 2 : cursor + 1;
2581
+ }
2582
+ }
2583
+ if (source[cursor] !== "=" || source[cursor + 1] === "=") continue;
2584
+ const initializer = skipTrivia(cursor + 1);
2585
+ bindingAt.set(initializer, name);
2586
+ const target = IDENT_START_RE.exec(source.slice(initializer))?.[0];
2587
+ if (!target) continue;
2588
+ const rest = source.slice(initializer + target.length);
2589
+ if (/^[ \t]*(?:(?:as|satisfies)\s[^;\n]*)?(?:\/\/[^\n]*)?(?:;|\r?\n(?!\s*[.([?`])|$)/.test(rest)) aliasOf.set(name, target);
2590
+ }
1909
2591
  const boundIdentifierBefore = (index) => {
2592
+ const declared = bindingAt.get(index);
2593
+ if (declared) return declared;
1910
2594
  const match = /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*$/.exec(source.slice(0, index));
1911
2595
  return match ? match[1] : null;
1912
2596
  };
1913
2597
  const chainRootIdentifiers = /* @__PURE__ */ new Set();
1914
- const createSystemAnchor = /createSystem\s*\(/g;
2598
+ const createSystemAnchor = rootCall === null ? null : new RegExp(`${rootCall}\\s*\\(`, "g");
1915
2599
  let anchorMatch;
1916
- while ((anchorMatch = createSystemAnchor.exec(source)) !== null) {
2600
+ while ((anchorMatch = createSystemAnchor?.exec(source) ?? null) !== null) {
1917
2601
  let pos = anchorMatch.index + anchorMatch[0].length;
1918
2602
  let depth = 1;
1919
2603
  while (pos < source.length && depth > 0) {
@@ -1944,22 +2628,37 @@ function extractSystemFilePackages(systemFilePath) {
1944
2628
  }
1945
2629
  if (identifiers.size === 0) return [];
1946
2630
  const importMap = /* @__PURE__ */ new Map();
2631
+ for (const binding of parsedImports ?? []) importMap.set(binding.local, binding.source);
1947
2632
  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;
1948
- let importMatch;
1949
- while ((importMatch = importRegex.exec(source)) !== null) {
1950
- const [, comboDefault, namedImports, defaultImport, specifier] = importMatch;
1951
- if (comboDefault) importMap.set(comboDefault, specifier);
1952
- if (defaultImport) importMap.set(defaultImport, specifier);
1953
- if (namedImports) for (const binding of namedImports.split(",")) {
1954
- const parts = binding.trim().split(/\s+as\s+/);
1955
- const localName = (parts[1] || parts[0]).trim();
1956
- if (localName) importMap.set(localName, specifier);
2633
+ if (!parsedImports) {
2634
+ let importMatch;
2635
+ while ((importMatch = importRegex.exec(source)) !== null) {
2636
+ const [, comboDefault, namedImports, defaultImport, specifier] = importMatch;
2637
+ if (comboDefault) importMap.set(comboDefault, specifier);
2638
+ if (defaultImport) importMap.set(defaultImport, specifier);
2639
+ if (namedImports) for (const binding of namedImports.split(",")) {
2640
+ const parts = binding.trim().split(/\s+as\s+/);
2641
+ const localName = (parts[1] || parts[0]).trim();
2642
+ if (localName) importMap.set(localName, specifier);
2643
+ }
1957
2644
  }
1958
2645
  }
1959
2646
  const systemFileDir = (0, path.dirname)(systemFilePath);
1960
2647
  const packages = /* @__PURE__ */ new Set();
2648
+ /** The import a local alias chain ends at, or the identifier itself. */
2649
+ const throughAliases = (id) => {
2650
+ const seen = /* @__PURE__ */ new Set();
2651
+ let current = id;
2652
+ while (!importMap.has(current) && !seen.has(current)) {
2653
+ seen.add(current);
2654
+ const target = aliasOf.get(current);
2655
+ if (!target) break;
2656
+ current = target;
2657
+ }
2658
+ return current;
2659
+ };
1961
2660
  for (const id of identifiers) {
1962
- const specifier = importMap.get(id);
2661
+ const specifier = importMap.get(throughAliases(id));
1963
2662
  if (!specifier) continue;
1964
2663
  if (specifier.startsWith(".")) packages.add((0, path.resolve)(systemFileDir, specifier));
1965
2664
  else packages.add(specifier);
@@ -2018,31 +2717,113 @@ function createSourceCorpus(host, ingestor = createSourceIngestor(host)) {
2018
2717
  //#endregion
2019
2718
  //#region pipeline/asset-placeholders.ts
2020
2719
  const ASSET_PLACEHOLDER_PREFIX = "animus-asset:";
2021
- const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2022
- const PREFIX_RE = escapeRegExp(ASSET_PLACEHOLDER_PREFIX);
2023
- const QUOTED_PLACEHOLDER_RE = new RegExp(`(['"])${PREFIX_RE}([^'"]*?)\\1`, "g");
2024
- const BARE_PLACEHOLDER_RE = new RegExp(`${PREFIX_RE}([^'")\\s]+)`, "g");
2025
- function findAssetSpecifiers(css) {
2720
+ const URL_OPEN_RE = /url\(\s*(['"]?)/gi;
2721
+ const IMAGE_SET_OPEN_RE = /(?:-webkit-)?image-set\(/gi;
2722
+ const BARE_SPECIFIER_RE = /^[^'")\s]+/;
2723
+ /** The index of the `)` that closes the `(` at `open`, outside strings. */
2724
+ function closingParen(css, open) {
2725
+ let depth = 0;
2726
+ let quote = null;
2727
+ for (let i = open; i < css.length; i += 1) {
2728
+ const c = css[i];
2729
+ if (quote !== null) {
2730
+ if (c === quote) quote = null;
2731
+ } else if (c === "\"" || c === "'") quote = c;
2732
+ else if (c === "(") depth += 1;
2733
+ else if (c === ")") {
2734
+ depth -= 1;
2735
+ if (depth === 0) return i;
2736
+ }
2737
+ }
2738
+ return css.length;
2739
+ }
2740
+ function quotedReference(css, quoteAt) {
2741
+ const start = quoteAt + 1;
2742
+ if (!css.startsWith("animus-asset:", start)) return null;
2743
+ const end = css.indexOf(css[quoteAt], start);
2744
+ if (end === -1) return null;
2745
+ return {
2746
+ start,
2747
+ end,
2748
+ specifier: css.slice(start + 13, end)
2749
+ };
2750
+ }
2751
+ function assetReferences(css) {
2026
2752
  if (!css.includes("animus-asset:")) return [];
2027
- const seen = /* @__PURE__ */ new Set();
2028
- const remainder = css.replace(QUOTED_PLACEHOLDER_RE, (_match, _quote, specifier) => {
2029
- seen.add(specifier);
2030
- return "";
2031
- });
2032
- for (const match of remainder.matchAll(BARE_PLACEHOLDER_RE)) seen.add(match[1]);
2033
- return [...seen];
2753
+ const references = [];
2754
+ for (const open of css.matchAll(URL_OPEN_RE)) {
2755
+ const at = (open.index ?? 0) + open[0].length;
2756
+ if (open[1]) {
2757
+ const reference = quotedReference(css, at - 1);
2758
+ if (reference) references.push(reference);
2759
+ continue;
2760
+ }
2761
+ if (!css.startsWith("animus-asset:", at)) continue;
2762
+ const specifier = BARE_SPECIFIER_RE.exec(css.slice(at + 13))?.[0];
2763
+ if (specifier === void 0) continue;
2764
+ const end = at + 13 + specifier.length;
2765
+ references.push({
2766
+ start: at,
2767
+ end,
2768
+ specifier
2769
+ });
2770
+ }
2771
+ for (const open of css.matchAll(IMAGE_SET_OPEN_RE)) {
2772
+ const paren = (open.index ?? 0) + open[0].length - 1;
2773
+ const close = closingParen(css, paren);
2774
+ let depth = 0;
2775
+ let candidateStart = true;
2776
+ for (let i = paren + 1; i < close; i += 1) {
2777
+ const c = css[i];
2778
+ if (/\s/.test(c)) continue;
2779
+ if (c === "\"" || c === "'") {
2780
+ if (depth === 0 && candidateStart) {
2781
+ const reference = quotedReference(css, i);
2782
+ if (reference) references.push(reference);
2783
+ }
2784
+ const end = css.indexOf(c, i + 1);
2785
+ i = end === -1 ? close : end;
2786
+ } else if (c === "(") depth += 1;
2787
+ else if (c === ")") depth -= 1;
2788
+ else if (c === "," && depth === 0) {
2789
+ candidateStart = true;
2790
+ continue;
2791
+ }
2792
+ candidateStart = false;
2793
+ }
2794
+ }
2795
+ return references.sort((a, b) => a.start - b.start);
2796
+ }
2797
+ function findAssetSpecifiers(css) {
2798
+ return [...new Set(assetReferences(css).map((reference) => reference.specifier))];
2034
2799
  }
2035
2800
  function substituteAssetPlaceholders(css, urlBySpecifier) {
2036
2801
  if (urlBySpecifier.size === 0) return css;
2037
- if (!css.includes("animus-asset:")) return css;
2038
- const specifiers = [...urlBySpecifier.keys()].sort((a, b) => b.length - a.length);
2039
2802
  let out = css;
2040
- for (const specifier of specifiers) {
2041
- const placeholder = new RegExp(escapeRegExp(ASSET_PLACEHOLDER_PREFIX + specifier) + String.raw`(?=['")\s]|$)`, "g");
2042
- out = out.replace(placeholder, () => urlBySpecifier.get(specifier));
2803
+ for (const reference of assetReferences(css).reverse()) {
2804
+ const url = urlBySpecifier.get(reference.specifier);
2805
+ if (url === void 0) continue;
2806
+ out = out.slice(0, reference.start) + url + out.slice(reference.end);
2043
2807
  }
2044
2808
  return out;
2045
2809
  }
2810
+ /**
2811
+ * Specifiers of placeholder text anywhere inside a `url()` or `image-set()`
2812
+ * call, a reference or not: after substitution any of it is a broken load.
2813
+ */
2814
+ function placeholdersInAssetCalls(css) {
2815
+ if (!css.includes("animus-asset:")) return [];
2816
+ const calls = [...css.matchAll(URL_OPEN_RE), ...css.matchAll(IMAGE_SET_OPEN_RE)].map((open) => {
2817
+ const paren = css.indexOf("(", open.index ?? 0);
2818
+ return [paren, closingParen(css, paren)];
2819
+ });
2820
+ const found = /* @__PURE__ */ new Set();
2821
+ for (let at = css.indexOf(ASSET_PLACEHOLDER_PREFIX); at !== -1; at = css.indexOf(ASSET_PLACEHOLDER_PREFIX, at + 1)) {
2822
+ if (!calls.some(([open, close]) => open < at && at < close)) continue;
2823
+ found.add(/^[^'")\s,]*/.exec(css.slice(at + 13))[0]);
2824
+ }
2825
+ return [...found];
2826
+ }
2046
2827
  function findSheetAssetSpecifiers(sheets) {
2047
2828
  return [.../* @__PURE__ */ new Set([
2048
2829
  ...findAssetSpecifiers(sheets.variableCss),
@@ -2062,12 +2843,22 @@ const UNSUBSTITUTED_ASSET_CODE = "animus.asset.unsubstituted-placeholder";
2062
2843
  /** A placeholder in emitted CSS is a broken URL in the browser: it fails a
2063
2844
  * strict build and warns otherwise. */
2064
2845
  function reportSurvivingAssetPlaceholders(css, report) {
2065
- const specifiers = findAssetSpecifiers(css);
2846
+ const specifiers = placeholdersInAssetCalls(css);
2066
2847
  if (specifiers.length === 0) return;
2067
- const message = `${report.prefix} asset() placeholders reached emitted CSS unsubstituted: ${specifiers.join(", ")} (${UNSUBSTITUTED_ASSET_CODE})`;
2848
+ const message = `${report.prefix} asset() placeholders reached ${report.surface ?? "emitted CSS"} unsubstituted: ${specifiers.join(", ")} (${UNSUBSTITUTED_ASSET_CODE})`;
2068
2849
  if (report.strict) throw new Error(message);
2069
2850
  report.warn(message);
2070
2851
  }
2852
+ /**
2853
+ * The runtime code an analysis generates: each dynamic prop config, which the
2854
+ * shared prop map module carries, and each component's replacement. Extraction
2855
+ * lifts asset() out of them into root variables, so none should hold one.
2856
+ * Both embed values as JSON strings, so their quote and backslash escapes are
2857
+ * undone for scanning: the text is for `findAssetSpecifiers`, not to run.
2858
+ */
2859
+ function generatedModuleCode(manifest) {
2860
+ return [JSON.stringify(manifest.dynamic_props), ...Object.values(manifest.components).map((component) => component.replacement)].join("\n").replace(/\\(["\\])/g, "$1");
2861
+ }
2071
2862
  //#endregion
2072
2863
  //#region pipeline/resolve-asset.ts
2073
2864
  /**
@@ -2266,14 +3057,9 @@ function buildPathAliasesJson(pairs, rootDir) {
2266
3057
  //#endregion
2267
3058
  //#region pipeline/post-process-css.ts
2268
3059
  function resolveLightningTargets(explicitTargets, rootDir) {
2269
- let queries;
2270
- if (explicitTargets) queries = Array.isArray(explicitTargets) ? explicitTargets : [explicitTargets];
2271
- else {
2272
- const detected = (0, browserslist.default)(void 0, { path: rootDir });
2273
- queries = detected.length > 0 ? detected : (0, browserslist.default)("defaults");
2274
- }
2275
- const [firstQuery] = queries;
2276
- return (0, lightningcss.browserslistToTargets)(firstQuery !== void 0 && firstQuery.includes(" ") ? (0, browserslist.default)(queries) : queries);
3060
+ if (explicitTargets) return (0, lightningcss.browserslistToTargets)((0, browserslist.default)(explicitTargets));
3061
+ const detected = (0, browserslist.default)(void 0, { path: rootDir });
3062
+ return (0, lightningcss.browserslistToTargets)(detected.length > 0 ? detected : (0, browserslist.default)("defaults"));
2277
3063
  }
2278
3064
  function postProcessCss(css, opts) {
2279
3065
  if (!css) return css;
@@ -2291,186 +3077,28 @@ function postProcessCss(css, opts) {
2291
3077
  }
2292
3078
  }
2293
3079
  //#endregion
2294
- //#region pipeline/manifest-diagnostics.ts
2295
- /** Stable code for selector forms with no substitutable subject; the
2296
- * extraction engine mints the same string. */
2297
- const SELECTOR_UNSUPPORTED_SUBJECT = "animus.selector.unsupported-subject";
2298
- /** The one matcher for the engine's unresolved-parent chain-drop message;
2299
- * consumers match through the exported helpers, never their own copy. */
2300
- const UNRESOLVED_PARENT_RE = /chain dropped: could not resolve parent component '([^']+)'/;
2301
- function isUnresolvedParentDrop(diagnostic) {
2302
- return UNRESOLVED_PARENT_RE.test(diagnostic.message);
2303
- }
2304
- function unresolvedParentName(diagnostic) {
2305
- return UNRESOLVED_PARENT_RE.exec(diagnostic.message)?.[1] ?? null;
2306
- }
2307
- /** True when the selector carries at least one substitutable `&` subject
2308
- * outside quoted text. */
2309
- function hasSelectorSubject(value) {
2310
- let quote = null;
2311
- let escaped = false;
2312
- for (const c of value) {
2313
- if (escaped) {
2314
- escaped = false;
2315
- continue;
2316
- }
2317
- if (c === "\\") {
2318
- escaped = true;
2319
- continue;
2320
- }
2321
- if (quote !== null) {
2322
- if (c === quote) quote = null;
2323
- } else if (c === "\"" || c === "'") quote = c;
2324
- else if (c === "&") return true;
2325
- }
2326
- return false;
2327
- }
2328
- /**
2329
- * Coded diagnostics for selector-shaped alias values with no substitutable
2330
- * subject; alias values never reach the evaluator's own key guard.
2331
- */
2332
- function collectSelectorAliasDiagnostics(selectorAliasesJson) {
2333
- if (!selectorAliasesJson) return [];
2334
- const aliases = parseInternalWire(selectorAliasesJson, "selectorAliasesJson (the system loader's selector-alias registry)");
2335
- const diagnostics = [];
2336
- for (const [name, value] of Object.entries(aliases)) if (value.includes("&") && !hasSelectorSubject(value)) diagnostics.push({
2337
- file: "system",
2338
- component: name,
2339
- kind: "warn",
2340
- message: `selector alias '${name}' value '${value}' has no substitutable '&' subject outside quoted text (${SELECTOR_UNSUPPORTED_SUBJECT})`,
2341
- code: SELECTOR_UNSUPPORTED_SUBJECT,
2342
- severity: severityFor(SELECTOR_UNSUPPORTED_SUBJECT)
2343
- });
2344
- return diagnostics;
2345
- }
2346
- const UNREADABLE_SOURCE_FILE = "animus.ingestion.unreadable-source-file";
2347
- /**
2348
- * A configured source file that could not be read is a LOST INPUT: the
2349
- * emitted artifacts silently lack whatever that file declared.
2350
- */
2351
- function unreadableSourceDiagnostic(relPath, error) {
2352
- return {
2353
- file: relPath,
2354
- component: "source",
2355
- kind: "warn",
2356
- message: `configured source file ${relPath} could not be read and was skipped: ${String(error)}`,
2357
- code: UNREADABLE_SOURCE_FILE,
2358
- severity: severityFor(UNREADABLE_SOURCE_FILE)
2359
- };
2360
- }
2361
- /** The collision entry code minted by the system package's merge. */
2362
- const VOCABULARY_COLLISION = "animus.vocabulary.collision";
2363
- /** A theme `@property` registration browsers would ignore; it is not emitted. */
2364
- const INVALID_PROPERTY_REGISTRATION = "animus.theme.invalid-property-registration";
2365
- /** Minted when a sealed kit with registered vocabulary is consumed through
2366
- * the deprecated `from()`/`includes:` verbs. */
2367
- const VOCABULARY_LEGACY_VERB = "animus.vocabulary.legacy-verb";
2368
- /** A `var()` fallback that never applies: the property is registered with an
2369
- * initial value. */
2370
- const PROPERTY_FALLBACK_SUPPRESSED = "animus.property.fallback-suppressed";
2371
- /** The same, where the fallback heads a chain of further `var()` reads. */
2372
- const PROPERTY_FALLBACK_CHAIN_SUPPRESSED = "animus.property.fallback-chain-suppressed";
2373
- /** A declared property set in keyframes or transitioned without a typed
2374
- * registration, so it does not interpolate. */
2375
- const PROPERTY_UNREGISTERED_ANIMATION = "animus.property.unregistered-animation";
2376
- /** The same, where `allow-discrete` shows interpolation was intended. */
2377
- const PROPERTY_DISCRETE_ANIMATION = "animus.property.discrete-animation";
2378
- /** A custom property whose resolved value is `var()` of itself, which makes
2379
- * it invalid at computed-value time. */
2380
- const PROPERTY_SELF_REFERENCE = "animus.property.self-reference";
2381
- const DIAGNOSTIC_SEVERITY = /* @__PURE__ */ new Map([
2382
- [SELECTOR_UNSUPPORTED_SUBJECT, "error"],
2383
- [UNREADABLE_SOURCE_FILE, "error"],
2384
- [VOCABULARY_COLLISION, "warn"],
2385
- [VOCABULARY_LEGACY_VERB, "warn"],
2386
- [INVALID_PROPERTY_REGISTRATION, "error"],
2387
- [PROPERTY_FALLBACK_SUPPRESSED, "info"],
2388
- [PROPERTY_FALLBACK_CHAIN_SUPPRESSED, "warn"],
2389
- [PROPERTY_UNREGISTERED_ANIMATION, "info"],
2390
- [PROPERTY_DISCRETE_ANIMATION, "warn"],
2391
- [PROPERTY_SELF_REFERENCE, "warn"]
2392
- ]);
2393
- /** An unlisted code is `warn`: a witness kind from a newer system package
2394
- * must not fail the strict build of a host that predates its writer. */
2395
- function severityFor(code) {
2396
- return (code === void 0 ? void 0 : DIAGNOSTIC_SEVERITY.get(code)) ?? "warn";
2397
- }
2398
- /**
2399
- * The one mapper of a sealed system's witness entries for every host: the
2400
- * loader's evaluation host shims `console`, so the record is the channel.
2401
- */
2402
- function vocabularyWitnessDiagnostics(vocabularyWitnessesJson) {
2403
- if (!vocabularyWitnessesJson) return [];
2404
- const entries = parseInternalWire(vocabularyWitnessesJson, "vocabularyWitnessesJson (the sealed system's vocabulary witness record)");
2405
- const diagnostics = [];
2406
- for (const entry of entries) if (entry.code === "animus.vocabulary.collision") diagnostics.push({
2407
- file: "system",
2408
- component: entry.name ?? "keyframes",
2409
- kind: "warn",
2410
- message: `keyframes vocabulary "${entry.name}" is registered by both ${entry.loser} and ${entry.winner} — ${entry.winner} wins; rename one collection (${entry.code})`,
2411
- code: entry.code,
2412
- severity: severityFor(entry.code)
2413
- });
2414
- else if (entry.code === "animus.vocabulary.legacy-verb") diagnostics.push({
2415
- file: "system",
2416
- component: "keyframes",
2417
- kind: "warn",
2418
- 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})`,
2419
- code: entry.code,
2420
- severity: severityFor(entry.code)
2421
- });
2422
- else diagnostics.push({
2423
- file: "system",
2424
- component: "vocabulary",
2425
- kind: "warn",
2426
- 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})` : ""}`,
2427
- code: entry.code,
2428
- severity: severityFor(entry.code)
2429
- });
2430
- return diagnostics;
2431
- }
2432
- /** The diagnostics a loaded system's own records carry: vocabulary
2433
- * witnesses and invalid `@property` registrations. */
2434
- function systemLoadDiagnostics(system) {
2435
- const registrations = (system.invalidPropertyRegistrations ?? []).map(({ name, reason }) => ({
2436
- file: "system",
2437
- component: name,
2438
- kind: "warn",
2439
- 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})`,
2440
- code: INVALID_PROPERTY_REGISTRATION,
2441
- severity: severityFor(INVALID_PROPERTY_REGISTRATION)
2442
- }));
2443
- return [...vocabularyWitnessDiagnostics(system.vocabularyWitnessesJson), ...registrations];
2444
- }
2445
- /**
2446
- * The single strict-escalation policy point for both extraction plugins:
2447
- * error-severity diagnostics throw together under `strict`, else warn.
2448
- */
2449
- function surfaceManifestDiagnostics(manifest, warn, policy = {}) {
2450
- const errors = [];
2451
- const diagnostics = policy.prepend?.length ? [...policy.prepend, ...manifest.diagnostics ?? []] : manifest.diagnostics ?? [];
2452
- for (const diagnostic of diagnostics) {
2453
- let line = null;
2454
- if (diagnostic.kind === "bail") line = `⚠ ${diagnostic.component} not extracted: ${diagnostic.message}`;
2455
- else if (diagnostic.kind === "skip") line = `⚠ ${diagnostic.component}: skipped ${diagnostic.message}`;
2456
- else if (diagnostic.kind === "warn") line = `${diagnostic.severity === "info" ? "ℹ" : "⚠"} ${diagnostic.file}: ${diagnostic.component}: ${diagnostic.message}`;
2457
- if (line === null) continue;
2458
- if (diagnostic.code && !diagnostic.message.includes(diagnostic.code)) line += ` [${diagnostic.code}]`;
2459
- if (diagnostic.severity === "info") {
2460
- policy.info?.(line);
2461
- continue;
2462
- }
2463
- if (policy.strict && diagnostic.severity === "error") {
2464
- errors.push(`${diagnostic.code ?? "error"} — ${diagnostic.component}: ${diagnostic.message}`);
2465
- continue;
2466
- }
2467
- warn(line);
3080
+ //#region pipeline/property-registrations.ts
3081
+ const PROPERTY_AT_RULE = /@property\s+/gi;
3082
+ const RULE_BODY = /\s*\{[^}]*\}\n?/y;
3083
+ /** Each `@property` rule, through one newline after it, with its name read
3084
+ * as CSS tokenizes an identifier: escapes decoded, non-ASCII included. */
3085
+ function propertyRules(css) {
3086
+ const rules = [];
3087
+ for (const match of css.matchAll(PROPERTY_AT_RULE)) {
3088
+ if (match.index < (rules.at(-1)?.end ?? 0)) continue;
3089
+ const name = (0, _animus_ui_properties.identifierAt)(css, match.index + match[0].length);
3090
+ if (!name?.name.startsWith("--")) continue;
3091
+ RULE_BODY.lastIndex = name.end;
3092
+ const body = RULE_BODY.exec(css);
3093
+ if (body === null) continue;
3094
+ rules.push({
3095
+ start: match.index,
3096
+ end: name.end + body[0].length,
3097
+ name: name.name
3098
+ });
2468
3099
  }
2469
- if (errors.length > 0) throw new Error(`[animus] strict: ${errors.length} error diagnostic(s):\n${errors.join("\n")}`);
3100
+ return rules;
2470
3101
  }
2471
- //#endregion
2472
- //#region pipeline/property-registrations.ts
2473
- const PROPERTY_RULE = /@property\s+(--[\w-]+)\s*\{[^}]*\}\n?/g;
2474
3102
  /** The data types a registration's `syntax` may name. */
2475
3103
  const SYNTAX_TYPES = /* @__PURE__ */ new Set([
2476
3104
  "angle",
@@ -2490,44 +3118,40 @@ const SYNTAX_TYPES = /* @__PURE__ */ new Set([
2490
3118
  "url"
2491
3119
  ]);
2492
3120
  const SYNTAX_COMPONENT = /^(?:<([a-z-]+)>[+#]?|-?[a-zA-Z_][\w-]*)$/;
2493
- /** The syntax types whose values can carry a font- or container-relative
2494
- * unit, which makes a typed initial value depend on context. */
2495
- const DIMENSION_TYPES = /* @__PURE__ */ new Set([
2496
- "image",
2497
- "length",
2498
- "length-percentage",
2499
- "transform-function",
2500
- "transform-list"
2501
- ]);
2502
- 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;
2503
- const SUBSTITUTION = /\b(?:var|env|attr)\(/i;
2504
- /** Quoted strings and `url()` hold text, never units or substitutions. */
2505
- const LITERAL_TEXT = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|url\([^)]*\)/gi;
3121
+ const INITIAL_VALUE = /(initial-value\s*:\s*)([^;}]*)/;
3122
+ const MATH_FUNCTION = /\b(?:calc|min|max|clamp)\(/i;
2506
3123
  /**
2507
3124
  * Removes the `@property` rules browsers would ignore — an unknown syntax
2508
3125
  * component, a syntax other than `*` without an initial value, an initial
2509
- * value that depends on context, or one the syntax does not accept.
2510
- * Minifying a stylesheet that contains such a rule can also fail.
3126
+ * value that substitutes another value or depends on context, math that
3127
+ * breaks calculation syntax, or a value the syntax does not accept.
3128
+ * Minifying a stylesheet that contains such a rule can also fail. An
3129
+ * all-absolute math initial value is written as its computed value, which
3130
+ * the minifier reads.
2511
3131
  */
2512
3132
  function splitInvalidPropertyRegistrations(css) {
2513
3133
  const invalid = [];
3134
+ let kept = "";
3135
+ let copied = 0;
3136
+ for (const { start, end, name } of propertyRules(css)) {
3137
+ const checked = checkRegistration(css.slice(start, end));
3138
+ kept += css.slice(copied, start);
3139
+ if ("rule" in checked) kept += checked.rule;
3140
+ else invalid.push({
3141
+ name,
3142
+ reason: checked.reason
3143
+ });
3144
+ copied = end;
3145
+ }
2514
3146
  return {
2515
- css: css.replace(PROPERTY_RULE, (rule, name) => {
2516
- const reason = registrationFailure(rule);
2517
- if (reason === null) return rule;
2518
- invalid.push({
2519
- name,
2520
- reason
2521
- });
2522
- return "";
2523
- }),
3147
+ css: kept + css.slice(copied),
2524
3148
  invalid
2525
3149
  };
2526
3150
  }
2527
3151
  /** The registrations in `css`, by custom-property name. */
2528
3152
  function propertyRegistrations(css) {
2529
3153
  const registrations = /* @__PURE__ */ new Map();
2530
- for (const match of css.matchAll(PROPERTY_RULE)) registrations.set(match[1], readRegistration(match[0]));
3154
+ for (const { start, end, name } of propertyRules(css)) registrations.set(name, readRegistration(css.slice(start, end)));
2531
3155
  return registrations;
2532
3156
  }
2533
3157
  function readRegistration(rule) {
@@ -2536,42 +3160,56 @@ function readRegistration(rule) {
2536
3160
  initialValue: /initial-value\s*:\s*([^;}]*)/.exec(rule)?.[1]?.trim()
2537
3161
  };
2538
3162
  }
2539
- function registrationFailure(rule) {
3163
+ /** The rule to keep, with an all-absolute math initial value folded, or why
3164
+ * a browser would ignore it. */
3165
+ function checkRegistration(rule) {
2540
3166
  const { syntax, initialValue } = readRegistration(rule);
3167
+ if (initialValue !== void 0 && (0, _animus_ui_properties.substitutesValue)(initialValue)) return { reason: `initialValue "${initialValue}" substitutes another value; var(), env() and attr() are not allowed in an initial value` };
3168
+ let kept = rule;
3169
+ let folded;
2541
3170
  if (syntax !== "*") {
2542
- const components = syntax.split("|").map((component) => ({
3171
+ const unknown = syntax.split("|").map((component) => ({
2543
3172
  text: component.trim(),
2544
3173
  match: SYNTAX_COMPONENT.exec(component.trim())
2545
- }));
2546
- const unknown = components.find(({ match }) => match === null || match[1] !== void 0 && !SYNTAX_TYPES.has(match[1]));
2547
- if (unknown !== void 0) return `syntax "${syntax}" has the unknown component "${unknown.text}"`;
2548
- if (initialValue === void 0) return `syntax "${syntax}" needs an initialValue`;
2549
- const dimensional = components.some(({ match }) => match?.[1] !== void 0 && DIMENSION_TYPES.has(match[1]));
2550
- const value = initialValue.replace(LITERAL_TEXT, "");
2551
- 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`;
3174
+ })).find(({ match }) => match === null || match[1] !== void 0 && !SYNTAX_TYPES.has(match[1]));
3175
+ if (unknown !== void 0) return { reason: `syntax "${syntax}" has the unknown component "${unknown.text}"` };
3176
+ if (initialValue === void 0) return { reason: `syntax "${syntax}" needs an initialValue` };
3177
+ if ((0, _animus_ui_properties.dependsOnContext)(initialValue)) return { reason: `initialValue "${initialValue}" depends on context; font- and container-relative units are not allowed` };
3178
+ folded = (0, _animus_ui_properties.foldInitialValue)(syntax, initialValue);
3179
+ if (folded?.kind === "invalid") return { reason: `initialValue "${initialValue}" is not a valid calculation: ${folded.reason}` };
3180
+ if (folded?.kind === "rejected") return { reason: `initialValue "${initialValue}" computes to the ${folded.type} ${folded.text}, which syntax "${syntax}" does not accept` };
3181
+ if (folded?.kind === "folded") kept = rule.replace(INITIAL_VALUE, `$1${folded.text}`);
2552
3182
  }
2553
3183
  try {
2554
3184
  (0, lightningcss.transform)({
2555
3185
  filename: "animus-registrations.css",
2556
- code: Buffer.from(rule)
3186
+ code: Buffer.from(kept)
2557
3187
  });
2558
- return null;
3188
+ return { rule: kept };
2559
3189
  } catch (error) {
2560
3190
  const detail = error instanceof Error ? error.message : String(error);
2561
- return `the CSS parser rejects \`${rule.trim()}\` (${detail})`;
3191
+ 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})` };
3192
+ 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` };
3193
+ return { reason: `the CSS parser rejects \`${kept.trim()}\` (${detail})` };
2562
3194
  }
2563
3195
  }
2564
3196
  //#endregion
2565
3197
  //#region pipeline/property-diagnostics.ts
2566
3198
  /**
2567
3199
  * Reports custom properties that will not behave as their CSS suggests: a
2568
- * property that refers to itself and, once the system emits an `@property`
2569
- * rule, a fallback an initial value suppresses and an animated property that
2570
- * cannot interpolate. The CSS itself is never changed.
3200
+ * property that refers to itself, a fallback a registered initial value
3201
+ * applies ahead of, and an animated property that cannot interpolate. Sheets
3202
+ * are read through the shared CSS tokenizer, as the prefix pass reads them:
3203
+ * comments are trivia, quoted text holds no names, and escapes and function
3204
+ * case are decoded. The CSS itself is never changed.
2571
3205
  */
2572
3206
  function checkCustomProperties(input) {
2573
3207
  const registered = propertyRegistrations(input.system.variableCss);
2574
- const declared = declaredProperties(input.system.contextualVarsJson);
3208
+ const declaredNames = input.system.contextualProperties ?? [];
3209
+ const declared = new Set(declaredNames);
3210
+ const withInitialValue = [...registered].filter(([, registration]) => registration.initialValue !== void 0).map(([name]) => name);
3211
+ const writers = currentVarWriters(input.system.propConfigJson);
3212
+ const currentVars = new Set(writers.keys());
2575
3213
  const findings = [];
2576
3214
  const sheets = [{
2577
3215
  css: input.componentCss,
@@ -2581,8 +3219,20 @@ function checkCustomProperties(input) {
2581
3219
  owner: globalOwner
2582
3220
  }];
2583
3221
  for (const { css, owner } of sheets) {
3222
+ if (!mayHoldFinding(css, withInitialValue, declaredNames, currentVars)) continue;
2584
3223
  const parsed = declarations(css);
2585
- const discreteRules = new Set(parsed.filter((d) => d.property === "transition-behavior" && /\ballow-discrete\b/.test(d.value)).map((d) => d.rule));
3224
+ for (const cycle of currentVarCycles(parsed, currentVars)) {
3225
+ const [first] = cycle;
3226
+ const names = cycle.map((d) => d.property);
3227
+ const props = [...new Set(cycle.flatMap((d) => writers.get(d.property) ?? []))];
3228
+ findings.push({
3229
+ code: PROPERTY_CURRENT_VAR_CYCLE,
3230
+ property: first.property,
3231
+ 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`,
3232
+ owner: owner(first)
3233
+ });
3234
+ }
3235
+ const discreteRules = new Set(parsed.filter((d) => d.property === "transition-behavior" && topLevelItems(d.values).some(isDiscrete)).map((d) => d.rule));
2586
3236
  for (const declaration of parsed) {
2587
3237
  const found = (code, property, message) => findings.push({
2588
3238
  code,
@@ -2590,139 +3240,275 @@ function checkCustomProperties(input) {
2590
3240
  message,
2591
3241
  owner: owner(declaration)
2592
3242
  });
2593
- if (isSelfReference(declaration)) {
3243
+ const reads = (0, _animus_ui_properties.variableReads)(declaration.values);
3244
+ if (isSelfReference(declaration, reads)) {
2594
3245
  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`);
2595
3246
  continue;
2596
3247
  }
2597
- if (registered.size === 0) continue;
2598
- for (const read of varReads(declaration.value)) {
3248
+ if (readsItselfDirectly(declaration, reads)) {
3249
+ const { property, value } = declaration;
3250
+ 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`);
3251
+ continue;
3252
+ }
3253
+ if (readsItselfOnlyInFallback(declaration, reads)) {
3254
+ const { property, value } = declaration;
3255
+ 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`);
3256
+ }
3257
+ for (const read of reads) {
2599
3258
  const registration = registered.get(read.name);
2600
3259
  if (read.fallback === void 0 || registration?.initialValue === void 0) continue;
2601
- const chain = read.fallback.startsWith("var(");
2602
- 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`);
3260
+ const { fallback } = read;
3261
+ const chain = reads.some((other) => other.start === fallback.first);
3262
+ 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`);
2603
3263
  }
2604
3264
  for (const animated of animatedProperties(declaration, discreteRules)) {
2605
- if (!declared.has(animated.name)) continue;
3265
+ if (animated.discrete || !declared.has(animated.name)) continue;
2606
3266
  const reason = interpolationGap(registered.get(animated.name));
2607
3267
  if (reason === null) continue;
2608
- 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`);
2609
- 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`);
3268
+ 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`);
2610
3269
  }
2611
3270
  }
2612
3271
  }
2613
3272
  return toDiagnostics(findings);
2614
3273
  }
2615
- function declaredProperties(contextualVarsJson) {
2616
- if (!contextualVarsJson) return /* @__PURE__ */ new Set();
2617
- const byScale = parseInternalWire(contextualVarsJson, "contextualVarsJson (the theme's contextual variable names)");
2618
- return new Set(Object.values(byScale).flatMap((names) => names.map((name) => `--${name}`)));
3274
+ /**
3275
+ * Whether a sheet can hold a finding, shown by its text before anything is
3276
+ * parsed: a custom property reading its own name, a `var(` with a fallback
3277
+ * of a registration that has an initial value, a transition or keyframes
3278
+ * and a declared contextual variable's name, or a `var(` of a currentVar name
3279
+ * when two or more props write one. An escape or a comment can hide
3280
+ * a name or a function from the text, so a sheet with either is always
3281
+ * parsed.
3282
+ */
3283
+ function mayHoldFinding(css, withInitialValue, declared, currentVars) {
3284
+ 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));
3285
+ }
3286
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3287
+ /** A custom-property declaration key, as the text shows one: anything but
3288
+ * CSS whitespace and the characters that end a name, so a non-ASCII space
3289
+ * stays part of the name, as CSS reads it. */
3290
+ const CUSTOM_PROPERTY_KEY = /(--[^\t\n\f\r :;{}()"'\\/]+)[\t\n\f\r ]*:/g;
3291
+ const VAR_OPEN = /var\(\s*/iy;
3292
+ const isNameCode = (c) => c >= 48 && c <= 57 || c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 45 || c === 95 || c >= 128;
3293
+ /**
3294
+ * Whether a custom-property declaration has `var(` of its own whole name in
3295
+ * its value, bounded as the declaration scanner bounds one: by a `;`, `{` or
3296
+ * `}` outside parentheses, brackets and quoted text. `var` matches in any
3297
+ * case, so this errs toward parsing.
3298
+ */
3299
+ function readsOwnName(css) {
3300
+ for (const match of css.matchAll(CUSTOM_PROPERTY_KEY)) {
3301
+ const name = match[1];
3302
+ let depth = 0;
3303
+ let quote = -1;
3304
+ for (let i = match.index + match[0].length; i < css.length; i += 1) {
3305
+ const c = css.charCodeAt(i);
3306
+ if (quote !== -1) {
3307
+ if (c === quote || c === 10 || c === 13 || c === 12) quote = -1;
3308
+ } else if (c === 34 || c === 39) quote = c;
3309
+ else if (c === 40 || c === 91) depth += 1;
3310
+ else if (c === 41 || c === 93) depth = Math.max(0, depth - 1);
3311
+ else if (depth === 0 && (c === 59 || c === 123 || c === 125)) break;
3312
+ else if (c === 118 || c === 86) {
3313
+ VAR_OPEN.lastIndex = i;
3314
+ const at = VAR_OPEN.test(css) ? VAR_OPEN.lastIndex : -1;
3315
+ if (at !== -1 && css.startsWith(name, at) && !isNameCode(css.charCodeAt(at + name.length))) return true;
3316
+ }
3317
+ }
3318
+ }
3319
+ return false;
2619
3320
  }
2620
- /** Declarations in engine-emitted CSS: one `property: value` per `;`. */
2621
- function declarations(css) {
2622
- const found = [];
2623
- const stack = [];
2624
- let rules = 0;
2625
- let segment = 0;
2626
- let parens = 0;
2627
- let quote = null;
2628
- const take = (end) => {
2629
- const text = css.slice(segment, end);
2630
- const colon = text.indexOf(":");
2631
- const current = stack[stack.length - 1];
2632
- if (current === void 0 || colon <= 0) return;
2633
- found.push({
2634
- property: text.slice(0, colon).trim(),
2635
- value: text.slice(colon + 1).replace(/;$/, "").trim(),
2636
- start: segment + (text.length - text.trimStart().length),
2637
- end,
2638
- rule: current.rule,
2639
- context: stack.map((entry) => entry.prelude)
2640
- });
3321
+ /** `items` as text without comments, at-keywords decoded. */
3322
+ function textOf(css, items) {
3323
+ let text = "";
3324
+ const add = (token) => {
3325
+ if (token.type === "whitespace") text += " ";
3326
+ else if (token.type === "at-keyword") text += `@${token.value.toLowerCase()}`;
3327
+ else text += css.slice(token.start, token.end);
2641
3328
  };
2642
- for (let i = 0; i < css.length; i += 1) {
2643
- const c = css[i];
2644
- if (quote !== null) {
2645
- if (c === "\\") i += 1;
2646
- else if (c === quote) quote = null;
2647
- continue;
3329
+ const walk = (list) => {
3330
+ for (const item of list) if (item.kind === "token") add(item.token);
3331
+ else {
3332
+ add(item.open);
3333
+ walk(item.children);
3334
+ if (item.close) add(item.close);
2648
3335
  }
2649
- if (c === "\"" || c === "'") quote = c;
2650
- else if (c === "(") parens += 1;
2651
- else if (c === ")") parens -= 1;
2652
- else if (parens === 0 && c === "{") {
2653
- stack.push({
2654
- prelude: css.slice(segment, i).trim(),
2655
- rule: rules
3336
+ };
3337
+ walk(items);
3338
+ return text.trim();
3339
+ }
3340
+ /** Declarations in a sheet: one `property: value` per `;` inside a `{}`
3341
+ * block, read from component values, so comments separate nothing. */
3342
+ function declarations(css) {
3343
+ const found = [];
3344
+ let rules = 0;
3345
+ const visit = (items, context, rule) => {
3346
+ let segment = [];
3347
+ const take = () => {
3348
+ const declaration = declarationOf(css, segment);
3349
+ if (rule !== void 0 && declaration !== void 0) found.push({
3350
+ ...declaration,
3351
+ rule,
3352
+ context
2656
3353
  });
3354
+ segment = [];
3355
+ };
3356
+ for (const item of items) if (item.kind === "block" && item.open.type === "{") {
3357
+ const prelude = textOf(css, segment);
3358
+ segment = [];
2657
3359
  rules += 1;
2658
- segment = i + 1;
2659
- } else if (parens === 0 && c === ";") {
2660
- take(i + 1);
2661
- segment = i + 1;
2662
- } else if (parens === 0 && c === "}") {
2663
- take(i);
2664
- stack.pop();
2665
- segment = i + 1;
2666
- }
2667
- }
3360
+ visit(item.children, [...context, prelude], rules - 1);
3361
+ } else if (item.kind === "token" && item.token.type === ";") take();
3362
+ else segment.push(item);
3363
+ take();
3364
+ };
3365
+ visit((0, _animus_ui_properties.componentValues)((0, _animus_ui_properties.tokenize)(css)), [], void 0);
2668
3366
  return found;
2669
3367
  }
2670
- function isSelfReference(declaration) {
2671
- return declaration.property.startsWith("--") && declaration.value.replace(/\s+/g, "") === `var(${declaration.property})`;
2672
- }
2673
- /** Every `var()` read in `value`, nested fallbacks included. */
2674
- function varReads(value) {
2675
- const reads = [];
2676
- for (const match of value.matchAll(/var\(\s*(--[\w-]+)\s*/g)) {
2677
- const after = (match.index ?? 0) + match[0].length;
2678
- if (value[after] !== ",") {
2679
- reads.push({
2680
- name: match[1],
2681
- fallback: void 0
2682
- });
2683
- continue;
3368
+ /** The declaration a `;`-separated segment holds, if any. A bad string or
3369
+ * url makes a declaration invalid, so browsers drop it, and so does this. */
3370
+ function declarationOf(css, segment) {
3371
+ const colon = segment.findIndex((item) => item.kind === "token" && item.token.type === ":");
3372
+ const named = segment.slice(0, Math.max(colon, 0)).filter((i) => !(0, _animus_ui_properties.isSpace)(i));
3373
+ const values = trimSpace(segment.slice(colon + 1));
3374
+ if (colon === -1 || named.length === 0 || (0, _animus_ui_properties.someValue)(values, (v) => v.kind === "token" && (v.token.type === "bad-url" || v.token.type === "bad-string"))) return;
3375
+ const [only] = named;
3376
+ const ident = named.length === 1 && only.kind === "token" && only.token.type === "ident" ? only.token.value : void 0;
3377
+ const custom = ident?.startsWith("--") === true;
3378
+ return {
3379
+ property: custom && ident !== void 0 ? ident : (ident ?? textOf(css, named)).toLowerCase(),
3380
+ custom,
3381
+ value: values.length > 0 ? css.slice(values[0].start, values[values.length - 1].end) : "",
3382
+ values
3383
+ };
3384
+ }
3385
+ function trimSpace(items) {
3386
+ let start = 0;
3387
+ let end = items.length;
3388
+ while (start < end && (0, _animus_ui_properties.isSpace)(items[start])) start += 1;
3389
+ while (end > start && (0, _animus_ui_properties.isSpace)(items[end - 1])) end -= 1;
3390
+ return items.slice(start, end);
3391
+ }
3392
+ /** Whether the value is exactly `var()` of the declared property. */
3393
+ function isSelfReference(declaration, reads) {
3394
+ 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;
3395
+ }
3396
+ /** Whether a read of the declared property sits outside every other read's
3397
+ * fallback, as in `--a: var(--a, red)` or `--a: calc(var(--a) + 1px)`: cyclic
3398
+ * whatever the fallback, as the bare `var(--a)` is. */
3399
+ function readsItselfDirectly(declaration, reads) {
3400
+ return declaration.custom && reads.some((read) => read.name === declaration.property && isDirectRead(read, reads));
3401
+ }
3402
+ /** Whether `read` sits outside every other read's fallback, so it is taken
3403
+ * whenever the declaration is. */
3404
+ function isDirectRead(read, reads) {
3405
+ return !reads.some((outer) => outer !== read && outer.fallback !== void 0 && outer.fallback.start <= read.start && read.end <= outer.fallback.end);
3406
+ }
3407
+ /** Each custom property a `currentVar` writes, with the props writing it. */
3408
+ function currentVarWriters(propConfigJson) {
3409
+ const writers = /* @__PURE__ */ new Map();
3410
+ if (propConfigJson === void 0) return writers;
3411
+ const config = JSON.parse(propConfigJson);
3412
+ for (const [prop, entry] of Object.entries(config)) {
3413
+ if (!entry.currentVar) continue;
3414
+ writers.set(entry.currentVar, [...writers.get(entry.currentVar) ?? [], prop]);
3415
+ }
3416
+ return writers;
3417
+ }
3418
+ /**
3419
+ * Cycles among the `currentVar` writes of one rule, read directly. One rule
3420
+ * is one element under one set of conditions, so its writes are always
3421
+ * active together; writes in different rules, or a read only inside another
3422
+ * read's fallback, are left alone. Each cycle is listed once, in declaration
3423
+ * order.
3424
+ */
3425
+ function currentVarCycles(parsed, currentVars) {
3426
+ if (currentVars.size === 0) return [];
3427
+ const byRule = /* @__PURE__ */ new Map();
3428
+ for (const declaration of parsed) {
3429
+ if (!declaration.custom || !currentVars.has(declaration.property)) continue;
3430
+ const writes = byRule.get(declaration.rule) ?? /* @__PURE__ */ new Map();
3431
+ writes.set(declaration.property, declaration);
3432
+ byRule.set(declaration.rule, writes);
3433
+ }
3434
+ const cycles = [];
3435
+ for (const writes of byRule.values()) {
3436
+ const edges = /* @__PURE__ */ new Map();
3437
+ for (const [name, declaration] of writes) {
3438
+ const reads = (0, _animus_ui_properties.variableReads)(declaration.values);
3439
+ edges.set(name, reads.filter((read) => read.name !== name && writes.has(read.name) && isDirectRead(read, reads)).map((read) => read.name));
3440
+ }
3441
+ for (const component of stronglyConnected(edges)) {
3442
+ if (component.length < 2) continue;
3443
+ cycles.push([...writes.values()].filter((d) => component.includes(d.property)));
2684
3444
  }
2685
- let depth = 1;
2686
- let close = after + 1;
2687
- for (; close < value.length && depth > 0; close += 1) if (value[close] === "(") depth += 1;
2688
- else if (value[close] === ")") depth -= 1;
2689
- reads.push({
2690
- name: match[1],
2691
- fallback: value.slice(after + 1, close - 1).trim()
2692
- });
2693
3445
  }
2694
- return reads;
3446
+ return cycles;
3447
+ }
3448
+ /** Tarjan's strongly connected components of a small graph. */
3449
+ function stronglyConnected(edges) {
3450
+ const index = /* @__PURE__ */ new Map();
3451
+ const low = /* @__PURE__ */ new Map();
3452
+ const stack = [];
3453
+ const onStack = /* @__PURE__ */ new Set();
3454
+ const components = [];
3455
+ const visit = (node) => {
3456
+ index.set(node, index.size);
3457
+ low.set(node, index.get(node));
3458
+ stack.push(node);
3459
+ onStack.add(node);
3460
+ for (const next of edges.get(node) ?? []) if (!index.has(next)) {
3461
+ visit(next);
3462
+ low.set(node, Math.min(low.get(node), low.get(next)));
3463
+ } else if (onStack.has(next)) low.set(node, Math.min(low.get(node), index.get(next)));
3464
+ if (low.get(node) === index.get(node)) {
3465
+ const component = [];
3466
+ let member;
3467
+ do {
3468
+ member = stack.pop();
3469
+ onStack.delete(member);
3470
+ component.push(member);
3471
+ } while (member !== node);
3472
+ components.push(component);
3473
+ }
3474
+ };
3475
+ for (const node of edges.keys()) if (!index.has(node)) visit(node);
3476
+ return components;
3477
+ }
3478
+ /** Whether every read of the declared property sits inside another read's
3479
+ * fallback, as in `--a: var(--b, var(--a))`. */
3480
+ function readsItselfOnlyInFallback(declaration, reads) {
3481
+ if (!declaration.custom) return false;
3482
+ const own = reads.filter((read) => read.name === declaration.property);
3483
+ return own.length > 0 && own.every((read) => reads.some((outer) => outer.fallback !== void 0 && outer.fallback.start <= read.start && read.end <= outer.fallback.end));
2695
3484
  }
2696
3485
  /** Custom properties this declaration sets in a keyframe or names in a
2697
3486
  * transition. */
2698
3487
  function animatedProperties(declaration, discreteRules) {
2699
3488
  const keyframes = declaration.context.find((prelude) => prelude.startsWith("@keyframes"));
2700
- if (keyframes !== void 0 && declaration.property.startsWith("--")) return [{
3489
+ if (keyframes !== void 0 && declaration.custom) return [{
2701
3490
  name: declaration.property,
2702
3491
  how: `set in ${keyframes}`,
2703
3492
  discrete: false
2704
3493
  }];
2705
3494
  if (declaration.property !== "transition" && declaration.property !== "transition-property") return [];
2706
- return topLevelItems(declaration.value).flatMap((item) => {
2707
- const property = item.trim().split(/\s+/)[0] ?? "";
2708
- return property.startsWith("--") ? [{
2709
- name: property,
3495
+ return topLevelItems(declaration.values).flatMap((item) => {
3496
+ const named = item.find((value) => value.kind === "token" && value.token.type === "ident" && value.token.value.startsWith("--"));
3497
+ return named?.kind === "token" ? [{
3498
+ name: named.token.value,
2710
3499
  how: "transitioned",
2711
- discrete: /\ballow-discrete\b/.test(item) || discreteRules.has(declaration.rule)
3500
+ discrete: isDiscrete(item) || discreteRules.has(declaration.rule)
2712
3501
  }] : [];
2713
3502
  });
2714
3503
  }
2715
- function topLevelItems(value) {
2716
- const items = [];
2717
- let depth = 0;
2718
- let start = 0;
2719
- for (let i = 0; i < value.length; i += 1) if (value[i] === "(") depth += 1;
2720
- else if (value[i] === ")") depth -= 1;
2721
- else if (value[i] === "," && depth === 0) {
2722
- items.push(value.slice(start, i));
2723
- start = i + 1;
2724
- }
2725
- items.push(value.slice(start));
3504
+ function isDiscrete(item) {
3505
+ return item.some((value) => value.kind === "token" && value.token.type === "ident" && value.token.value.toLowerCase() === "allow-discrete");
3506
+ }
3507
+ /** The comma-separated items of a value, each its top-level values. */
3508
+ function topLevelItems(values) {
3509
+ const items = [[]];
3510
+ for (const value of values) if (value.kind === "token" && value.token.type === ",") items.push([]);
3511
+ else items[items.length - 1].push(value);
2726
3512
  return items;
2727
3513
  }
2728
3514
  /** Why a property cannot interpolate, or `null` when it can. */
@@ -2732,7 +3518,7 @@ function interpolationGap(registration) {
2732
3518
  return null;
2733
3519
  }
2734
3520
  function componentOwner(manifest) {
2735
- const components = Object.values(manifest.components);
3521
+ const components = Object.values(manifest.components ?? {});
2736
3522
  return (declaration) => {
2737
3523
  const selector = [...declaration.context].reverse().find((prelude) => !prelude.startsWith("@"));
2738
3524
  const classes = (selector ?? "").match(/\.-?[_a-zA-Z][\w-]*/g) ?? [];
@@ -2854,12 +3640,33 @@ function buildAnalysisInputs(opts) {
2854
3640
  externalDirsJson: opts.externalDirs?.length && hasSourceThemeManifests(opts.system) ? JSON.stringify(opts.externalDirs) : null
2855
3641
  };
2856
3642
  if (opts.system.declarationScalesJson) inputs.declarationScalesJson = opts.system.declarationScalesJson;
3643
+ const context = opts.analysisContext;
3644
+ if (context?.skippedSources?.length || context?.unbundledComputedImports || context?.packageDirs?.length) inputs.analysisContextJson = JSON.stringify({
3645
+ skippedSources: context.skippedSources ?? [],
3646
+ unbundledComputedImports: context.unbundledComputedImports ?? false,
3647
+ packageDirs: context.packageDirs ?? []
3648
+ });
2857
3649
  return inputs;
2858
3650
  }
2859
3651
  function hasSourceThemeManifests(system) {
2860
3652
  const json = system.sourceThemeManifestsJson ?? "";
2861
3653
  return json.length > 0 && json !== "{}";
2862
3654
  }
3655
+ /** Systems whose load diagnostics were surfaced: each loaded system reports
3656
+ * them once, not again on every analysis and hot update. The set is per
3657
+ * process. Next's webpack compilers share one analysis in one process, but
3658
+ * its build worker and Turbopack builds analyze in two processes, which
3659
+ * repeat every diagnostic alike, component ones included. */
3660
+ const surfacedSystems = /* @__PURE__ */ new WeakSet();
3661
+ /** The loaded system's own diagnostics, the first time it is analyzed. A
3662
+ * strict error keeps failing every analysis until the system changes. */
3663
+ function systemDiagnostics(system, strict) {
3664
+ const diagnostics = [...collectSelectorAliasDiagnostics(system.selectorAliasesJson), ...systemLoadDiagnostics(system)];
3665
+ const blocking = strict === true && diagnostics.some((d) => d.severity === "error");
3666
+ if (surfacedSystems.has(system) && !blocking) return [];
3667
+ surfacedSystems.add(system);
3668
+ return diagnostics;
3669
+ }
2863
3670
  /**
2864
3671
  * The one analysis invocation both plugins share. Error handling stays at
2865
3672
  * the call site (strict-mode throw vs warn).
@@ -2874,18 +3681,20 @@ function runProjectAnalysis(engineApi, opts) {
2874
3681
  const extractMs = Math.round(performance.now() - t);
2875
3682
  t = performance.now();
2876
3683
  const manifest = JSON.parse(manifestJson);
3684
+ const systemErrors = systemLoadDiagnostics(opts.system).filter((diagnostic) => diagnostic.kind === "error");
3685
+ if (systemErrors.length > 0) manifest.diagnostics = [...systemErrors, ...manifest.diagnostics ?? []];
3686
+ const componentCss = applyUnitFallback(manifest.css);
2877
3687
  const propertyDiagnostics = checkCustomProperties({
2878
3688
  system: opts.system,
2879
3689
  manifest,
2880
- componentCss: applyUnitFallback(manifest.css),
3690
+ componentCss,
2881
3691
  globalCss: manifest.sheets.global
2882
3692
  });
2883
3693
  surfaceManifestDiagnostics(manifest, opts.warn, {
2884
3694
  strict: opts.strict,
2885
3695
  info: opts.info,
2886
3696
  prepend: [
2887
- ...collectSelectorAliasDiagnostics(opts.system.selectorAliasesJson),
2888
- ...systemLoadDiagnostics(opts.system),
3697
+ ...systemDiagnostics(opts.system, opts.strict),
2889
3698
  ...propertyDiagnostics,
2890
3699
  ...opts.extraDiagnostics ?? []
2891
3700
  ]
@@ -2895,7 +3704,7 @@ function runProjectAnalysis(engineApi, opts) {
2895
3704
  manifest,
2896
3705
  manifestJson,
2897
3706
  globalCss: manifest.sheets.global,
2898
- componentCss: applyUnitFallback(manifest.css),
3707
+ componentCss,
2899
3708
  inputs,
2900
3709
  timings: {
2901
3710
  serializeMs,
@@ -2947,6 +3756,47 @@ function serializeStaticCss(config) {
2947
3756
  return json === "{}" ? null : json;
2948
3757
  }
2949
3758
  //#endregion
3759
+ //#region pipeline/property-names.ts
3760
+ function createPropertyNames(declared, prefix, themeNames = []) {
3761
+ const finals = /* @__PURE__ */ new Map();
3762
+ for (const name of [...new Set(declared)].sort()) finals.set(name, `${prefix}-${name}`);
3763
+ const aliases = /* @__PURE__ */ new Map();
3764
+ for (const [name, final] of finals) if (!finals.has(final)) aliases.set(final, name);
3765
+ const others = /* @__PURE__ */ new Map();
3766
+ for (const name of [...new Set(themeNames)].sort()) if (!finals.has(name)) others.set(name, `${prefix}-${name}`);
3767
+ return {
3768
+ identity: (spelling) => finals.has(spelling) ? spelling : aliases.get(spelling) ?? (others.has(spelling) ? spelling : void 0),
3769
+ finalName: (name) => finals.get(name) ?? others.get(name),
3770
+ ambiguous: () => [...others.keys()].filter((name) => aliases.has(name)).map((name) => ({
3771
+ name,
3772
+ contextual: aliases.get(name) ?? name
3773
+ }))
3774
+ };
3775
+ }
3776
+ /**
3777
+ * Gives every custom-property token that names a managed property its final
3778
+ * name, in one pass: declaration keys, `var()` reads at any depth (fallbacks
3779
+ * included), `@property` names, transition-property lists and style queries.
3780
+ * Names are read through the shared CSS tokenizer, whole and with escapes
3781
+ * decoded, escaped leading dashes included, so an undeclared identifier that
3782
+ * contains or starts with a managed name is never renamed in part. Quoted
3783
+ * strings, `url()` and comments are copied unchanged.
3784
+ */
3785
+ function renameCustomProperties(css, names) {
3786
+ let out = "";
3787
+ let copied = 0;
3788
+ for (const token of (0, _animus_ui_properties.tokenize)(css)) {
3789
+ if (token.type !== "ident" || !token.value.startsWith("--")) continue;
3790
+ const spelling = token.value.slice(2);
3791
+ const identity = names.identity(spelling);
3792
+ const final = identity === void 0 ? void 0 : names.finalName(identity);
3793
+ if (final === void 0 || final === spelling) continue;
3794
+ out += `${css.slice(copied, token.start)}--${final}`;
3795
+ copied = token.end;
3796
+ }
3797
+ return out + css.slice(copied);
3798
+ }
3799
+ //#endregion
2950
3800
  //#region pipeline/prefix.ts
2951
3801
  /** Prefix variable references without changing their matching rules. */
2952
3802
  function prefixVariableReferences(prefix, value) {
@@ -2980,6 +3830,61 @@ function applyPrefix(prefix, variableMapJson, variableCss, themeJson, contextual
2980
3830
  }
2981
3831
  return result;
2982
3832
  }
3833
+ /** The runtime's transport variables are `--animus-<prop>`. */
3834
+ const TRANSPORT_NAMESPACE = "animus-";
3835
+ /**
3836
+ * The prefix applied through one map: every variable the theme defines and
3837
+ * every declared contextual variable takes `--${prefix}-${name}`, each
3838
+ * reference renamed once. Each contextual entry carries its declared and
3839
+ * final name, so lookups run on the declared identity and the extractor
3840
+ * emits the final name.
3841
+ */
3842
+ function applyPropertyNames(prefix, artifacts) {
3843
+ const byScale = artifacts.contextualVarsJson ? parseInternalWire(artifacts.contextualVarsJson, "contextualVarsJson (the theme's contextual variable names)") : null;
3844
+ const declared = Object.values(byScale ?? {}).flat();
3845
+ const variableMap = parseInternalWire(artifacts.variableMapJson, "variableMapJson (the theme's token variables)");
3846
+ const managed = createPropertyNames(declared, prefix, [...Object.values(variableMap).map((name) => name.replace(/^--/, "")), ...[...artifacts.variableCss.matchAll(THEME_DEFINITION)].map((m) => m[1])]);
3847
+ const nameConflicts = [...[...new Set(declared)].flatMap((name) => {
3848
+ const final = managed.finalName(name) ?? name;
3849
+ return final.startsWith(TRANSPORT_NAMESPACE) ? [{
3850
+ name,
3851
+ final,
3852
+ reason: "transport"
3853
+ }] : [];
3854
+ }), ...managed.ambiguous().map(({ name, contextual }) => ({
3855
+ name,
3856
+ final: contextual,
3857
+ reason: "theme-variable"
3858
+ }))];
3859
+ const rename = (value) => renameCustomProperties(value, managed);
3860
+ return {
3861
+ variableMapJson: JSON.stringify(Object.fromEntries(Object.entries(variableMap).map(([key, name]) => [key, rename(name)]))),
3862
+ variableCss: rename(artifacts.variableCss),
3863
+ themeJson: renameTokenValues(artifacts.themeJson, rename),
3864
+ contextualVarsJson: byScale === null ? null : JSON.stringify(Object.fromEntries(Object.entries(byScale).map(([scale, names]) => [scale, names.map((name) => ({
3865
+ name,
3866
+ var: managed.finalName(name) ?? name
3867
+ }))]))),
3868
+ declarationScalesJson: artifacts.declarationScalesJson === null ? null : renameDeclarationRecords(artifacts.declarationScalesJson, rename),
3869
+ nameConflicts,
3870
+ contextualProperties: [...new Set(declared)].map((name) => `--${managed.finalName(name) ?? name}`)
3871
+ };
3872
+ }
3873
+ /** A custom property the theme's CSS defines or registers. */
3874
+ const THEME_DEFINITION = /(?:@property\s+|(?:^|[\s;{]))--([\w-]+)(?=\s*[:{])/g;
3875
+ /** Renames inside the token map's values. */
3876
+ function renameTokenValues(themeJson, rename) {
3877
+ const tokens = parseInternalWire(themeJson, "themeJson (the theme's flattened token values)");
3878
+ return JSON.stringify(Object.fromEntries(Object.entries(tokens).map(([path, value]) => [path, rename(value)])));
3879
+ }
3880
+ /** Renames inside every declaration-scale record value. */
3881
+ function renameDeclarationRecords(declarationScalesJson, rename) {
3882
+ const scales = parseInternalWire(declarationScalesJson, "declarationScalesJson (the theme's declaration scales)");
3883
+ return JSON.stringify(Object.fromEntries(Object.entries(scales).map(([name, scale]) => [name, {
3884
+ ...scale,
3885
+ values: Object.fromEntries(Object.entries(scale.values).map(([key, record]) => [key, Object.fromEntries(Object.entries(record).map(([property, value]) => [property, rename(value)]))]))
3886
+ }])));
3887
+ }
2983
3888
  //#endregion
2984
3889
  //#region pipeline/system-config.ts
2985
3890
  /**
@@ -2995,7 +3900,28 @@ function loadSystemConfig(engineApi, opts) {
2995
3900
  let variableCss = registrations.css;
2996
3901
  let contextualVarsJson = config.contextualVarsJson || null;
2997
3902
  let declarationScalesJson = config.declarationScalesJson || null;
2998
- if (opts.prefix) {
3903
+ const declaredNames = Object.values(parseInternalWire(contextualVarsJson ?? "{}", "contextualVarsJson (the theme's contextual variable names)")).flat();
3904
+ let contextualProperties = declaredNames.map((name) => `--${name}`);
3905
+ let legacyPrefixedContextualVars = [];
3906
+ let prefixNameConflicts = [];
3907
+ if (opts.prefix && opts.prefixContextualVars) {
3908
+ const resolved = applyPropertyNames(opts.prefix, {
3909
+ variableMapJson,
3910
+ variableCss,
3911
+ themeJson: scalesJson,
3912
+ contextualVarsJson,
3913
+ declarationScalesJson
3914
+ });
3915
+ variableMapJson = resolved.variableMapJson;
3916
+ variableCss = resolved.variableCss;
3917
+ scalesJson = resolved.themeJson;
3918
+ contextualVarsJson = resolved.contextualVarsJson;
3919
+ contextualProperties = resolved.contextualProperties;
3920
+ prefixNameConflicts = resolved.nameConflicts;
3921
+ declarationScalesJson = resolved.declarationScalesJson;
3922
+ } else if (opts.prefix) {
3923
+ legacyPrefixedContextualVars = declaredNames;
3924
+ contextualProperties = declaredNames.map((name) => `--${opts.prefix}-${name}`);
2999
3925
  const prefixed = applyPrefix(opts.prefix, variableMapJson, variableCss, scalesJson, contextualVarsJson || void 0);
3000
3926
  variableMapJson = prefixed.variableMapJson;
3001
3927
  variableCss = prefixed.variableCss;
@@ -3010,6 +3936,7 @@ function loadSystemConfig(engineApi, opts) {
3010
3936
  variableMapJson,
3011
3937
  variableCss,
3012
3938
  contextualVarsJson,
3939
+ contextualProperties: [...new Set(contextualProperties)],
3013
3940
  selectorAliasesJson: config.selectorAliases || null,
3014
3941
  conditionAliasesJson: config.conditionAliases || null,
3015
3942
  transformSourcesJson: config.transformSources || null,
@@ -3023,6 +3950,8 @@ function loadSystemConfig(engineApi, opts) {
3023
3950
  if (declarationScalesJson) system.declarationScalesJson = declarationScalesJson;
3024
3951
  if (config.propertyRecordsJson) system.propertyRecordsJson = config.propertyRecordsJson;
3025
3952
  if (registrations.invalid.length > 0) system.invalidPropertyRegistrations = registrations.invalid;
3953
+ if (prefixNameConflicts.length > 0) system.prefixNameConflicts = prefixNameConflicts;
3954
+ if (legacyPrefixedContextualVars.length > 0) system.legacyPrefixedContextualVars = [...new Set(legacyPrefixedContextualVars)];
3026
3955
  return system;
3027
3956
  }
3028
3957
  //#endregion
@@ -3033,7 +3962,7 @@ function runStructuralSelfCheck(input) {
3033
3962
  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`);
3034
3963
  for (const { specifier, outcome } of input.externalOutcomes ?? []) if (outcome === "empty") failures.push(`include '${specifier}' resolved but discovered no component sources`);
3035
3964
  else if (outcome === "unresolvable") failures.push(`include '${specifier}' could not be resolved`);
3036
- if (!input.variableCss.includes(":root")) failures.push("No :root variable block found in variable CSS");
3965
+ 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");
3037
3966
  if ((input.assembledCss ?? `${input.variableCss}\n${input.globalCss}\n${input.componentCss}`).includes("__TRANSFORM__")) failures.push("Unresolved __TRANSFORM__ placeholders found in CSS output");
3038
3967
  if (input.componentCount > 0 && (input.componentCss.length > 0 || input.assembledCss !== void 0)) {
3039
3968
  const assembled = input.assembledCss ?? assembleStylesheet({
@@ -3042,9 +3971,16 @@ function runStructuralSelfCheck(input) {
3042
3971
  globalCss: input.globalCss,
3043
3972
  componentCss: input.componentCss
3044
3973
  });
3045
- const baseIdx = assembled.search(/@layer\s+anm-base\s*\{/);
3046
- const variantsIdx = assembled.search(/@layer\s+anm-variants\s*\{/);
3047
- 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})`);
3974
+ const blocks = ANIMUS_LAYERS.map((layer) => ({
3975
+ layer,
3976
+ offset: assembled.search(new RegExp(`@layer\\s+${layer}\\s*\\{`))
3977
+ })).filter(({ offset }) => offset !== -1);
3978
+ const misplaced = blocks.findIndex(({ offset }, index) => index > 0 && offset <= blocks[index - 1].offset);
3979
+ if (misplaced !== -1) {
3980
+ const earlier = blocks[misplaced - 1];
3981
+ const later = blocks[misplaced];
3982
+ failures.push(`CSS layer ordering violated — @layer ${earlier.layer} (offset ${earlier.offset}) must precede @layer ${later.layer} (offset ${later.offset})`);
3983
+ }
3048
3984
  }
3049
3985
  return failures;
3050
3986
  }
@@ -3076,6 +4012,7 @@ function buildDynamicPropConfig(dynamicProps) {
3076
4012
  if (meta.negative) entry.negative = true;
3077
4013
  if (meta.strict) entry.strict = true;
3078
4014
  if (meta.keywords?.length) entry.keywords = meta.keywords;
4015
+ if (meta.currentVar) entry.currentVar = meta.currentVar;
3079
4016
  configEntries[propName] = entry;
3080
4017
  }
3081
4018
  return configEntries;
@@ -3106,188 +4043,6 @@ function buildSystemPropsModule(opts) {
3106
4043
  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` : "");
3107
4044
  }
3108
4045
  //#endregion
3109
- //#region pipeline/tsconfig-paths.ts
3110
- /**
3111
- * tsconfig `paths` → alias pairs, for drivers with no live bundler config.
3112
- * The nearest config declaring `paths` wins wholesale — TS never merges.
3113
- */
3114
- function stripJsonc(text) {
3115
- let out = "";
3116
- let inString = false;
3117
- let inLine = false;
3118
- let inBlock = false;
3119
- for (let i = 0; i < text.length; i++) {
3120
- const ch = text[i];
3121
- const next = text[i + 1];
3122
- if (inLine) {
3123
- if (ch === "\n") {
3124
- inLine = false;
3125
- out += ch;
3126
- }
3127
- continue;
3128
- }
3129
- if (inBlock) {
3130
- if (ch === "*" && next === "/") {
3131
- inBlock = false;
3132
- i++;
3133
- }
3134
- continue;
3135
- }
3136
- if (inString) {
3137
- out += ch;
3138
- if (ch === "\\") {
3139
- out += next ?? "";
3140
- i++;
3141
- } else if (ch === "\"") inString = false;
3142
- continue;
3143
- }
3144
- if (ch === "\"") {
3145
- inString = true;
3146
- out += ch;
3147
- continue;
3148
- }
3149
- if (ch === "/" && next === "/") {
3150
- inLine = true;
3151
- i++;
3152
- continue;
3153
- }
3154
- if (ch === "/" && next === "*") {
3155
- inBlock = true;
3156
- i++;
3157
- continue;
3158
- }
3159
- out += ch;
3160
- }
3161
- return out.replace(/,(\s*[}\]])/g, "$1");
3162
- }
3163
- /** `Object(value) === value` holds for exactly the blocks and lists
3164
- * `JSON.parse` produces; the `[object Object]` tag separates the two. */
3165
- function isJsonBlock(value) {
3166
- return Object(value) === value && Object.prototype.toString.call(value) === "[object Object]";
3167
- }
3168
- /** A block or a list. A declared `paths` OWNS the setting and blocks its
3169
- * parents' even when it contributes nothing usable. */
3170
- function isJsonKeyed(value) {
3171
- return Object(value) === value;
3172
- }
3173
- /** A JSON string, excluding the boxed `String` object, which no path join
3174
- * accepts. */
3175
- function isJsonString(value) {
3176
- return Object(value) !== value && Object.prototype.toString.call(value) === "[object String]";
3177
- }
3178
- function decodePaths(value) {
3179
- if (!isJsonKeyed(value)) return null;
3180
- const decoded = /* @__PURE__ */ new Map();
3181
- for (const [pattern, targets] of Object.entries(value)) {
3182
- const [first] = Array.isArray(targets) ? targets : [];
3183
- if (isJsonString(first)) decoded.set(pattern, first);
3184
- }
3185
- return decoded;
3186
- }
3187
- /** TypeScript accepts one specifier or an array; a non-string member names
3188
- * no config and is dropped here. */
3189
- function decodeExtends(value) {
3190
- return (Array.isArray(value) ? value : [value]).filter(isJsonString);
3191
- }
3192
- function readConfig(path$2) {
3193
- let raw;
3194
- try {
3195
- raw = (0, fs.readFileSync)(path$2, "utf-8");
3196
- } catch {
3197
- return null;
3198
- }
3199
- let parsed;
3200
- try {
3201
- parsed = JSON.parse(stripJsonc(raw));
3202
- } catch {
3203
- return null;
3204
- }
3205
- const root = isJsonBlock(parsed) ? parsed : {};
3206
- const compilerOptions = isJsonBlock(root.compilerOptions) ? root.compilerOptions : {};
3207
- return {
3208
- dir: (0, path.dirname)(path$2),
3209
- baseUrl: isJsonString(compilerOptions.baseUrl) ? compilerOptions.baseUrl : null,
3210
- paths: decodePaths(compilerOptions.paths),
3211
- extends: decodeExtends(root.extends)
3212
- };
3213
- }
3214
- function resolveExtendsTarget(specifier, fromDir) {
3215
- if (specifier.startsWith(".") || (0, path.isAbsolute)(specifier)) {
3216
- const base = (0, path.resolve)(fromDir, specifier);
3217
- for (const candidate of [
3218
- base,
3219
- `${base}.json`,
3220
- (0, path.join)(base, "tsconfig.json")
3221
- ]) if ((0, fs.existsSync)(candidate)) return candidate;
3222
- return null;
3223
- }
3224
- try {
3225
- return require.resolve(specifier.endsWith(".json") ? specifier : `${specifier}/tsconfig.json`, { paths: [fromDir] });
3226
- } catch {
3227
- try {
3228
- return require.resolve(specifier, { paths: [fromDir] });
3229
- } catch {
3230
- return null;
3231
- }
3232
- }
3233
- }
3234
- /** Child-first flattened extends chain starting at `entryPath`. */
3235
- function loadChain(entryPath) {
3236
- const chain = [];
3237
- const visited = /* @__PURE__ */ new Set();
3238
- const queue = [entryPath];
3239
- while (queue.length > 0) {
3240
- const path$3 = queue.shift();
3241
- const key = (0, path.resolve)(path$3);
3242
- if (visited.has(key)) continue;
3243
- visited.add(key);
3244
- const node = readConfig(key);
3245
- if (!node) continue;
3246
- chain.push(node);
3247
- for (const parent of node.extends) {
3248
- const resolved = resolveExtendsTarget(parent, node.dir);
3249
- if (resolved) queue.push(resolved);
3250
- }
3251
- }
3252
- return chain;
3253
- }
3254
- /**
3255
- * The project's tsconfig path aliases as `buildPathAliasesJson` pairs; empty
3256
- * when no readable tsconfig, or no usable `paths`, exists.
3257
- */
3258
- function readTsconfigAliasPairs(rootDir) {
3259
- const chain = loadChain((0, path.join)(rootDir, "tsconfig.json"));
3260
- if (chain.length === 0) return [];
3261
- const pathsOwner = chain.find((node) => node.paths !== null);
3262
- const paths = pathsOwner?.paths ?? null;
3263
- if (pathsOwner === void 0 || paths === null) return [];
3264
- const baseOwner = chain.find((node) => node.baseUrl !== null);
3265
- const baseUrl = baseOwner?.baseUrl ?? null;
3266
- const base = baseOwner !== void 0 && baseUrl !== null ? (0, path.resolve)(baseOwner.dir, baseUrl) : pathsOwner.dir;
3267
- const pairs = [];
3268
- for (const [pattern, target] of paths) {
3269
- const patternStars = pattern.split("*").length - 1;
3270
- const targetStars = target.split("*").length - 1;
3271
- if (pattern === "*" || patternStars > 1 || targetStars > 1) continue;
3272
- if (pattern.endsWith("/*")) {
3273
- if (!target.endsWith("/*")) continue;
3274
- pairs.push({
3275
- pattern: pattern.slice(0, -2),
3276
- target: (0, path.resolve)(base, target.slice(0, -2)),
3277
- kind: "prefix"
3278
- });
3279
- continue;
3280
- }
3281
- if (patternStars > 0 || targetStars > 0) continue;
3282
- pairs.push({
3283
- pattern,
3284
- target: (0, path.resolve)(base, target),
3285
- kind: "exact"
3286
- });
3287
- }
3288
- return pairs;
3289
- }
3290
- //#endregion
3291
4046
  //#region pipeline/error-diagnostics.ts
3292
4047
  function assertNoErrorDiagnostics(diagnostics) {
3293
4048
  const errors = (diagnostics ?? []).filter((diagnostic) => diagnostic.kind === "error");
@@ -3594,6 +4349,12 @@ Object.defineProperty(exports, "enforceExternalTokenContracts", {
3594
4349
  return enforceExternalTokenContracts;
3595
4350
  }
3596
4351
  });
4352
+ Object.defineProperty(exports, "engineImportParser", {
4353
+ enumerable: true,
4354
+ get: function() {
4355
+ return engineImportParser;
4356
+ }
4357
+ });
3597
4358
  Object.defineProperty(exports, "excludeCollectedPackages", {
3598
4359
  enumerable: true,
3599
4360
  get: function() {
@@ -3630,6 +4391,12 @@ Object.defineProperty(exports, "firstOwners", {
3630
4391
  return firstOwners;
3631
4392
  }
3632
4393
  });
4394
+ Object.defineProperty(exports, "generatedModuleCode", {
4395
+ enumerable: true,
4396
+ get: function() {
4397
+ return generatedModuleCode;
4398
+ }
4399
+ });
3633
4400
  Object.defineProperty(exports, "hashReplacementPlans", {
3634
4401
  enumerable: true,
3635
4402
  get: function() {
@@ -3648,6 +4415,12 @@ Object.defineProperty(exports, "isAdvisorySourceDiagnostic", {
3648
4415
  return isAdvisorySourceDiagnostic;
3649
4416
  }
3650
4417
  });
4418
+ Object.defineProperty(exports, "isDeletedSource", {
4419
+ enumerable: true,
4420
+ get: function() {
4421
+ return isDeletedSource;
4422
+ }
4423
+ });
3651
4424
  Object.defineProperty(exports, "isEngineTransformExtension", {
3652
4425
  enumerable: true,
3653
4426
  get: function() {
@@ -3660,6 +4433,18 @@ Object.defineProperty(exports, "isExcludedPackageRelativePath", {
3660
4433
  return isExcludedPackageRelativePath;
3661
4434
  }
3662
4435
  });
4436
+ Object.defineProperty(exports, "isJsonBlock", {
4437
+ enumerable: true,
4438
+ get: function() {
4439
+ return isJsonBlock;
4440
+ }
4441
+ });
4442
+ Object.defineProperty(exports, "isJsonString", {
4443
+ enumerable: true,
4444
+ get: function() {
4445
+ return isJsonString;
4446
+ }
4447
+ });
3663
4448
  Object.defineProperty(exports, "isPathWithinRoot", {
3664
4449
  enumerable: true,
3665
4450
  get: function() {
@@ -3678,12 +4463,24 @@ Object.defineProperty(exports, "loadSystemConfig", {
3678
4463
  return loadSystemConfig;
3679
4464
  }
3680
4465
  });
4466
+ Object.defineProperty(exports, "noKitFilesDiagnostics", {
4467
+ enumerable: true,
4468
+ get: function() {
4469
+ return noKitFilesDiagnostics;
4470
+ }
4471
+ });
3681
4472
  Object.defineProperty(exports, "parseFilesJson", {
3682
4473
  enumerable: true,
3683
4474
  get: function() {
3684
4475
  return parseFilesJson;
3685
4476
  }
3686
4477
  });
4478
+ Object.defineProperty(exports, "parseInternalWire", {
4479
+ enumerable: true,
4480
+ get: function() {
4481
+ return parseInternalWire;
4482
+ }
4483
+ });
3687
4484
  Object.defineProperty(exports, "postProcessCss", {
3688
4485
  enumerable: true,
3689
4486
  get: function() {