@pikacss/core 0.0.52 → 0.0.54

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.
package/dist/index.mjs CHANGED
@@ -489,6 +489,7 @@ const PROPERTY_EFFECTS = {
489
489
  "flex-direction": ["flex-direction"],
490
490
  "flex-flow": ["flex-direction", "flex-wrap"],
491
491
  "flex-grow": ["flex-grow"],
492
+ "flex-line-count": ["flex-line-count"],
492
493
  "flex-shrink": ["flex-shrink"],
493
494
  "flex-wrap": ["flex-wrap"],
494
495
  "float": ["float"],
@@ -1148,6 +1149,7 @@ const PROPERTY_EFFECTS = {
1148
1149
  "widows": ["widows"],
1149
1150
  "width": ["width"],
1150
1151
  "will-change": ["will-change"],
1152
+ "window-drag": ["window-drag"],
1151
1153
  "word-break": ["word-break"],
1152
1154
  "word-space-transform": ["word-space-transform"],
1153
1155
  "word-spacing": ["word-spacing"],
@@ -1301,13 +1303,13 @@ const numOfChars = chars.length;
1301
1303
  * @param num - The non-negative integer to encode.
1302
1304
  * @returns A short alphabetic string unique to the given integer.
1303
1305
  *
1304
- * @remarks Used to generate compact, human-readable atomic style class IDs. The encoding is deterministic: the same number always produces the same string.
1306
+ * @remarks Used to generate compact, human-readable atomic style class IDs. The encoding is deterministic (the same number always produces the same string) and least-significant-digit first: `52` maps to `'aa'`, `53` to `'ba'`, and so on.
1305
1307
  *
1306
1308
  * @example
1307
1309
  * ```ts
1308
1310
  * numberToChars(0) // 'a'
1309
1311
  * numberToChars(51) // 'Z'
1310
- * numberToChars(52) // 'ba'
1312
+ * numberToChars(52) // 'aa'
1311
1313
  * ```
1312
1314
  */
1313
1315
  function numberToChars(num) {
@@ -1378,27 +1380,111 @@ function isNotString(value) {
1378
1380
  return typeof value !== "string";
1379
1381
  }
1380
1382
  /**
1381
- * Tests whether a value conforms to the `InternalPropertyValue` shape: a string, a `[value, fallback[]]` tuple, or nullish.
1383
+ * Type-narrowing guard that returns `true` when the value is a plain object record (non-null, non-array object).
1384
+ * @internal
1385
+ *
1386
+ * @param value - The value to test.
1387
+ * @returns `true` if the value is an object that is neither `null` nor an array, narrowing the type to `Record<string, unknown>`.
1388
+ *
1389
+ * @remarks Used to distinguish nested definition objects (variables, design tokens) from scalar values and arrays while walking configuration trees.
1390
+ *
1391
+ * @example
1392
+ * ```ts
1393
+ * isPlainObjectRecord({ a: 1 }) // true
1394
+ * isPlainObjectRecord([1, 2]) // false
1395
+ * isPlainObjectRecord(null) // false
1396
+ * ```
1397
+ */
1398
+ function isPlainObjectRecord(value) {
1399
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1400
+ }
1401
+ const REGEXP_SPECIAL_CHARS_RE = /[.*+?^${}()|[\]\\/-]/g;
1402
+ /**
1403
+ * Escapes regular expression special characters in a string so it can be embedded in a `RegExp` source as a literal match.
1404
+ * @internal
1405
+ *
1406
+ * @param value - The literal text to escape.
1407
+ * @returns The input with every regex special character prefixed by a backslash.
1408
+ *
1409
+ * @remarks Runtime substitute for `RegExp.escape()` (available in Node.js >= 24 but not yet typed by TypeScript 5.9). Escapes all regex syntax characters plus `/` and `-`; the extra escapes are identity escapes in non-`u`-flag patterns, so the result is safe to embed in the non-unicode-mode regexes built by the engine and integrations.
1410
+ *
1411
+ * @example
1412
+ * ```ts
1413
+ * escapeRegExp('i-icon?') // 'i\\-icon\\?'
1414
+ * new RegExp(`^${escapeRegExp('a.b')}$`).test('a.b') // true
1415
+ * ```
1416
+ */
1417
+ function escapeRegExp(value) {
1418
+ return value.replace(REGEXP_SPECIAL_CHARS_RE, "\\$&");
1419
+ }
1420
+ function isPropertyValueScalar(v) {
1421
+ return typeof v === "string" || typeof v === "number";
1422
+ }
1423
+ /**
1424
+ * Tests whether a value conforms to the `InternalPropertyValue` shape: a string, a number, a `[value, fallback[]]` tuple, or nullish.
1382
1425
  * @internal
1383
1426
  *
1384
1427
  * @param v - The value to inspect.
1385
1428
  * @returns `true` if the value is a valid property value.
1386
1429
  *
1387
- * @remarks During extraction, the engine uses this guard to distinguish CSS property values from nested selector objects or style item arrays.
1430
+ * @remarks During extraction, the engine uses this guard to distinguish CSS property values from nested selector objects or style item arrays. Numbers are accepted because the csstype-based input types allow numeric values such as `0`; they are converted to strings during value normalization.
1388
1431
  *
1389
1432
  * @example
1390
1433
  * ```ts
1391
1434
  * isPropertyValue('red') // true
1435
+ * isPropertyValue(0) // true
1392
1436
  * isPropertyValue(['red', ['blue']]) // true
1437
+ * isPropertyValue(['auto', [0]]) // true
1393
1438
  * isPropertyValue(null) // true
1394
1439
  * isPropertyValue({ color: 'red' }) // false
1395
1440
  * ```
1396
1441
  */
1397
1442
  function isPropertyValue(v) {
1398
- if (Array.isArray(v)) return v.length === 2 && typeof v[0] === "string" && Array.isArray(v[1]) && v[1].every((i) => typeof i === "string");
1443
+ if (Array.isArray(v)) return v.length === 2 && isPropertyValueScalar(v[0]) && Array.isArray(v[1]) && v[1].every(isPropertyValueScalar);
1399
1444
  if (v == null) return true;
1400
- if (typeof v === "string") return true;
1401
- return false;
1445
+ return isPropertyValueScalar(v);
1446
+ }
1447
+ /**
1448
+ * Applies a transform to the parts of a string outside quoted segments,
1449
+ * leaving single- and double-quoted content (e.g. attribute values) untouched.
1450
+ * @internal
1451
+ *
1452
+ * @param str - The string to scan.
1453
+ * @param transform - Transform applied to each unquoted segment.
1454
+ * @returns The reassembled string with transformed unquoted segments and untouched quoted segments.
1455
+ *
1456
+ * @remarks A backslash-escaped character is treated as a literal both inside and outside quoted segments, so a CSS-escaped quote in an identifier (e.g. `.it\'s`) never starts quoted-segment scanning.
1457
+ *
1458
+ * @example
1459
+ * ```ts
1460
+ * transformOutsideQuotes('[data-x="%"] %', s => s.replace(/%/g, 'pk-a'))
1461
+ * // '[data-x="%"] pk-a'
1462
+ * ```
1463
+ */
1464
+ function transformOutsideQuotes(str, transform) {
1465
+ let result = "";
1466
+ let segmentStart = 0;
1467
+ for (let i = 0; i < str.length; i++) {
1468
+ const ch = str[i];
1469
+ if (ch === "\\") {
1470
+ i++;
1471
+ continue;
1472
+ }
1473
+ if (ch === "\"" || ch === "'") {
1474
+ result += transform(str.slice(segmentStart, i));
1475
+ let j = i + 1;
1476
+ while (j < str.length && str[j] !== ch) {
1477
+ if (str[j] === "\\") j++;
1478
+ j++;
1479
+ }
1480
+ const end = Math.min(j, str.length - 1);
1481
+ result += str.slice(i, end + 1);
1482
+ i = end;
1483
+ segmentStart = i + 1;
1484
+ }
1485
+ }
1486
+ result += transform(str.slice(segmentStart));
1487
+ return result;
1402
1488
  }
1403
1489
  /**
1404
1490
  * Serializes a value to a JSON string for use as a deterministic cache key.
@@ -1470,23 +1556,25 @@ function appendAutocompleteEntries(set, values) {
1470
1556
  * @param entries - A record mapping keys to single or arrayed string values, or `undefined` to skip.
1471
1557
  * @returns `true` if at least one entry was added or extended; `false` if the input was nullish or empty.
1472
1558
  *
1473
- * @remarks Existing map entries are extended (not replaced) with the new values, maintaining all previously registered suggestions for a given key. This accumulative behavior allows multiple plugins to contribute value suggestions for the same property.
1559
+ * @remarks Existing map entries are extended (not replaced) with the new values, maintaining all previously registered suggestions for a given key. This accumulative behavior allows multiple plugins to contribute value suggestions for the same property. Values already present for a key are skipped, and the function returns `false` when nothing new was added.
1474
1560
  *
1475
1561
  * @example
1476
1562
  * ```ts
1477
1563
  * const map = new Map<string, string[]>()
1478
1564
  * appendAutocompleteRecordEntries(map, { color: ['red', 'blue'] }) // true
1479
1565
  * appendAutocompleteRecordEntries(map, { color: 'green' }) // true (now ['red','blue','green'])
1566
+ * appendAutocompleteRecordEntries(map, { color: 'green' }) // false (no change)
1480
1567
  * ```
1481
1568
  */
1482
1569
  function appendAutocompleteRecordEntries(map, entries) {
1483
1570
  if (entries == null) return false;
1484
1571
  let changed = false;
1485
1572
  for (const [key, value] of Object.entries(entries)) {
1486
- const nextValues = [value].flat();
1487
- if (nextValues.length === 0) continue;
1488
- const current = map.get(key) || [];
1489
- map.set(key, [...current, ...nextValues]);
1573
+ const current = map.get(key);
1574
+ const existing = new Set(current);
1575
+ const added = [value].flat().filter((v) => !existing.has(v));
1576
+ if (added.length === 0) continue;
1577
+ map.set(key, [...current ?? [], ...added]);
1490
1578
  changed = true;
1491
1579
  }
1492
1580
  return changed;
@@ -1552,17 +1640,25 @@ function renderCSSStyleBlocks(blocks, isFormatted, depth = 0) {
1552
1640
  const propertySpace = isFormatted ? " " : "";
1553
1641
  const lineEnd = isFormatted ? "\n" : "";
1554
1642
  const lines = [];
1555
- blocks.forEach(({ properties, children }, selector) => {
1556
- if (properties.length === 0 && (children == null || children.size === 0)) return;
1643
+ blocks.forEach((blockBody, selector) => {
1644
+ if (hasRenderableBlockContent(blockBody) === false) return;
1645
+ const { properties, children } = blockBody;
1646
+ const childrenCss = children != null && children.size > 0 ? renderCSSStyleBlocks(children, isFormatted, depth + 1) : "";
1557
1647
  lines.push(...[
1558
1648
  `${blockIndent}${selector}${selectorEnd}{`,
1559
1649
  ...properties.map(({ property, value }) => `${blockBodyIndent}${property}:${propertySpace}${value};`),
1560
- ...children != null && children.size > 0 ? [renderCSSStyleBlocks(children, isFormatted, depth + 1)] : [],
1650
+ ...childrenCss !== "" ? [childrenCss] : [],
1561
1651
  `${blockIndent}}`
1562
1652
  ]);
1563
1653
  });
1564
1654
  return lines.join(lineEnd);
1565
1655
  }
1656
+ function hasRenderableBlockContent(blockBody) {
1657
+ if (blockBody.properties.length > 0) return true;
1658
+ if (blockBody.children == null) return false;
1659
+ for (const child of blockBody.children.values()) if (hasRenderableBlockContent(child)) return true;
1660
+ return false;
1661
+ }
1566
1662
  //#endregion
1567
1663
  //#region src/atomic-style.ts
1568
1664
  /**
@@ -1788,9 +1884,48 @@ const LAYER_SELECTOR_PREFIX = "@layer ";
1788
1884
  * Global regex matching all occurrences of {@link ATOMIC_STYLE_ID_PLACEHOLDER}
1789
1885
  * for batch replacement in selector templates.
1790
1886
  *
1887
+ * A `%` directly preceded by a digit is treated as a literal percentage
1888
+ * (e.g. `@supports (width: 50%)`), not a placeholder.
1889
+ *
1890
+ * @internal
1891
+ */
1892
+ const ATOMIC_STYLE_ID_PLACEHOLDER_RE_GLOBAL = /(?<!\d)%/g;
1893
+ /**
1894
+ * Tests whether a selector string contains the atomic style ID placeholder.
1895
+ *
1896
+ * Uses the same digit-protection rule as {@link ATOMIC_STYLE_ID_PLACEHOLDER_RE_GLOBAL}:
1897
+ * `%` preceded by a digit is a literal percentage, not a placeholder.
1898
+ * A `%` inside single- or double-quoted content (e.g. an attribute value)
1899
+ * is never treated as a placeholder.
1900
+ *
1791
1901
  * @internal
1792
1902
  */
1793
- const ATOMIC_STYLE_ID_PLACEHOLDER_RE_GLOBAL = /%/g;
1903
+ function hasAtomicStyleIdPlaceholder(selector) {
1904
+ if (!selector.includes("\"") && !selector.includes("'")) {
1905
+ ATOMIC_STYLE_ID_PLACEHOLDER_RE_GLOBAL.lastIndex = 0;
1906
+ return ATOMIC_STYLE_ID_PLACEHOLDER_RE_GLOBAL.test(selector);
1907
+ }
1908
+ let found = false;
1909
+ transformOutsideQuotes(selector, (segment) => {
1910
+ if (found === false) {
1911
+ ATOMIC_STYLE_ID_PLACEHOLDER_RE_GLOBAL.lastIndex = 0;
1912
+ found = ATOMIC_STYLE_ID_PLACEHOLDER_RE_GLOBAL.test(segment);
1913
+ }
1914
+ return segment;
1915
+ });
1916
+ return found;
1917
+ }
1918
+ /**
1919
+ * Replaces every atomic style ID placeholder in a selector string with the given atomic style ID.
1920
+ *
1921
+ * Applies the same rules as {@link hasAtomicStyleIdPlaceholder}: a `%` directly
1922
+ * preceded by a digit is a literal percentage, and quoted content is never rewritten.
1923
+ *
1924
+ * @internal
1925
+ */
1926
+ function replaceAtomicStyleIdPlaceholder(selector, atomicStyleId) {
1927
+ return transformOutsideQuotes(selector, (segment) => segment.replace(ATOMIC_STYLE_ID_PLACEHOLDER_RE_GLOBAL, () => atomicStyleId));
1928
+ }
1794
1929
  //#endregion
1795
1930
  //#region src/extractor.ts
1796
1931
  function replaceBySplitAndJoin(str, split, mapFn, join) {
@@ -1811,7 +1946,7 @@ const ATTRIBUTE_SUFFIX_MATCH_RE_GLOBAL = /\$=/g;
1811
1946
  * @param options.defaultSelector - The selector template that replaces `$` placeholders.
1812
1947
  * @returns An array of normalized selector strings with all placeholders resolved.
1813
1948
  *
1814
- * @remarks The `$` character in a selector is replaced with the engine's `defaultSelector`. The `%` character is the atomic style ID placeholder, preserved for later substitution. Attribute suffix matches (`$=`) are protected from the `$` replacement.
1949
+ * @remarks The `$` character in a selector is replaced with the engine's `defaultSelector`. The `%` character is the atomic style ID placeholder, preserved for later substitution. Attribute suffix matches (`$=`) are protected from the `$` replacement. Content inside single or double quotes is never rewritten, and a `%` directly preceded by a digit (e.g. `@supports (width: 50%)`) is treated as a literal percentage.
1815
1950
  *
1816
1951
  * @example
1817
1952
  * ```ts
@@ -1820,7 +1955,7 @@ const ATTRIBUTE_SUFFIX_MATCH_RE_GLOBAL = /\$=/g;
1820
1955
  * ```
1821
1956
  */
1822
1957
  function normalizeSelectors({ selectors, defaultSelector }) {
1823
- return selectors.map((s) => replaceBySplitAndJoin(s.replace(RE_SPLIT, ","), ATOMIC_STYLE_ID_PLACEHOLDER_RE_GLOBAL, (a) => replaceBySplitAndJoin(a, ATTRIBUTE_SUFFIX_MATCH_RE_GLOBAL, (b) => replaceBySplitAndJoin(b, DEFAULT_SELECTOR_PLACEHOLDER_RE_GLOBAL, null, defaultSelector), ATTRIBUTE_SUFFIX_MATCH), "%"));
1958
+ return selectors.map((s) => transformOutsideQuotes(s, (segment) => replaceBySplitAndJoin(segment.replace(RE_SPLIT, ","), ATOMIC_STYLE_ID_PLACEHOLDER_RE_GLOBAL, (a) => replaceBySplitAndJoin(a, ATTRIBUTE_SUFFIX_MATCH_RE_GLOBAL, (b) => replaceBySplitAndJoin(b, DEFAULT_SELECTOR_PLACEHOLDER_RE_GLOBAL, null, defaultSelector), ATTRIBUTE_SUFFIX_MATCH), "%")));
1824
1959
  }
1825
1960
  /**
1826
1961
  * Normalizes a raw `InternalPropertyValue` into the extraction output format: an array of trimmed, deduplicated CSS value strings with fallbacks ordered before the primary value, or `null`/`undefined` to signal removal.
@@ -1829,11 +1964,12 @@ function normalizeSelectors({ selectors, defaultSelector }) {
1829
1964
  * @param value - The raw property value to normalize.
1830
1965
  * @returns An array of CSS value strings (fallbacks first, primary last), or `null`/`undefined` for removal.
1831
1966
  *
1832
- * @remarks For tuple values `[primary, fallbacks]`, duplicates among fallbacks are removed and the primary value is appended last so CSS cascade uses it as the effective value while older browsers fall back to earlier entries.
1967
+ * @remarks For tuple values `[primary, fallbacks]`, duplicates among fallbacks are removed and the primary value is appended last so CSS cascade uses it as the effective value while older browsers fall back to earlier entries. Numeric values (e.g. `0`) are converted to strings.
1833
1968
  *
1834
1969
  * @example
1835
1970
  * ```ts
1836
1971
  * normalizeValue('red') // ['red']
1972
+ * normalizeValue(0) // ['0']
1837
1973
  * normalizeValue(['red', ['blue']]) // ['blue', 'red']
1838
1974
  * normalizeValue(null) // null
1839
1975
  * ```
@@ -1842,11 +1978,11 @@ function normalizeValue(value) {
1842
1978
  if (value == null) return value;
1843
1979
  if (Array.isArray(value)) {
1844
1980
  const [primary, fallbacks] = value;
1845
- const p = primary.trim();
1981
+ const p = String(primary).trim();
1846
1982
  const seen = new Set([p]);
1847
1983
  const result = [];
1848
1984
  for (const v of fallbacks) {
1849
- const s = v.trim();
1985
+ const s = String(v).trim();
1850
1986
  if (!seen.has(s)) {
1851
1987
  seen.add(s);
1852
1988
  result.push(s);
@@ -1855,7 +1991,7 @@ function normalizeValue(value) {
1855
1991
  result.push(p);
1856
1992
  return result;
1857
1993
  }
1858
- return [value.trim()];
1994
+ return [String(value).trim()];
1859
1995
  }
1860
1996
  /**
1861
1997
  * Recursively walks a style definition tree, extracting each CSS property-value pair into a flat list of `ExtractedStyleContent` entries with their full selector chain.
@@ -1885,18 +2021,24 @@ function normalizeValue(value) {
1885
2021
  * ```
1886
2022
  */
1887
2023
  async function extract({ styleDefinition, levels = [], result = [], defaultSelector, transformSelectors, transformStyleItems, transformStyleDefinitions }) {
1888
- for (const definition of await transformStyleDefinitions([styleDefinition])) for (const [k, v] of Object.entries(definition)) if (isPropertyValue(v)) {
1889
- const selector = normalizeSelectors({
1890
- selectors: await transformSelectors(levels),
1891
- defaultSelector
1892
- });
1893
- if (selector.length === 0 || selector.every((s) => s.includes("%") === false)) selector.push(defaultSelector);
1894
- result.push({
1895
- selector,
1896
- property: toKebab(k),
1897
- value: normalizeValue(v)
1898
- });
1899
- } else if (Array.isArray(v)) for (const styleItem of await transformStyleItems(v)) {
2024
+ let scopeSelector;
2025
+ const resolveScopeSelector = async () => {
2026
+ if (scopeSelector == null) {
2027
+ const selector = normalizeSelectors({
2028
+ selectors: await transformSelectors(levels),
2029
+ defaultSelector
2030
+ });
2031
+ if (selector.length === 0 || selector.every((s) => hasAtomicStyleIdPlaceholder(s) === false)) selector.push(defaultSelector);
2032
+ scopeSelector = selector;
2033
+ }
2034
+ return scopeSelector;
2035
+ };
2036
+ for (const definition of await transformStyleDefinitions([styleDefinition])) for (const [k, v] of Object.entries(definition)) if (isPropertyValue(v)) result.push({
2037
+ selector: await resolveScopeSelector(),
2038
+ property: toKebab(k),
2039
+ value: normalizeValue(v)
2040
+ });
2041
+ else if (Array.isArray(v)) for (const styleItem of await transformStyleItems(v)) {
1900
2042
  if (typeof styleItem === "string") continue;
1901
2043
  await extract({
1902
2044
  styleDefinition: styleItem,
@@ -2100,7 +2242,7 @@ function resolvePlugins(plugins) {
2100
2242
  * ```ts
2101
2243
  * export default defineEnginePlugin({
2102
2244
  * name: 'my-plugin',
2103
- * configureRawConfig: (config) => ({ ...config, important: true }),
2245
+ * configureRawConfig: (config) => ({ ...config, important: { default: true } }),
2104
2246
  * })
2105
2247
  * ```
2106
2248
  */
@@ -2110,8 +2252,10 @@ function defineEnginePlugin(plugin) {
2110
2252
  /* c8 ignore end */
2111
2253
  //#endregion
2112
2254
  //#region src/plugins/important.ts
2255
+ const TRAILING_IMPORTANT_RE = /!\s*important\s*$/i;
2113
2256
  function appendImportant(v) {
2114
- return v.endsWith("!important") ? v : `${v} !important`;
2257
+ const value = String(v);
2258
+ return TRAILING_IMPORTANT_RE.test(value) ? value : `${value} !important`;
2115
2259
  }
2116
2260
  function modifyPropertyValue(value) {
2117
2261
  if (value == null) return null;
@@ -2123,7 +2267,7 @@ function modifyPropertyValue(value) {
2123
2267
  *
2124
2268
  * @returns An `EnginePlugin` that intercepts `transformStyleDefinitions` to conditionally append `!important` to every property value.
2125
2269
  *
2126
- * @remarks When `EngineConfig.important.default` is `true`, all property values receive `!important` unless the style definition explicitly sets `__important: false`. Individual style definitions can also opt-in with `__important: true` regardless of the default.
2270
+ * @remarks When `EngineConfig.important.default` is `true`, all property values receive `!important` unless the style definition explicitly sets `__important: false`. Individual style definitions can also opt-in with `__important: true` regardless of the default. An explicit `__important` flag is propagated into nested selector blocks (which may override it with their own explicit flag). The `__shortcut` reference is never modified.
2127
2271
  *
2128
2272
  * @example
2129
2273
  * ```ts
@@ -2132,6 +2276,16 @@ function modifyPropertyValue(value) {
2132
2276
  */
2133
2277
  function important() {
2134
2278
  let defaultValue;
2279
+ function propagateExplicitFlag(v, flag) {
2280
+ if (Array.isArray(v)) return v.map((item) => typeof item === "object" && item !== null && !Array.isArray(item) ? {
2281
+ __important: flag,
2282
+ ...item
2283
+ } : item);
2284
+ return {
2285
+ __important: flag,
2286
+ ...v
2287
+ };
2288
+ }
2135
2289
  return defineEnginePlugin({
2136
2290
  name: "core:important",
2137
2291
  rawConfigConfigured(config) {
@@ -2146,10 +2300,13 @@ function important() {
2146
2300
  transformStyleDefinitions(styleDefinitions) {
2147
2301
  return styleDefinitions.map((styleDefinition) => {
2148
2302
  const { __important, ...rest } = styleDefinition;
2149
- if ((__important ?? defaultValue) === false) return rest;
2303
+ const explicit = __important;
2304
+ const important = explicit ?? defaultValue;
2305
+ if (important === false && explicit == null) return rest;
2150
2306
  return Object.fromEntries(Object.entries(rest).map(([k, v]) => {
2151
- if (isPropertyValue(v)) return [k, modifyPropertyValue(v)];
2152
- return [k, v];
2307
+ if (k === "__shortcut") return [k, v];
2308
+ if (isPropertyValue(v)) return [k, important ? modifyPropertyValue(v) : v];
2309
+ return [k, explicit == null ? v : propagateExplicitFlag(v, explicit)];
2153
2310
  }));
2154
2311
  });
2155
2312
  }
@@ -2195,11 +2352,12 @@ function keyframes() {
2195
2352
  }
2196
2353
  };
2197
2354
  engine.keyframes.add(...configList);
2198
- engine.addPreflight((engine) => {
2355
+ engine.addPreflight((engine, _isFormatted, ctx) => {
2199
2356
  const maybeUsedName = /* @__PURE__ */ new Set();
2200
- engine.store.atomicStyles.forEach(({ content: { property, value } }) => {
2357
+ engine.store.atomicStyles.forEach(({ content: { property, value } }, id) => {
2358
+ if (ctx?.usedAtomicStyleIds != null && ctx.usedAtomicStyleIds.has(id) === false) return;
2201
2359
  if (property === "animation-name") {
2202
- value.forEach((name) => maybeUsedName.add(name));
2360
+ value.forEach((name) => addToSet(maybeUsedName, ...name.split(",").map((v) => v.trim())));
2203
2361
  return;
2204
2362
  }
2205
2363
  if (property === "animation") value.forEach((value) => {
@@ -2211,7 +2369,7 @@ function keyframes() {
2211
2369
  const maybeUsedKeyframes = Array.from(engine.keyframes.store.values()).filter(({ name, frames, pruneUnused }) => (pruneUnused === false || maybeUsedName.has(name)) && frames != null);
2212
2370
  const preflightDefinition = {};
2213
2371
  maybeUsedKeyframes.forEach(({ name, frames }) => {
2214
- preflightDefinition[`@keyframes ${name}`] = Object.fromEntries(Object.entries(frames).map(([frame, properties]) => [frame, properties]));
2372
+ preflightDefinition[`@keyframes ${name}`] = frames;
2215
2373
  });
2216
2374
  return preflightDefinition;
2217
2375
  });
@@ -2268,6 +2426,10 @@ function stripGlobalFlag(re) {
2268
2426
  var AbstractResolver = class {
2269
2427
  /** Cache of previously resolved input-string → result pairs. */
2270
2428
  _resolvedResultsMap = /* @__PURE__ */ new Map();
2429
+ /** Negative cache of input strings that matched no rule at all. Retryable-unresolved dynamic results (a matched dynamic rule whose value fn returned nullish) are never stored here. */
2430
+ _unmatchedStrings = /* @__PURE__ */ new Set();
2431
+ /** Index of static rules keyed by their exact match string (first registered rule wins on string collisions), giving O(1) static lookups. */
2432
+ _staticRulesByString = /* @__PURE__ */ new Map();
2271
2433
  /** Registry of static rules keyed by their unique key. */
2272
2434
  staticRulesMap = /* @__PURE__ */ new Map();
2273
2435
  /** Registry of dynamic rules keyed by their unique key. */
@@ -2286,7 +2448,7 @@ var AbstractResolver = class {
2286
2448
  * @param rule - The static rule to register.
2287
2449
  * @returns `this` for chaining.
2288
2450
  *
2289
- * @remarks Overwrites any existing static rule with the same key.
2451
+ * @remarks Overwrites any existing static rule with the same key. The entire resolution cache is cleared because cached results (including recursively expanded ones) may depend on the previous rule.
2290
2452
  *
2291
2453
  * @example
2292
2454
  * ```ts
@@ -2295,7 +2457,14 @@ var AbstractResolver = class {
2295
2457
  */
2296
2458
  addStaticRule(rule) {
2297
2459
  log.debug(`Adding static rule: ${rule.key}`);
2460
+ const previous = this.staticRulesMap.get(rule.key);
2298
2461
  this.staticRulesMap.set(rule.key, rule);
2462
+ if (previous != null) {
2463
+ if (previous.string !== rule.string) this._reindexStaticRuleString(previous.string);
2464
+ this._reindexStaticRuleString(rule.string);
2465
+ } else if (this._staticRulesByString.has(rule.string) === false) this._staticRulesByString.set(rule.string, rule);
2466
+ this._resolvedResultsMap.clear();
2467
+ this._unmatchedStrings.clear();
2299
2468
  return this;
2300
2469
  }
2301
2470
  /**
@@ -2304,7 +2473,7 @@ var AbstractResolver = class {
2304
2473
  * @param key - The key of the static rule to remove.
2305
2474
  * @returns `this` for chaining.
2306
2475
  *
2307
- * @remarks Logs a warning if the key does not exist. Also evicts the cached result for the rule's input string.
2476
+ * @remarks Logs a warning if the key does not exist. The entire resolution cache is cleared because cached results (including recursively expanded ones) may depend on the removed rule.
2308
2477
  *
2309
2478
  * @example
2310
2479
  * ```ts
@@ -2319,16 +2488,32 @@ var AbstractResolver = class {
2319
2488
  }
2320
2489
  log.debug(`Removing static rule: ${key}`);
2321
2490
  this.staticRulesMap.delete(key);
2322
- this._resolvedResultsMap.delete(rule.string);
2491
+ if (this._staticRulesByString.get(rule.string) === rule) this._reindexStaticRuleString(rule.string);
2492
+ this._resolvedResultsMap.clear();
2493
+ this._unmatchedStrings.clear();
2323
2494
  return this;
2324
2495
  }
2325
2496
  /**
2497
+ * Recomputes the string-index entry for a given match string after a rule mutation.
2498
+ *
2499
+ * @param string - The match string whose index entry should be recomputed.
2500
+ *
2501
+ * @remarks Scans `staticRulesMap` in insertion order so the first registered rule matching the string wins, mirroring the pre-index linear-scan behavior. Removes the entry when no rule matches the string anymore.
2502
+ */
2503
+ _reindexStaticRuleString(string) {
2504
+ for (const rule of this.staticRulesMap.values()) if (rule.string === string) {
2505
+ this._staticRulesByString.set(string, rule);
2506
+ return;
2507
+ }
2508
+ this._staticRulesByString.delete(string);
2509
+ }
2510
+ /**
2326
2511
  * Registers a dynamic rule in the resolver.
2327
2512
  *
2328
2513
  * @param rule - The dynamic rule to register.
2329
2514
  * @returns `this` for chaining.
2330
2515
  *
2331
- * @remarks Overwrites any existing dynamic rule with the same key.
2516
+ * @remarks Overwrites any existing dynamic rule with the same key. The entire resolution cache is cleared because cached results (including recursively expanded ones) may depend on the previous rule.
2332
2517
  *
2333
2518
  * @example
2334
2519
  * ```ts
@@ -2338,6 +2523,8 @@ var AbstractResolver = class {
2338
2523
  addDynamicRule(rule) {
2339
2524
  log.debug(`Adding dynamic rule: ${rule.key}`);
2340
2525
  this.dynamicRulesMap.set(rule.key, rule);
2526
+ this._resolvedResultsMap.clear();
2527
+ this._unmatchedStrings.clear();
2341
2528
  return this;
2342
2529
  }
2343
2530
  /**
@@ -2346,7 +2533,7 @@ var AbstractResolver = class {
2346
2533
  * @param key - The key of the dynamic rule to remove.
2347
2534
  * @returns `this` for chaining.
2348
2535
  *
2349
- * @remarks Iterates through all cached results and deletes any whose input string matches the removed rule's pattern. Logs a warning if the key does not exist.
2536
+ * @remarks Logs a warning if the key does not exist. The entire resolution cache is cleared because cached results (including recursively expanded ones) may depend on the removed rule.
2350
2537
  *
2351
2538
  * @example
2352
2539
  * ```ts
@@ -2354,19 +2541,14 @@ var AbstractResolver = class {
2354
2541
  * ```
2355
2542
  */
2356
2543
  removeDynamicRule(key) {
2357
- const rule = this.dynamicRulesMap.get(key);
2358
- if (rule == null) {
2544
+ if (this.dynamicRulesMap.get(key) == null) {
2359
2545
  log.warn(`Dynamic rule not found for removal: ${key}`);
2360
2546
  return this;
2361
2547
  }
2362
2548
  log.debug(`Removing dynamic rule: ${key}`);
2363
- const matchedResolvedStringList = Array.from(this._resolvedResultsMap.keys()).filter((string) => {
2364
- rule.stringPattern.lastIndex = 0;
2365
- return rule.stringPattern.test(string);
2366
- });
2367
2549
  this.dynamicRulesMap.delete(key);
2368
- matchedResolvedStringList.forEach((string) => this._resolvedResultsMap.delete(string));
2369
- log.debug(` - Cleared ${matchedResolvedStringList.length} cached results`);
2550
+ this._resolvedResultsMap.clear();
2551
+ this._unmatchedStrings.clear();
2370
2552
  return this;
2371
2553
  }
2372
2554
  /**
@@ -2375,7 +2557,7 @@ var AbstractResolver = class {
2375
2557
  * @param string - The input string to resolve.
2376
2558
  * @returns The resolved result wrapper, or `null`/`undefined` if no rule matches.
2377
2559
  *
2378
- * @remarks Results are cached for subsequent calls. Invokes `onResolved` after a successful match. Dynamic rule matching is async because `createResolved` may return a `Promise`.
2560
+ * @remarks Results are cached for subsequent calls. Invokes `onResolved` after a successful match. Dynamic rule matching is async because `createResolved` may return a `Promise`. When a dynamic rule's `createResolved` returns `undefined`/`null`, the input is treated as unresolved and no cache entry is stored, so a later resolve call re-invokes the rule.
2379
2561
  *
2380
2562
  * @example
2381
2563
  * ```ts
@@ -2388,7 +2570,11 @@ var AbstractResolver = class {
2388
2570
  log.debug(`Resolved from cache: ${string}`);
2389
2571
  return existedResult;
2390
2572
  }
2391
- const staticRule = Array.from(this.staticRulesMap.values()).find((rule) => rule.string === string);
2573
+ if (this._unmatchedStrings.has(string)) {
2574
+ log.debug(`Resolution failed (cached): ${string}`);
2575
+ return;
2576
+ }
2577
+ const staticRule = this._staticRulesByString.get(string);
2392
2578
  if (staticRule != null) {
2393
2579
  log.debug(`Resolved by static rule: ${staticRule.key}`);
2394
2580
  const resolvedResult = { value: staticRule.resolved };
@@ -2407,13 +2593,19 @@ var AbstractResolver = class {
2407
2593
  }
2408
2594
  }
2409
2595
  if (dynamicRule != null && matched != null) {
2596
+ const value = await dynamicRule.createResolved(matched);
2597
+ if (value == null) {
2598
+ log.debug(`Dynamic rule "${dynamicRule.key}" returned no value for "${string}", treating as unresolved (not cached)`);
2599
+ return;
2600
+ }
2410
2601
  log.debug(`Resolved by dynamic rule: ${dynamicRule.key}`);
2411
- const resolvedResult = { value: await dynamicRule.createResolved(matched) };
2602
+ const resolvedResult = { value };
2412
2603
  this._resolvedResultsMap.set(string, resolvedResult);
2413
2604
  this.onResolved(string, "dynamic", resolvedResult);
2414
2605
  return resolvedResult;
2415
2606
  }
2416
2607
  log.debug(`Resolution failed for: ${string}`);
2608
+ this._unmatchedStrings.add(string);
2417
2609
  }
2418
2610
  /**
2419
2611
  * Updates or creates the cached resolved result for a given input string.
@@ -2425,10 +2617,11 @@ var AbstractResolver = class {
2425
2617
  *
2426
2618
  * @example
2427
2619
  * ```ts
2428
- * resolver._setResolvedResult('hover', ['&:hover'])
2620
+ * resolver._setResolvedResult('hover', ['$:hover'])
2429
2621
  * ```
2430
2622
  */
2431
2623
  _setResolvedResult(string, resolved) {
2624
+ this._unmatchedStrings.delete(string);
2432
2625
  const resolvedResult = this._resolvedResultsMap.get(string);
2433
2626
  if (resolvedResult) {
2434
2627
  resolvedResult.value = resolved;
@@ -2449,7 +2642,7 @@ var AbstractResolver = class {
2449
2642
  * ```ts
2450
2643
  * class SelectorResolver extends RecursiveResolver<string> { }
2451
2644
  * const result = await resolver.resolve('hover-focus')
2452
- * // ['&:hover', '&:focus'] after recursive expansion
2645
+ * // ['$:hover', '$:focus'] after recursive expansion
2453
2646
  * ```
2454
2647
  */
2455
2648
  var RecursiveResolver = class extends AbstractResolver {
@@ -2485,6 +2678,13 @@ var RecursiveResolver = class extends AbstractResolver {
2485
2678
  return result;
2486
2679
  }
2487
2680
  };
2681
+ function createDynamicResolvedFactory(fn) {
2682
+ return async (match) => {
2683
+ const value = await fn(match);
2684
+ if (value == null) return value;
2685
+ return [value].flat(1);
2686
+ };
2687
+ }
2488
2688
  /**
2489
2689
  * Normalizes a user-supplied rule shorthand into a `ResolvedRuleConfig`, a plain redirect string, or `undefined`.
2490
2690
  * @internal
@@ -2499,61 +2699,44 @@ var RecursiveResolver = class extends AbstractResolver {
2499
2699
  * - **Tuple**: `[string, T | T[]]` for static rules, `[RegExp, fn, autocomplete?]` for dynamic rules.
2500
2700
  * - **Object**: `{ [keyName]: string | RegExp, value: T | fn, autocomplete?: string[] }`.
2501
2701
  *
2702
+ * A dynamic rule's value function may return `undefined`/`null` to signal a retryable-unresolved result: the resolver treats the input as unresolved and stores no cache entry, so the rule is re-invoked on a later resolve call.
2703
+ *
2502
2704
  * @example
2503
2705
  * ```ts
2504
- * resolveRuleConfig(['hover', '&:hover'], 'selector')
2706
+ * resolveRuleConfig(['hover', '$:hover'], 'selector')
2505
2707
  * // { type: 'static', rule: { key: 'hover', ... }, autocomplete: ['hover'] }
2506
2708
  * ```
2507
2709
  */
2508
2710
  function resolveRuleConfig(config, keyName) {
2509
2711
  if (typeof config === "string") return config;
2510
- if (Array.isArray(config)) {
2511
- if (typeof config[0] === "string" && typeof config[1] !== "function") return {
2512
- type: "static",
2513
- rule: {
2514
- key: config[0],
2515
- string: config[0],
2516
- resolved: [config[1]].flat(1)
2517
- },
2518
- autocomplete: [config[0]]
2519
- };
2520
- if (config[0] instanceof RegExp && typeof config[1] === "function") {
2521
- const fn = config[1];
2522
- return {
2523
- type: "dynamic",
2524
- rule: {
2525
- key: config[0].source,
2526
- stringPattern: stripGlobalFlag(config[0]),
2527
- createResolved: async (match) => [await fn(match)].flat(1)
2528
- },
2529
- autocomplete: config[2] != null ? [config[2]].flat(1) : []
2530
- };
2531
- }
2532
- return;
2533
- }
2534
2712
  if (typeof config !== "object" || config === null) return;
2535
- const configKey = config[keyName];
2536
- if (typeof configKey === "string" && typeof config.value !== "function") return {
2713
+ const { key, value, autocomplete } = Array.isArray(config) ? {
2714
+ key: config[0],
2715
+ value: config[1],
2716
+ autocomplete: config[2]
2717
+ } : {
2718
+ key: config[keyName],
2719
+ value: config.value,
2720
+ autocomplete: config.autocomplete
2721
+ };
2722
+ if (typeof key === "string" && typeof value !== "function") return {
2537
2723
  type: "static",
2538
2724
  rule: {
2539
- key: configKey,
2540
- string: configKey,
2541
- resolved: [config.value].flat(1)
2725
+ key,
2726
+ string: key,
2727
+ resolved: [value].flat(1)
2542
2728
  },
2543
- autocomplete: [configKey]
2729
+ autocomplete: [key]
2730
+ };
2731
+ if (key instanceof RegExp && typeof value === "function") return {
2732
+ type: "dynamic",
2733
+ rule: {
2734
+ key: key.source,
2735
+ stringPattern: stripGlobalFlag(key),
2736
+ createResolved: createDynamicResolvedFactory(value)
2737
+ },
2738
+ autocomplete: autocomplete != null ? [autocomplete].flat(1) : []
2544
2739
  };
2545
- if (configKey instanceof RegExp && typeof config.value === "function") {
2546
- const fn = config.value;
2547
- return {
2548
- type: "dynamic",
2549
- rule: {
2550
- key: configKey.source,
2551
- stringPattern: stripGlobalFlag(configKey),
2552
- createResolved: async (match) => [await fn(match)].flat(1)
2553
- },
2554
- autocomplete: "autocomplete" in config && config.autocomplete != null ? [config.autocomplete].flat(1) : []
2555
- };
2556
- }
2557
2740
  }
2558
2741
  //#endregion
2559
2742
  //#region src/plugins/selectors.ts
@@ -2589,11 +2772,8 @@ function selectors() {
2589
2772
  engine.appendAutocomplete({ selectors: resolved });
2590
2773
  return;
2591
2774
  }
2592
- const addRule = {
2593
- static: () => engine.selectors.resolver.addStaticRule(resolved.rule),
2594
- dynamic: () => engine.selectors.resolver.addDynamicRule(resolved.rule)
2595
- }[resolved.type];
2596
- addRule?.();
2775
+ if (resolved.type === "static") engine.selectors.resolver.addStaticRule(resolved.rule);
2776
+ else engine.selectors.resolver.addDynamicRule(resolved.rule);
2597
2777
  engine.appendAutocomplete({ selectors: resolved.autocomplete });
2598
2778
  });
2599
2779
  }
@@ -2621,7 +2801,7 @@ var SelectorResolver = class extends RecursiveResolver {};
2621
2801
  *
2622
2802
  * @example
2623
2803
  * ```ts
2624
- * const resolved = resolveSelectorConfig(['hover', '&:hover'])
2804
+ * const resolved = resolveSelectorConfig(['hover', '$:hover'])
2625
2805
  * ```
2626
2806
  */
2627
2807
  function resolveSelectorConfig(config) {
@@ -2661,11 +2841,8 @@ function shortcuts() {
2661
2841
  engine.appendAutocomplete({ shortcuts: resolved });
2662
2842
  return;
2663
2843
  }
2664
- const addRule = {
2665
- static: () => engine.shortcuts.resolver.addStaticRule(resolved.rule),
2666
- dynamic: () => engine.shortcuts.resolver.addDynamicRule(resolved.rule)
2667
- }[resolved.type];
2668
- addRule?.();
2844
+ if (resolved.type === "static") engine.shortcuts.resolver.addStaticRule(resolved.rule);
2845
+ else engine.shortcuts.resolver.addDynamicRule(resolved.rule);
2669
2846
  engine.appendAutocomplete({ shortcuts: resolved.autocomplete });
2670
2847
  });
2671
2848
  }
@@ -2695,10 +2872,14 @@ function shortcuts() {
2695
2872
  const result = [];
2696
2873
  for (const styleDefinition of styleDefinitions) if ("__shortcut" in styleDefinition) {
2697
2874
  const { __shortcut, ...rest } = styleDefinition;
2875
+ const explicitImportant = rest.__important ?? null;
2698
2876
  const applied = [];
2699
2877
  for (const shortcut of __shortcut == null ? [] : [__shortcut].flat(1)) {
2700
2878
  const resolved = (await engine.shortcuts.resolver.resolve(shortcut)).filter(isNotString);
2701
- applied.push(...resolved);
2879
+ applied.push(...explicitImportant == null ? resolved : resolved.map((definition) => ({
2880
+ __important: explicitImportant,
2881
+ ...definition
2882
+ })));
2702
2883
  }
2703
2884
  result.push(...applied, rest);
2704
2885
  } else result.push(styleDefinition);
@@ -2758,18 +2939,22 @@ function variables() {
2758
2939
  rawVariables.forEach((variables) => engine.variables.add(variables));
2759
2940
  engine.addPreflight({
2760
2941
  id: "core:variables",
2761
- preflight: async (engine) => {
2942
+ preflight: async (engine, isFormatted, ctx) => {
2762
2943
  const used = /* @__PURE__ */ new Set();
2763
- engine.store.atomicStyles.forEach(({ content: { value } }) => {
2944
+ engine.store.atomicStyles.forEach(({ content: { value } }, id) => {
2945
+ if (ctx?.usedAtomicStyleIds != null && ctx.usedAtomicStyleIds.has(id) === false) return;
2764
2946
  value.flatMap(extractUsedVarNames).forEach((name) => used.add(normalizeVariableName(name)));
2765
2947
  });
2766
2948
  const otherPreflights = engine.config.preflights.filter((p) => p.id !== "core:variables");
2767
- (await Promise.all(otherPreflights.map(({ fn }) => Promise.resolve().then(() => fn(engine, false)).catch(() => null)))).forEach((result) => {
2949
+ (await Promise.all(otherPreflights.map(({ fn }) => engine.invokePreflight(fn, isFormatted, ctx).catch(() => null)))).forEach((result) => {
2768
2950
  if (result == null) return;
2769
2951
  extractUsedVarNamesFromPreflightResult(result).forEach((name) => used.add(name));
2770
2952
  });
2771
- const varMap = /* @__PURE__ */ new Map();
2772
- for (const [name, list] of engine.variables.store.entries()) varMap.set(name, list);
2953
+ const varMap = engine.variables.store;
2954
+ for (const [name, entries] of varMap.entries()) {
2955
+ if (used.has(name)) continue;
2956
+ if (safeSet.has(name) || entries.some((entry) => entry.pruneUnused === false)) used.add(name);
2957
+ }
2773
2958
  const queue = Array.from(used);
2774
2959
  while (queue.length > 0) {
2775
2960
  const name = queue.pop();
@@ -2816,9 +3001,6 @@ function mergeVariablesDefinition(target, source) {
2816
3001
  }
2817
3002
  return target;
2818
3003
  }
2819
- function isPlainObjectRecord(value) {
2820
- return typeof value === "object" && value !== null && !Array.isArray(value);
2821
- }
2822
3004
  function createResolveVariablesFn({ pruneUnused: defaultPruneUnused = true } = {}) {
2823
3005
  function _resolveVariables(variables, levels, result) {
2824
3006
  for (const [key, value] of Object.entries(variables)) if (key.startsWith("--")) {
@@ -2857,7 +3039,7 @@ function resolveAutocompleteValueTargets({ asValueOf }) {
2857
3039
  if (targets.has("*")) return ["*"];
2858
3040
  return [...targets];
2859
3041
  }
2860
- const VAR_NAME_RE = /var\((--[\w-]+)/g;
3042
+ const VAR_NAME_RE = /var\(\s*(--[\w-]+)/g;
2861
3043
  /**
2862
3044
  * Extracts all CSS variable names referenced via `var(--*)` calls in a string.
2863
3045
  *
@@ -2977,11 +3159,11 @@ const DEFAULT_LAYERS = {
2977
3159
  async function createEngine(config = {}) {
2978
3160
  log.debug("Creating engine with config:", config);
2979
3161
  const corePlugins = [
2980
- important(),
2981
3162
  variables(),
2982
3163
  keyframes(),
2983
3164
  selectors(),
2984
- shortcuts()
3165
+ shortcuts(),
3166
+ important()
2985
3167
  ];
2986
3168
  log.debug("Core plugins loaded:", corePlugins.length);
2987
3169
  const plugins = resolvePlugins([...corePlugins, ...config.plugins || []]);
@@ -3027,6 +3209,12 @@ var Engine = class {
3027
3209
  /** The engine's runtime store holding registered atomic styles and their ID mappings. */
3028
3210
  store = createEngineStore();
3029
3211
  /**
3212
+ * Absolute paths of external files this engine's config depends on (e.g. token files loaded by plugins).
3213
+ *
3214
+ * @remarks Plugins register paths via `addConfigDependency` during `configureEngine`. Integration layers (e.g. the unplugin) watch these files and re-create the engine when they change.
3215
+ */
3216
+ configDependencies = /* @__PURE__ */ new Set();
3217
+ /**
3030
3218
  * Creates an engine instance from a resolved configuration.
3031
3219
  *
3032
3220
  * @param config - The fully resolved engine configuration.
@@ -3048,6 +3236,45 @@ var Engine = class {
3048
3236
  });
3049
3237
  }
3050
3238
  /**
3239
+ * Invokes a preflight function, memoizing the result in the given render-pass context.
3240
+ *
3241
+ * @param fn - The preflight function to invoke.
3242
+ * @param isFormatted - Whether the preflight should produce formatted output.
3243
+ * @param ctx - The render-pass context created by `renderPreflights`. When provided, each function executes at most once per pass and the context is forwarded to the preflight function.
3244
+ * @returns A promise of the preflight result.
3245
+ *
3246
+ * @remarks Within one render pass each function executes at most once; concurrent callers (e.g. the variables pruning preflight scanning other preflights for `var()` usage) share the same promise. The memoization is scoped to `ctx`, so overlapping `renderPreflights` calls never interfere with each other. Without a context the function is invoked directly without caching — this is a deliberate change from the previous instance-scoped memoization, which could not tell overlapping passes apart. A preflight that inspects other preflights must therefore forward the `ctx` it receives (its third argument); calling with only `(fn, isFormatted)` from inside a pass runs `fn` again instead of reusing the pass result.
3247
+ *
3248
+ * @example
3249
+ * ```ts
3250
+ * const result = await engine.invokePreflight(preflight.fn, false, ctx)
3251
+ * ```
3252
+ */
3253
+ invokePreflight(fn, isFormatted, ctx) {
3254
+ if (ctx == null) return Promise.resolve().then(() => fn(this, isFormatted));
3255
+ let invocation = ctx.invocations.get(fn);
3256
+ if (invocation == null) {
3257
+ invocation = Promise.resolve().then(() => fn(this, isFormatted, ctx));
3258
+ ctx.invocations.set(fn, invocation);
3259
+ }
3260
+ return invocation;
3261
+ }
3262
+ /**
3263
+ * Registers an external file path as a config dependency of this engine.
3264
+ *
3265
+ * @param path - The file path (ideally absolute) the current config was derived from.
3266
+ *
3267
+ * @remarks Call from a plugin (typically in `configureEngine`) after loading data from disk. Integration layers watch registered paths and rebuild the engine when any of them changes.
3268
+ *
3269
+ * @example
3270
+ * ```ts
3271
+ * engine.addConfigDependency('/project/design.md')
3272
+ * ```
3273
+ */
3274
+ addConfigDependency(path) {
3275
+ this.configDependencies.add(path);
3276
+ }
3277
+ /**
3051
3278
  * Fires the `preflightUpdated` hook to notify plugins that preflight content has changed.
3052
3279
  *
3053
3280
  *
@@ -3145,7 +3372,7 @@ var Engine = class {
3145
3372
  * Processes style items through the plugin pipeline and registers the resulting atomic styles in the store.
3146
3373
  *
3147
3374
  * @param itemList - Style items to process: string references (shortcuts) and/or style definition objects.
3148
- * @returns An array of atomic style IDs (and unresolved string references) in insertion order.
3375
+ * @returns An array containing any unresolved string references first, followed by atomic style IDs in resolution order.
3149
3376
  *
3150
3377
  * @remarks Runs `transformStyleItems` and `extractStyleDefinition` hooks, resolves each extracted content into an atomic style, deduplicates by base key, and fires `atomicStyleAdded` for new entries.
3151
3378
  *
@@ -3184,20 +3411,27 @@ var Engine = class {
3184
3411
  * Renders all registered preflight definitions into a CSS string.
3185
3412
  *
3186
3413
  * @param isFormatted - Whether to produce human-readable CSS with newlines and indentation.
3414
+ * @param options - Optional render-pass options.
3415
+ * @param options.usedAtomicStyleIds - Atomic style IDs considered "in use" for this pass. When provided, pruning preflights (variables, keyframes) only consider these atomic styles instead of the whole append-only store; when omitted, all stored atomic styles are considered.
3187
3416
  * @returns The rendered preflight CSS, including `@import` statements, optional `@layer` wrappers, and all preflight content.
3188
3417
  *
3189
- * @remarks Evaluates each preflight function, groups output by layer, wraps unlayered preflights in the default preflights layer (when present), and respects configured layer ordering.
3418
+ * @remarks Evaluates each preflight function, groups output by layer, wraps unlayered preflights in the default preflights layer (when present), and respects configured layer ordering. Each call creates its own `PreflightContext`, so within one pass each preflight function executes exactly once even when passes overlap.
3190
3419
  *
3191
3420
  * @example
3192
3421
  * ```ts
3193
3422
  * const css = await engine.renderPreflights(true)
3423
+ * const scoped = await engine.renderPreflights(true, { usedAtomicStyleIds: ['pk-a'] })
3194
3424
  * ```
3195
3425
  */
3196
- async renderPreflights(isFormatted) {
3426
+ async renderPreflights(isFormatted, options = {}) {
3197
3427
  log.debug("Rendering preflights...");
3198
3428
  const lineEnd = isFormatted ? "\n" : "";
3429
+ const ctx = {
3430
+ invocations: /* @__PURE__ */ new Map(),
3431
+ usedAtomicStyleIds: options.usedAtomicStyleIds == null ? void 0 : new Set(options.usedAtomicStyleIds)
3432
+ };
3199
3433
  const rendered = (await Promise.all(this.config.preflights.map(async ({ layer, fn }) => {
3200
- const result = await fn(this, isFormatted);
3434
+ const result = await this.invokePreflight(fn, isFormatted, ctx);
3201
3435
  return {
3202
3436
  layer,
3203
3437
  css: (typeof result === "string" ? result : await renderPreflightDefinition({
@@ -3394,15 +3628,10 @@ function groupAtomicStylesByLayer({ styles, layerOrder, defaultUtilitiesLayer })
3394
3628
  layerGroups
3395
3629
  };
3396
3630
  }
3397
- function isWithLayer(p) {
3398
- if (typeof p !== "object" || p === null) return false;
3399
- const record = p;
3400
- return typeof record.layer === "string" && record.preflight !== void 0;
3401
- }
3402
- function isWithId(p) {
3631
+ function hasPreflightWrapper(p, key) {
3403
3632
  if (typeof p !== "object" || p === null) return false;
3404
3633
  const record = p;
3405
- return typeof record.id === "string" && record.preflight !== void 0;
3634
+ return typeof record[key] === "string" && record.preflight !== void 0;
3406
3635
  }
3407
3636
  /**
3408
3637
  * Normalizes a `Preflight` input into a `ResolvedPreflight` by extracting optional `layer` and `id` wrappers.
@@ -3421,11 +3650,11 @@ function isWithId(p) {
3421
3650
  function resolvePreflight(preflight) {
3422
3651
  let layer;
3423
3652
  let id;
3424
- if (isWithLayer(preflight)) {
3653
+ if (hasPreflightWrapper(preflight, "layer")) {
3425
3654
  layer = preflight.layer;
3426
3655
  preflight = preflight.preflight;
3427
3656
  }
3428
- if (isWithId(preflight)) {
3657
+ if (hasPreflightWrapper(preflight, "id")) {
3429
3658
  id = preflight.id;
3430
3659
  preflight = preflight.preflight;
3431
3660
  }
@@ -3543,9 +3772,9 @@ function renderAtomicStylesCss({ atomicStyles, isPreview, isFormatted }) {
3543
3772
  const blocks = /* @__PURE__ */ new Map();
3544
3773
  atomicStyles.forEach(({ id, content: { selector: rawSelector, property, value } }) => {
3545
3774
  const { selector } = splitLayerSelector(rawSelector);
3546
- if (selector.some((s) => s.includes("%")) === false || value == null) return;
3775
+ if (selector.some((s) => hasAtomicStyleIdPlaceholder(s)) === false || value == null) return;
3547
3776
  const renderObject = {
3548
- selector: isPreview ? selector : selector.map((s) => s.replace(ATOMIC_STYLE_ID_PLACEHOLDER_RE_GLOBAL, id)),
3777
+ selector: isPreview ? selector : selector.map((s) => replaceAtomicStyleIdPlaceholder(s, id)),
3549
3778
  properties: value.map((v) => ({
3550
3779
  property,
3551
3780
  value: v
@@ -3720,4 +3949,4 @@ function defineEngineConfig(config) {
3720
3949
  }
3721
3950
  /* c8 ignore end */
3722
3951
  //#endregion
3723
- export { appendAutocomplete, createEngine, createLogger, defineEngineConfig, defineEnginePlugin, log, renderCSSStyleBlocks, sortLayerNames };
3952
+ export { appendAutocomplete, createEngine, createLogger, defineEngineConfig, defineEnginePlugin, escapeRegExp, isPlainObjectRecord, log, renderCSSStyleBlocks, sortLayerNames };