domflax 0.1.4 → 0.2.0

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 (42) hide show
  1. package/README.md +47 -29
  2. package/dist/{chunk-EVENAJYI.js → chunk-EYQXQQQH.js} +3 -3
  3. package/dist/{chunk-3Z5ZWLXX.js → chunk-FPT4EJ6Q.js} +805 -1612
  4. package/dist/chunk-FPT4EJ6Q.js.map +1 -0
  5. package/dist/{chunk-5FWENSD2.js → chunk-JBM3MJRM.js} +149 -10
  6. package/dist/chunk-JBM3MJRM.js.map +1 -0
  7. package/dist/{chunk-H5KTGI3A.js → chunk-TTJEXWAC.js} +172 -5
  8. package/dist/chunk-TTJEXWAC.js.map +1 -0
  9. package/dist/cli.cjs +1032 -1640
  10. package/dist/cli.cjs.map +1 -1
  11. package/dist/cli.js +30 -10
  12. package/dist/cli.js.map +1 -1
  13. package/dist/index.cjs +1116 -1627
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +226 -485
  16. package/dist/index.d.ts +226 -485
  17. package/dist/index.js +16 -36
  18. package/dist/{pattern-CP9_HpVK.d.cts → pattern-DotR_dHs.d.cts} +1 -1
  19. package/dist/pattern-kit.cjs +60 -1
  20. package/dist/pattern-kit.cjs.map +1 -1
  21. package/dist/pattern-kit.d.cts +2 -2
  22. package/dist/pattern-kit.d.ts +2 -2
  23. package/dist/pattern-kit.js +1 -1
  24. package/dist/{pattern-CYgsv-jO.d.ts → pattern-urm5uuwj.d.ts} +1 -1
  25. package/dist/{resolve-ops-Ci7LgYHC.d.ts → resolve-ops-D8aQina5.d.cts} +11 -0
  26. package/dist/{resolve-ops-Ci7LgYHC.d.cts → resolve-ops-D8aQina5.d.ts} +11 -0
  27. package/dist/verify.d.cts +1 -1
  28. package/dist/verify.d.ts +1 -1
  29. package/dist/webpack-loader.cjs +1014 -1578
  30. package/dist/webpack-loader.cjs.map +1 -1
  31. package/dist/webpack-loader.d.cts +8 -2
  32. package/dist/webpack-loader.d.ts +8 -2
  33. package/dist/webpack-loader.js +7 -4
  34. package/dist/webpack-loader.js.map +1 -1
  35. package/dist/worker.cjs +983 -1601
  36. package/dist/worker.cjs.map +1 -1
  37. package/dist/worker.js +3 -3
  38. package/package.json +1 -1
  39. package/dist/chunk-3Z5ZWLXX.js.map +0 -1
  40. package/dist/chunk-5FWENSD2.js.map +0 -1
  41. package/dist/chunk-H5KTGI3A.js.map +0 -1
  42. /package/dist/{chunk-EVENAJYI.js.map → chunk-EYQXQQQH.js.map} +0 -0
@@ -96,6 +96,7 @@ function defaultMeta(safetyFloor = 0) {
96
96
  hasDynamicChildren: false,
97
97
  isComponent: false,
98
98
  hasDangerousHtml: false,
99
+ hasUnresolvedClasses: false,
99
100
  targetedByCombinator: false,
100
101
  targetedByStructuralPseudo: false,
101
102
  selectorDependents: 0,
@@ -1133,6 +1134,9 @@ function classifyFlattenOps(before, after, ops, norm) {
1133
1134
  if (!wrapper || wrapper.kind !== "element") {
1134
1135
  return { kind: "provably-safe", wrapperId: null, childId: null };
1135
1136
  }
1137
+ if (wrapper.meta.hasUnresolvedClasses) {
1138
+ return { kind: "needs-verification", wrapperId, childId: survivingChildOf(ops, wrapper, before) };
1139
+ }
1136
1140
  const childId = survivingChildOf(ops, wrapper, before);
1137
1141
  const wrapperComputed = norm.normalizeStyleMap(wrapper.computed);
1138
1142
  const childAfter = childId != null ? getElement(after, childId)?.computed ?? null : null;
@@ -1583,15 +1587,26 @@ function residualStyle(computed2, covered, norm) {
1583
1587
  }
1584
1588
  return { blocks };
1585
1589
  }
1590
+ function joinedLength(tokens) {
1591
+ if (tokens.length === 0) return 0;
1592
+ let len = tokens.length - 1;
1593
+ for (const t of tokens) len += t.length;
1594
+ return len;
1595
+ }
1596
+ var COMPRESS_FLOOR = 1;
1586
1597
  function syncClassesFromComputed(doc, resolver, norm) {
1587
1598
  const sink = createSyntheticSink();
1599
+ const isDroppable = (t) => resolver.owns(t) && resolver.selectorUsage(t).droppable;
1588
1600
  for (const id of elementIds(doc)) {
1589
1601
  const el = getElement(doc, id);
1590
1602
  if (!el) continue;
1591
- if (!el.meta.styleDirty) continue;
1592
1603
  if (el.classes.opaque || el.classes.hasDynamic) continue;
1604
+ const compressOnly = !el.meta.styleDirty;
1605
+ if (compressOnly && el.meta.safetyFloor < COMPRESS_FLOOR) continue;
1593
1606
  const tokens = staticTokensOf(el.classes);
1594
- const retained = tokens.filter((t) => !resolver.selectorUsage(t).droppable);
1607
+ if (tokens.length === 0) continue;
1608
+ const retained = tokens.filter((t) => !isDroppable(t));
1609
+ if (compressOnly && retained.length === tokens.length) continue;
1595
1610
  const covered = retained.length > 0 ? resolver.resolve({ classes: retained }).styles : null;
1596
1611
  const target = covered ? residualStyle(el.computed, covered, norm) : el.computed;
1597
1612
  const ctx = { normalizer: norm, sink };
@@ -1602,7 +1617,7 @@ function syncClassesFromComputed(doc, resolver, norm) {
1602
1617
  const seen = /* @__PURE__ */ new Set();
1603
1618
  for (const t of tokens) {
1604
1619
  if (seen.has(t)) continue;
1605
- const keep = emittedSet.has(t) || !resolver.selectorUsage(t).droppable;
1620
+ const keep = emittedSet.has(t) || !isDroppable(t);
1606
1621
  if (keep) {
1607
1622
  next.push(t);
1608
1623
  seen.add(t);
@@ -1614,10 +1629,98 @@ function syncClassesFromComputed(doc, resolver, norm) {
1614
1629
  seen.add(c);
1615
1630
  }
1616
1631
  if (sameTokens(next, tokens)) continue;
1632
+ if (compressOnly) {
1633
+ if (!norm.equals(resolver.resolve({ classes: next }).styles, el.computed)) continue;
1634
+ if (joinedLength(next) > joinedLength(tokens)) continue;
1635
+ }
1617
1636
  el.classes = staticClassList(el.classes, next);
1618
1637
  }
1619
1638
  }
1620
1639
 
1640
+ // ../core/src/compress-engine.ts
1641
+ var SEP = "";
1642
+ function tupleKey(condition, property, value, important) {
1643
+ return `${condition}${SEP}${property}${SEP}${value}${SEP}${important ? "1" : "0"}`;
1644
+ }
1645
+ function styleMapTuples(map, norm) {
1646
+ const out = [];
1647
+ const normalized = norm.normalizeStyleMap(map);
1648
+ for (const [ck, block] of normalized.blocks) {
1649
+ for (const [prop, decl] of block.decls) {
1650
+ out.push(tupleKey(String(ck), String(prop), String(decl.value), decl.important));
1651
+ }
1652
+ }
1653
+ return out;
1654
+ }
1655
+ var DEFAULT_MAX_UNIVERSE = 20;
1656
+ function minStringCover(universe, vocabulary, options = {}) {
1657
+ const uniq = [...new Set(universe)];
1658
+ if (uniq.length === 0) return [];
1659
+ const n = uniq.length;
1660
+ const max = options.maxUniverse ?? DEFAULT_MAX_UNIVERSE;
1661
+ if (n > max) return null;
1662
+ const bitOf = /* @__PURE__ */ new Map();
1663
+ uniq.forEach((t, i) => bitOf.set(t, i));
1664
+ const byMask = /* @__PURE__ */ new Map();
1665
+ for (const entry of vocabulary) {
1666
+ if (entry.tuples.length === 0) continue;
1667
+ let mask = 0;
1668
+ let ok = true;
1669
+ for (const t of entry.tuples) {
1670
+ const b = bitOf.get(t);
1671
+ if (b === void 0) {
1672
+ ok = false;
1673
+ break;
1674
+ }
1675
+ mask |= 1 << b;
1676
+ }
1677
+ if (!ok || mask === 0) continue;
1678
+ const cost = entry.token.length + 1;
1679
+ const prev = byMask.get(mask);
1680
+ if (!prev || cost < prev.cost || cost === prev.cost && entry.token < prev.token) {
1681
+ byMask.set(mask, { token: entry.token, mask, cost });
1682
+ }
1683
+ }
1684
+ const cands = [...byMask.values()];
1685
+ if (cands.length === 0) return null;
1686
+ const full = (1 << n) - 1;
1687
+ const byBit = Array.from({ length: n }, () => []);
1688
+ cands.forEach((c, ci) => {
1689
+ for (let b = 0; b < n; b += 1) if (c.mask & 1 << b) byBit[b].push(ci);
1690
+ });
1691
+ const size = full + 1;
1692
+ const dp = new Float64Array(size).fill(Infinity);
1693
+ const fromCand = new Int32Array(size).fill(-1);
1694
+ const fromMask = new Int32Array(size).fill(-1);
1695
+ dp[0] = 0;
1696
+ for (let mask = 0; mask < full; mask += 1) {
1697
+ const cur = dp[mask];
1698
+ if (!Number.isFinite(cur)) continue;
1699
+ let b = 0;
1700
+ while (b < n && mask & 1 << b) b += 1;
1701
+ for (const ci of byBit[b]) {
1702
+ const c = cands[ci];
1703
+ const nm = mask | c.mask;
1704
+ const cost = cur + c.cost;
1705
+ if (cost < dp[nm]) {
1706
+ dp[nm] = cost;
1707
+ fromCand[nm] = ci;
1708
+ fromMask[nm] = mask;
1709
+ }
1710
+ }
1711
+ }
1712
+ if (!Number.isFinite(dp[full])) return null;
1713
+ const chosen = [];
1714
+ let m = full;
1715
+ while (m !== 0) {
1716
+ const ci = fromCand[m];
1717
+ if (ci < 0) return null;
1718
+ chosen.push(cands[ci].token);
1719
+ m = fromMask[m];
1720
+ }
1721
+ return [...new Set(chosen)].sort();
1722
+ }
1723
+
1621
1724
  // ../pattern-kit/src/normalize.ts
1622
1725
  var INHERITED_PROPERTIES = [
1623
1726
  "azimuth",
@@ -1702,6 +1805,18 @@ var BOX_SIDES = {
1702
1805
  padding: ["padding-top", "padding-right", "padding-bottom", "padding-left"],
1703
1806
  margin: ["margin-top", "margin-right", "margin-bottom", "margin-left"],
1704
1807
  inset: ["top", "right", "bottom", "left"],
1808
+ "scroll-margin": [
1809
+ "scroll-margin-top",
1810
+ "scroll-margin-right",
1811
+ "scroll-margin-bottom",
1812
+ "scroll-margin-left"
1813
+ ],
1814
+ "scroll-padding": [
1815
+ "scroll-padding-top",
1816
+ "scroll-padding-right",
1817
+ "scroll-padding-bottom",
1818
+ "scroll-padding-left"
1819
+ ],
1705
1820
  "border-width": [
1706
1821
  "border-top-width",
1707
1822
  "border-right-width",
@@ -1719,8 +1834,35 @@ var BOX_SIDES = {
1719
1834
  "border-right-color",
1720
1835
  "border-bottom-color",
1721
1836
  "border-left-color"
1837
+ ],
1838
+ // `border-radius` 1–4 value form maps to the four CORNERS (TL, TR, BR, BL) — the same positional
1839
+ // pattern boxFourSides implements. Only the slash-free form is expanded (see expandShorthand).
1840
+ "border-radius": [
1841
+ "border-top-left-radius",
1842
+ "border-top-right-radius",
1843
+ "border-bottom-right-radius",
1844
+ "border-bottom-left-radius"
1722
1845
  ]
1723
1846
  };
1847
+ var AXIS_PAIRS = {
1848
+ overflow: ["overflow-x", "overflow-y"],
1849
+ "overscroll-behavior": ["overscroll-behavior-x", "overscroll-behavior-y"],
1850
+ "place-items": ["align-items", "justify-items"],
1851
+ "place-content": ["align-content", "justify-content"],
1852
+ "place-self": ["align-self", "justify-self"]
1853
+ };
1854
+ var LOGICAL_PAIRS = {
1855
+ "padding-inline": ["padding-left", "padding-right"],
1856
+ "padding-block": ["padding-top", "padding-bottom"],
1857
+ "margin-inline": ["margin-left", "margin-right"],
1858
+ "margin-block": ["margin-top", "margin-bottom"],
1859
+ "inset-inline": ["left", "right"],
1860
+ "inset-block": ["top", "bottom"],
1861
+ "scroll-padding-inline": ["scroll-padding-left", "scroll-padding-right"],
1862
+ "scroll-padding-block": ["scroll-padding-top", "scroll-padding-bottom"],
1863
+ "scroll-margin-inline": ["scroll-margin-left", "scroll-margin-right"],
1864
+ "scroll-margin-block": ["scroll-margin-top", "scroll-margin-bottom"]
1865
+ };
1724
1866
  function splitTopLevel(value) {
1725
1867
  const out = [];
1726
1868
  let depth = 0;
@@ -1754,6 +1896,7 @@ function boxFourSides(values) {
1754
1896
  }
1755
1897
  }
1756
1898
  function expandShorthand(prop, value) {
1899
+ if (prop === "border-radius" && value.includes("/")) return [[prop, value]];
1757
1900
  const box = BOX_SIDES[prop];
1758
1901
  if (box) {
1759
1902
  const parts = splitTopLevel(value);
@@ -1763,6 +1906,19 @@ function expandShorthand(prop, value) {
1763
1906
  }
1764
1907
  return [[prop, value]];
1765
1908
  }
1909
+ const axis = AXIS_PAIRS[prop];
1910
+ if (axis) {
1911
+ const parts = splitTopLevel(value);
1912
+ if (parts.length === 1) return [[axis[0], parts[0]], [axis[1], parts[0]]];
1913
+ if (parts.length === 2) return [[axis[0], parts[0]], [axis[1], parts[1]]];
1914
+ return [[prop, value]];
1915
+ }
1916
+ const logical = LOGICAL_PAIRS[prop];
1917
+ if (logical) {
1918
+ const parts = splitTopLevel(value);
1919
+ if (parts.length === 1) return [[logical[0], parts[0]], [logical[1], parts[0]]];
1920
+ return [[prop, value]];
1921
+ }
1766
1922
  if (prop === "gap" || prop === "grid-gap") {
1767
1923
  const parts = splitTopLevel(value);
1768
1924
  if (parts.length === 1) {
@@ -1920,7 +2076,12 @@ var VISUAL_PROPERTIES = /* @__PURE__ */ new Set([
1920
2076
  "border-right-color",
1921
2077
  "border-bottom-color",
1922
2078
  "border-left-color",
1923
- "border-radius",
2079
+ // `border-radius` is expanded to its four corner longhands by the shared normalizer, so the
2080
+ // paint-establishing check must match those (a rounded wrapper still clips its background).
2081
+ "border-top-left-radius",
2082
+ "border-top-right-radius",
2083
+ "border-bottom-right-radius",
2084
+ "border-bottom-left-radius",
1924
2085
  "box-shadow",
1925
2086
  "outline",
1926
2087
  "outline-width",
@@ -1946,6 +2107,7 @@ var hasOwnVisualStyle = (node, ctx) => {
1946
2107
  const el = asElement(node);
1947
2108
  if (!el) return false;
1948
2109
  if (el.meta.hasOwnVisualStyle) return true;
2110
+ if (el.meta.hasUnresolvedClasses) return true;
1949
2111
  const computedMap = ctx.computedOf(el) ?? el.computed;
1950
2112
  const norm = normalizer.normalizeStyleMap(computedMap);
1951
2113
  for (const block of norm.blocks.values()) {
@@ -2150,7 +2312,160 @@ function definePattern(config) {
2150
2312
  return validatePattern(spec);
2151
2313
  }
2152
2314
 
2153
- // ../patterns/src/library/flatten/display-contents-wrapper.pattern.ts
2315
+ // ../patterns/src/library/flex/flex-center-wrapper.pattern.ts
2316
+ var flexCenterWrapper = definePattern({
2317
+ name: "flex-center-wrapper",
2318
+ category: "flatten/flex/flex-center-wrapper",
2319
+ safety: 2,
2320
+ doc: {
2321
+ title: "Flatten flex-centering wrapper",
2322
+ summary: "A div that only centers a single child (display:flex; align-items:center; justify-content:center) is removed; the child gains place-self:center.",
2323
+ before: '<div style="display:flex;align-items:center;justify-content:center"><Child/></div>',
2324
+ after: '<Child style="place-self:center"/>',
2325
+ safetyRationale: "Wrapper paints nothing, carries no ref/handlers/dynamic children, and is not a combinator subject; inheritable styles are folded onto the child before removal."
2326
+ },
2327
+ match: {
2328
+ tag: "div",
2329
+ style: { display: "flex", alignItems: "center", justifyContent: "center" },
2330
+ onlyChild: "element",
2331
+ paintsNothing: true
2332
+ },
2333
+ rewrite: {
2334
+ flattenInto: "child",
2335
+ childGains: { placeSelf: "center" }
2336
+ },
2337
+ // Collapsing a flex-centering wrapper to `place-self:center` on the child is render-identical ONLY
2338
+ // when the child's NEW parent is a statically-known GRID that lets the wrapper fill its area (there
2339
+ // `place-self`'s align-self AND justify-self both take effect). Under that ONE context the flatten is
2340
+ // classified `provably-safe` and commits; under a flex/block/unknown parent — or when the wrapper
2341
+ // drops any own style — it stays `needs-verification` and the conservative production gate PRESERVES
2342
+ // it. Op-level correctness (purity, id-preserving unwrap, opacity-barrier safety) is additionally
2343
+ // asserted by the invariant suite over every pattern.
2344
+ test: {
2345
+ cases: [
2346
+ {
2347
+ name: "grid parent \u2192 flattened (child gains place-self-center)",
2348
+ before: '<div className="grid"><div className="flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2349
+ after: '<div className="grid"><span className="bg-red-200 place-self-center">x</span></div>'
2350
+ }
2351
+ ],
2352
+ noMatch: [
2353
+ // Non-grid (flex) parent (document root): `justify-self` is ignored in flex → not provably safe.
2354
+ '<div className="flex justify-center items-center"><div className="bg-red-200">Hello</div></div>',
2355
+ // Grid parent, but the wrapper drops padding when removed → not layout-neutral (rule 3).
2356
+ '<div className="grid"><div className="p-4 flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2357
+ // Grid parent forcing place-items-center: the wrapper would not fill its area → fill guard skips.
2358
+ '<div className="grid place-items-center"><div className="flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2359
+ // onClick is a hard opacity barrier → the wrapper is load-bearing regardless of the gate.
2360
+ '<div className="flex justify-center items-center" onClick={handleClick}><div className="bg-red-200">Hello</div></div>'
2361
+ ]
2362
+ }
2363
+ });
2364
+
2365
+ // ../patterns/src/library/fragment/redundant-fragment.pattern.ts
2366
+ function parentIsRedundantFragment(node, ctx) {
2367
+ const el = node;
2368
+ if (el.kind !== "element") return false;
2369
+ const parentId = el.parent;
2370
+ if (parentId == null) return false;
2371
+ const parent = ctx.doc.nodes.get(parentId);
2372
+ if (!parent || parent.kind !== "fragment") return false;
2373
+ if (parent.parent == null) return false;
2374
+ if (parent.children.length !== 1) return false;
2375
+ const m = parent.meta;
2376
+ if (m.hasKey || m.hasRef || m.hasEventHandlers || m.hasDynamicChildren || m.hasDangerousHtml || m.hasSpreadAttrs || m.isComponent) {
2377
+ return false;
2378
+ }
2379
+ if (m.targetedByCombinator || m.targetedByStructuralPseudo) return false;
2380
+ const fid = parentId;
2381
+ if (ctx.selectors.targetedByCombinator(fid) || ctx.selectors.targetedByStructuralPseudo(fid)) {
2382
+ return false;
2383
+ }
2384
+ if (ctx.selectors.reparentImpact(fid).size > 0) return false;
2385
+ return true;
2386
+ }
2387
+ var redundantFragment = definePattern({
2388
+ name: "redundant-fragment",
2389
+ category: "flatten/fragment/redundant-fragment",
2390
+ safety: 1,
2391
+ doc: {
2392
+ title: "Flatten redundant single-child fragment",
2393
+ summary: "A fragment whose only child is a single node is removed; the child is spliced up into the fragment's slot, preserving its IRNodeId, siblings, attributes and the CSS cascade.",
2394
+ before: "<><Child/></>",
2395
+ after: "<Child/>",
2396
+ safetyRationale: "A fragment paints nothing and renders no box; with exactly one child its removal changes no sibling/structural-pseudo match-set. Keyed fragments and fragments carrying ref/handlers/dynamic-children/raw-html/spread are excluded as opacity barriers."
2397
+ },
2398
+ match: parentIsRedundantFragment,
2399
+ rewrite: (ctx, rw) => {
2400
+ const parentId = ctx.node.parent;
2401
+ if (parentId == null) return null;
2402
+ const fragment = ctx.doc.nodes.get(parentId);
2403
+ if (!fragment || fragment.kind !== "fragment") return null;
2404
+ return [rw.unwrap(fragment)];
2405
+ },
2406
+ test: {
2407
+ cases: [
2408
+ {
2409
+ // A fragment renders no box, so unwrapping a single-child fragment is always layout-identical
2410
+ // → a provably-safe flatten: the child is spliced up into the fragment's slot.
2411
+ before: '<><span className="bg-red-200">Hi</span></>',
2412
+ after: '<span className="bg-red-200">Hi</span>'
2413
+ }
2414
+ ],
2415
+ noMatch: [
2416
+ // Two children ⇒ not a single-child fragment, so the fragment is load-bearing and stays.
2417
+ '<><span className="bg-red-200">A</span><span className="bg-green-200">B</span></>'
2418
+ ]
2419
+ }
2420
+ });
2421
+
2422
+ // ../patterns/src/library/grid/grid-center-wrapper.pattern.ts
2423
+ var gridCenterWrapper = definePattern({
2424
+ name: "grid-center-wrapper",
2425
+ category: "flatten/grid/grid-center-wrapper",
2426
+ safety: 2,
2427
+ doc: {
2428
+ title: "Flatten grid-centering wrapper",
2429
+ summary: "A div that only centers a single child (display:grid; align-items:center; justify-content:center) is removed; the child gains place-self:center.",
2430
+ before: '<div style="display:grid;align-items:center;justify-content:center"><Child/></div>',
2431
+ after: '<Child style="place-self:center"/>',
2432
+ safetyRationale: "Wrapper paints nothing, carries no ref/handlers/dynamic children, and is not a combinator subject; inheritable styles are folded onto the child before removal. The place-self:center collapse is committed by the gate only under a statically-known filling grid parent."
2433
+ },
2434
+ match: {
2435
+ tag: "div",
2436
+ style: { display: "grid", alignItems: "center", justifyContent: "center" },
2437
+ onlyChild: "element",
2438
+ paintsNothing: true
2439
+ },
2440
+ rewrite: {
2441
+ flattenInto: "child",
2442
+ childGains: { placeSelf: "center" }
2443
+ },
2444
+ // Like `flex-center-wrapper`, collapsing to `place-self:center` is render-identical ONLY when the
2445
+ // child's NEW parent is a statically-known GRID that lets the wrapper fill its area (there both halves
2446
+ // of place-self take effect). Under that ONE context the flatten is `provably-safe` and commits; under
2447
+ // a flex/block/unknown parent — or when the wrapper drops any own style — it stays `needs-verification`
2448
+ // and the conservative production gate PRESERVES it. Op-level correctness is asserted by the invariant suite.
2449
+ test: {
2450
+ cases: [
2451
+ {
2452
+ name: "grid parent \u2192 flattened (child gains place-self-center)",
2453
+ before: '<div className="grid"><div className="grid items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2454
+ after: '<div className="grid"><span className="bg-red-200 place-self-center">x</span></div>'
2455
+ }
2456
+ ],
2457
+ noMatch: [
2458
+ // Non-grid (document-root) parent: justify-self is ignored outside a grid → not provably safe.
2459
+ '<div className="grid justify-center items-center"><div className="bg-red-200">Hello</div></div>',
2460
+ // Grid parent, but the wrapper drops padding when removed → not layout-neutral, preserved.
2461
+ '<div className="grid"><div className="p-4 grid items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2462
+ // onClick is a hard opacity barrier → the wrapper is load-bearing regardless of the gate.
2463
+ '<div className="grid justify-center items-center" onClick={handleClick}><div className="bg-red-200">Hello</div></div>'
2464
+ ]
2465
+ }
2466
+ });
2467
+
2468
+ // ../patterns/src/library/wrapper/display-contents-wrapper.pattern.ts
2154
2469
  function asEl(node) {
2155
2470
  const n = node;
2156
2471
  return n.kind === "element" ? n : null;
@@ -2174,7 +2489,7 @@ var targetedByStructuralPseudo = (node, ctx) => {
2174
2489
  };
2175
2490
  var displayContentsWrapper = definePattern({
2176
2491
  name: "display-contents-wrapper",
2177
- category: "flatten/display-contents-wrapper",
2492
+ category: "flatten/wrapper/display-contents-wrapper",
2178
2493
  safety: 2,
2179
2494
  doc: {
2180
2495
  title: "Flatten display:contents wrapper",
@@ -2214,7 +2529,7 @@ var displayContentsWrapper = definePattern({
2214
2529
  }
2215
2530
  });
2216
2531
 
2217
- // ../patterns/src/library/flatten/empty-style-div.pattern.ts
2532
+ // ../patterns/src/library/wrapper/empty-style-div.pattern.ts
2218
2533
  function asEl2(node) {
2219
2534
  const n = node;
2220
2535
  return n.kind === "element" ? n : null;
@@ -2246,7 +2561,7 @@ var hasNonBlockDisplay = (node, ctx) => {
2246
2561
  };
2247
2562
  var emptyStyleDiv = definePattern({
2248
2563
  name: "empty-style-div",
2249
- category: "flatten/empty-style-div",
2564
+ category: "flatten/wrapper/empty-style-div",
2250
2565
  safety: 1,
2251
2566
  doc: {
2252
2567
  title: "Flatten empty-style div wrapper",
@@ -2285,342 +2600,73 @@ var emptyStyleDiv = definePattern({
2285
2600
  }
2286
2601
  });
2287
2602
 
2288
- // ../patterns/src/library/flatten/flex-center-wrapper.pattern.ts
2289
- var flexCenterWrapper = definePattern({
2290
- name: "flex-center-wrapper",
2291
- category: "flatten/flex-center-wrapper",
2603
+ // ../patterns/src/library/wrapper/inherited-only-wrapper.pattern.ts
2604
+ var INERT_HOST_TAGS = /* @__PURE__ */ new Set(["div", "span"]);
2605
+ var isInertHostTag = (node) => {
2606
+ const n = node;
2607
+ if (n.kind !== "element") return false;
2608
+ return INERT_HOST_TAGS.has(String(n.tag).toLowerCase());
2609
+ };
2610
+ var isComponentNode2 = (node) => {
2611
+ const n = node;
2612
+ return n.kind === "element" ? n.meta.isComponent : false;
2613
+ };
2614
+ var hasOnlyInheritedStyle = (node, ctx) => {
2615
+ const sm = normalizer.normalizeStyleMap(ctx.computed());
2616
+ let sawAny = false;
2617
+ for (const block of sm.blocks.values()) {
2618
+ for (const decl of block.decls.values()) {
2619
+ sawAny = true;
2620
+ const inherited = decl.inherited || normalizer.inherited.isInherited(decl.property);
2621
+ if (!inherited) return false;
2622
+ }
2623
+ }
2624
+ return sawAny;
2625
+ };
2626
+ var inheritedOnlyWrapper = definePattern({
2627
+ name: "inherited-only-wrapper",
2628
+ category: "flatten/wrapper/inherited-only-wrapper",
2292
2629
  safety: 2,
2293
2630
  doc: {
2294
- title: "Flatten flex-centering wrapper",
2295
- summary: "A div that only centers a single child (display:flex; align-items:center; justify-content:center) is removed; the child gains place-self:center.",
2296
- before: '<div style="display:flex;align-items:center;justify-content:center"><Child/></div>',
2297
- after: '<Child style="place-self:center"/>',
2298
- safetyRationale: "Wrapper paints nothing, carries no ref/handlers/dynamic children, and is not a combinator subject; inheritable styles are folded onto the child before removal."
2631
+ title: "Flatten inherited-only styling wrapper",
2632
+ summary: "A paint-free wrapper whose only own declarations are inherited properties (text-align, color, font-*, \u2026) is removed; its inherited style is folded onto the sole child, which keeps the same inherited values for the whole subtree.",
2633
+ before: '<div style="text-align:center"><Child/></div>',
2634
+ after: '<Child style="text-align:center"/>',
2635
+ safetyRationale: "Inherited properties reach descendants purely through inheritance, so folding them onto the child and removing the box is render-identical. The wrapper carries nothing non-inherited, establishes no box/formatting/stacking context, and is guarded by the auto-applied opacity-barrier + selector-safety set."
2299
2636
  },
2300
2637
  match: {
2301
- tag: "div",
2302
- style: { display: "flex", alignItems: "center", justifyContent: "center" },
2303
2638
  onlyChild: "element",
2304
- paintsNothing: true
2305
- },
2306
- rewrite: {
2307
- flattenInto: "child",
2308
- childGains: { placeSelf: "center" }
2639
+ paintsNothing: true,
2640
+ where: [isInertHostTag, not(isComponentNode2), hasOnlyInheritedStyle]
2309
2641
  },
2310
- // Collapsing a flex-centering wrapper to `place-self:center` on the child is render-identical ONLY
2311
- // when the child's NEW parent is a statically-known GRID that lets the wrapper fill its area (there
2312
- // `place-self`'s align-self AND justify-self both take effect). Under that ONE context the flatten is
2313
- // classified `provably-safe` and commits; under a flex/block/unknown parent — or when the wrapper
2314
- // drops any own style — it stays `needs-verification` and the conservative production gate PRESERVES
2315
- // it. Op-level correctness (purity, id-preserving unwrap, opacity-barrier safety) is additionally
2316
- // asserted by the invariant suite over every pattern.
2642
+ rewrite: { flattenInto: "child" },
2317
2643
  test: {
2318
2644
  cases: [
2319
2645
  {
2320
- name: "grid parent \u2192 flattened (child gains place-self-center)",
2321
- before: '<div className="grid"><div className="flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2322
- after: '<div className="grid"><span className="bg-red-200 place-self-center">x</span></div>'
2646
+ // `text-align:center` is inherited folded onto the child; the paint-free wrapper is removed.
2647
+ before: '<div className="text-center"><p className="bg-red-200">x</p></div>',
2648
+ after: '<p className="bg-red-200 text-center">x</p>'
2323
2649
  }
2324
2650
  ],
2325
2651
  noMatch: [
2326
- // Non-grid (flex) parent (document root): `justify-self` is ignored in flex not provably safe.
2327
- '<div className="flex justify-center items-center"><div className="bg-red-200">Hello</div></div>',
2328
- // Grid parent, but the wrapper drops padding when removed → not layout-neutral (rule 3).
2329
- '<div className="grid"><div className="p-4 flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2330
- // Grid parent forcing place-items-center: the wrapper would not fill its area fill guard skips.
2331
- '<div className="grid place-items-center"><div className="flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2332
- // onClick is a hard opacity barrier → the wrapper is load-bearing regardless of the gate.
2333
- '<div className="flex justify-center items-center" onClick={handleClick}><div className="bg-red-200">Hello</div></div>'
2334
- ]
2335
- }
2336
- });
2337
-
2338
- // ../patterns/src/library/flatten/inline-flex-center-wrapper.pattern.ts
2339
- var inlineFlexCenterWrapper = definePattern({
2340
- name: "inline-flex-center-wrapper",
2341
- category: "flatten/inline-flex-center-wrapper",
2342
- safety: 2,
2343
- doc: {
2344
- title: "Flatten inline-flex-centering wrapper",
2345
- summary: "A div that only centers a single child (display:inline-flex; align-items:center; justify-content:center) is removed; the child gains place-self:center.",
2346
- before: '<div style="display:inline-flex;align-items:center;justify-content:center"><Child/></div>',
2347
- after: '<Child style="place-self:center"/>',
2348
- safetyRationale: "Wrapper paints nothing, carries no ref/handlers/dynamic children, and is not a combinator subject; inheritable styles are folded onto the child before removal."
2349
- },
2350
- match: {
2351
- tag: "div",
2352
- style: { display: "inline-flex", alignItems: "center", justifyContent: "center" },
2353
- onlyChild: "element",
2354
- paintsNothing: true
2355
- },
2356
- rewrite: {
2357
- flattenInto: "child",
2358
- childGains: { placeSelf: "center" }
2359
- },
2360
- // Like its block-level sibling, this centering flatten is `needs-verification` (the wrapper's own
2361
- // `display:inline-flex` establishes a formatting context, and place-self centering only holds under
2362
- // a flex/grid parent), so the conservative production gate (`'provably-safe'`) REVERTS it — every
2363
- // case here is a no-match. Op-level correctness is covered by the invariant suite.
2364
- test: {
2365
- noMatch: [
2366
- // Even under a static flex/grid parent the centering flatten is not provably layout-neutral.
2367
- '<div className="grid"><div className="inline-flex items-center justify-center"><span className="bg-red-200">x</span></div></div>',
2368
- // Non-flex/grid parent (document root) → left unchanged.
2369
- '<div className="inline-flex justify-center items-center"><div className="bg-red-200">Hello</div></div>',
2370
- // onClick is a hard opacity barrier → the wrapper is load-bearing regardless of the gate.
2371
- '<div className="inline-flex justify-center items-center" onClick={handleClick}><div className="bg-red-200">Hello</div></div>'
2652
+ // `p-4` is a NON-inherited padding: removing the box would drop it, so the flatten-safety gate
2653
+ // reverts the unwrap and the wrapper is left unchanged.
2654
+ '<div className="p-4"><p className="bg-red-200">x</p></div>',
2655
+ // A `<p>` wrapper is NOT an inert host box: its UA default display/margins are not captured in the
2656
+ // class-derived computed style, so removing it is not provably layout-neutralleft unchanged.
2657
+ '<p className="text-center"><span className="bg-red-200">x</span></p>'
2372
2658
  ]
2373
2659
  }
2374
2660
  });
2375
2661
 
2376
- // ../patterns/src/library/flatten/nested-flex-merge.pattern.ts
2377
- function baseConditionStyleMap(decls) {
2378
- const map = /* @__PURE__ */ new Map();
2379
- for (const [prop, value] of decls) {
2380
- for (const decl of normalizer.normalizeDeclaration(prop, value, false)) {
2381
- map.set(decl.property, decl);
2382
- }
2383
- }
2384
- const block = { condition: BASE_CONDITION, decls: map };
2385
- const blocks = /* @__PURE__ */ new Map([[conditionKey(BASE_CONDITION), block]]);
2386
- return { blocks };
2387
- }
2388
- var DISPLAY_FLEX = baseConditionStyleMap([["display", "flex"]]);
2389
- var FLEX_CONTAINER_PROPERTIES = /* @__PURE__ */ new Set([
2390
- "display",
2391
- "flex-direction",
2392
- "flex-wrap",
2393
- "justify-content",
2394
- "align-items",
2395
- "align-content",
2396
- "place-content",
2397
- "place-items",
2398
- "row-gap",
2399
- "column-gap"
2400
- ]);
2401
- function outerMergeSafe(sm) {
2402
- const norm = normalizer.normalizeStyleMap(sm);
2403
- for (const block of norm.blocks.values()) {
2404
- for (const decl of block.decls.values()) {
2405
- if (FLEX_CONTAINER_PROPERTIES.has(String(decl.property))) continue;
2406
- if (decl.inherited) continue;
2407
- return false;
2408
- }
2409
- }
2410
- return true;
2662
+ // ../patterns/src/library/wrapper/passthrough-wrapper.pattern.ts
2663
+ function metaOf2(node) {
2664
+ const n = node;
2665
+ return n.kind === "element" ? n.meta : null;
2411
2666
  }
2412
- function flexConflict(outer, inner) {
2413
- const a = normalizer.normalizeStyleMap(outer);
2414
- const b = normalizer.normalizeStyleMap(inner);
2415
- for (const [key, blockA] of a.blocks) {
2416
- const blockB = b.blocks.get(key);
2417
- if (!blockB) continue;
2418
- for (const [prop, declA] of blockA.decls) {
2419
- if (!FLEX_CONTAINER_PROPERTIES.has(String(prop))) continue;
2420
- const declB = blockB.decls.get(prop);
2421
- if (declB && declB.value !== declA.value) return true;
2422
- }
2423
- }
2424
- return false;
2425
- }
2426
- function extractFlexStyle(sm) {
2427
- const blocks = /* @__PURE__ */ new Map();
2428
- for (const [key, block] of sm.blocks) {
2429
- const decls = /* @__PURE__ */ new Map();
2430
- for (const [prop, decl] of block.decls) {
2431
- if (FLEX_CONTAINER_PROPERTIES.has(String(prop))) decls.set(prop, decl);
2432
- }
2433
- if (decls.size > 0) blocks.set(key, { condition: block.condition, decls });
2434
- }
2435
- return { blocks };
2436
- }
2437
- var isInnerFlex = and(
2438
- isElement("div"),
2439
- computed(DISPLAY_FLEX),
2440
- not(targetedByCombinator)
2441
- );
2442
- var nestedFlexMerge = definePattern({
2443
- name: "nested-flex-merge",
2444
- category: "flatten/nested-flex-merge",
2445
- safety: 2,
2446
- doc: {
2447
- title: "Merge nested flex containers",
2448
- summary: "A flex container whose only child is itself a flex container with non-conflicting flex properties is collapsed into one; the wrapper is removed and its flex declarations merge onto the surviving child.",
2449
- before: '<div style="display:flex;align-items:center;gap:8px"><div style="display:flex;flex-direction:column"/></div>',
2450
- after: '<div style="display:flex;flex-direction:column;align-items:center;gap:8px"/>',
2451
- safetyRationale: "The wrapper paints nothing, declares only flex-container/inheritable properties, carries no ref/handlers/dynamic children, and is not a combinator subject; the two containers do not conflict on any flex property, so the union is unambiguous and lossless."
2452
- },
2453
- match: {
2454
- tag: "div",
2455
- style: { display: "flex" },
2456
- onlyChild: "element",
2457
- paintsNothing: true
2458
- },
2459
- rewrite: (ctx, rw) => {
2460
- const outer = ctx.node;
2461
- const inner = ctx.onlyElementChild();
2462
- if (!inner) return null;
2463
- if (!isInnerFlex(inner, ctx)) return null;
2464
- const outerStyle = ctx.computed();
2465
- const innerStyle = ctx.computedOf(inner);
2466
- if (!outerMergeSafe(outerStyle)) return null;
2467
- if (flexConflict(outerStyle, innerStyle)) return null;
2468
- return [
2469
- // 1. Preserve inheritable values (color/font/…) by folding them onto the child first.
2470
- rw.foldInheritedStyles(outer, inner, { conditions: "all" }),
2471
- // 2. Transfer the wrapper's flex-container declarations onto the child (target-wins keeps the
2472
- // child's value for any shared property — identical anyway, we proved non-conflict).
2473
- rw.mergeStyle(inner, null, extractFlexStyle(outerStyle), "target-wins"),
2474
- // 3. Remove the wrapper (structural-safe; hoists the child and preserves its IRNodeId).
2475
- rw.unwrap(outer)
2476
- ];
2477
- },
2478
- // Merging the outer flex container into the inner removes the outer's box, but a `display:flex`
2479
- // wrapper establishes a formatting context, so this is a `needs-verification` flatten that the
2480
- // conservative production gate (`'provably-safe'`) REVERTS — every case here is a no-match. The
2481
- // merge's op-level correctness (purity, id-preserving unwrap, opacity-barrier safety) is asserted
2482
- // by the invariant suite over every pattern.
2483
- test: {
2484
- noMatch: [
2485
- // The merge is real but not provably layout-neutral (the wrapper establishes a flex context),
2486
- // so under the conservative gate the nested containers are left in place.
2487
- '<div className="flex items-center gap-2" data-x="1"><div className="flex flex-col">X</div></div>',
2488
- // A non-flex wrapper does not match the flex-container signature → left unchanged anyway.
2489
- '<div className="block bg-blue-500"><div className="flex flex-col">X</div></div>'
2490
- ]
2491
- }
2492
- });
2493
-
2494
- // ../patterns/src/library/flatten/nested-grid-merge.pattern.ts
2495
- function baseConditionStyleMap2(decls) {
2496
- const map = /* @__PURE__ */ new Map();
2497
- for (const [prop, value] of decls) {
2498
- for (const decl of normalizer.normalizeDeclaration(prop, value, false)) {
2499
- map.set(decl.property, decl);
2500
- }
2501
- }
2502
- const block = { condition: BASE_CONDITION, decls: map };
2503
- const blocks = /* @__PURE__ */ new Map([[conditionKey(BASE_CONDITION), block]]);
2504
- return { blocks };
2505
- }
2506
- var DISPLAY_GRID = baseConditionStyleMap2([["display", "grid"]]);
2507
- var GRID_CONTAINER_PROPERTIES = /* @__PURE__ */ new Set([
2508
- "display",
2509
- "grid-template-columns",
2510
- "grid-template-rows",
2511
- "grid-template-areas",
2512
- "grid-auto-columns",
2513
- "grid-auto-rows",
2514
- "grid-auto-flow",
2515
- "justify-content",
2516
- "align-content",
2517
- "place-content",
2518
- "justify-items",
2519
- "align-items",
2520
- "place-items",
2521
- "row-gap",
2522
- "column-gap"
2523
- ]);
2524
- function outerMergeSafe2(sm) {
2525
- const norm = normalizer.normalizeStyleMap(sm);
2526
- for (const block of norm.blocks.values()) {
2527
- for (const decl of block.decls.values()) {
2528
- if (GRID_CONTAINER_PROPERTIES.has(String(decl.property))) continue;
2529
- if (decl.inherited) continue;
2530
- return false;
2531
- }
2532
- }
2533
- return true;
2534
- }
2535
- function gridConflict(outer, inner) {
2536
- const a = normalizer.normalizeStyleMap(outer);
2537
- const b = normalizer.normalizeStyleMap(inner);
2538
- for (const [key, blockA] of a.blocks) {
2539
- const blockB = b.blocks.get(key);
2540
- if (!blockB) continue;
2541
- for (const [prop, declA] of blockA.decls) {
2542
- if (!GRID_CONTAINER_PROPERTIES.has(String(prop))) continue;
2543
- const declB = blockB.decls.get(prop);
2544
- if (declB && declB.value !== declA.value) return true;
2545
- }
2546
- }
2547
- return false;
2548
- }
2549
- function extractGridStyle(sm) {
2550
- const blocks = /* @__PURE__ */ new Map();
2551
- for (const [key, block] of sm.blocks) {
2552
- const decls = /* @__PURE__ */ new Map();
2553
- for (const [prop, decl] of block.decls) {
2554
- if (GRID_CONTAINER_PROPERTIES.has(String(prop))) decls.set(prop, decl);
2555
- }
2556
- if (decls.size > 0) blocks.set(key, { condition: block.condition, decls });
2557
- }
2558
- return { blocks };
2559
- }
2560
- var isInnerGrid = and(
2561
- isElement("div"),
2562
- computed(DISPLAY_GRID),
2563
- not(targetedByCombinator)
2564
- );
2565
- var nestedGridMerge = definePattern({
2566
- name: "nested-grid-merge",
2567
- category: "flatten/nested-grid-merge",
2568
- safety: 2,
2569
- doc: {
2570
- title: "Merge nested grid containers",
2571
- summary: "A grid container whose only child is itself a grid container with non-conflicting grid properties is collapsed into one; the wrapper is removed and its grid declarations merge onto the surviving child.",
2572
- before: '<div style="display:grid;gap:8px"><div style="display:grid;grid-template-columns:1fr 1fr"/></div>',
2573
- after: '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px"/>',
2574
- safetyRationale: "The wrapper paints nothing, declares only grid-container/inheritable properties, carries no ref/handlers/dynamic children, and is not a combinator subject; the two containers do not conflict on any grid property, so the union is unambiguous and lossless."
2575
- },
2576
- match: {
2577
- tag: "div",
2578
- style: { display: "grid" },
2579
- onlyChild: "element",
2580
- paintsNothing: true
2581
- },
2582
- rewrite: (ctx, rw) => {
2583
- const outer = ctx.node;
2584
- const inner = ctx.onlyElementChild();
2585
- if (!inner) return null;
2586
- if (!isInnerGrid(inner, ctx)) return null;
2587
- const outerStyle = ctx.computed();
2588
- const innerStyle = ctx.computedOf(inner);
2589
- if (!outerMergeSafe2(outerStyle)) return null;
2590
- if (gridConflict(outerStyle, innerStyle)) return null;
2591
- return [
2592
- // 1. Preserve inheritable values (color/font/…) by folding them onto the child first.
2593
- rw.foldInheritedStyles(outer, inner, { conditions: "all" }),
2594
- // 2. Transfer the wrapper's grid-container declarations onto the child (target-wins keeps the
2595
- // child's value for any shared property — identical anyway, we proved non-conflict).
2596
- rw.mergeStyle(inner, null, extractGridStyle(outerStyle), "target-wins"),
2597
- // 3. Remove the wrapper (structural-safe; hoists the child and preserves its IRNodeId).
2598
- rw.unwrap(outer)
2599
- ];
2600
- },
2601
- // Like its flex sibling, this merge removes the outer container's box, but a `display:grid` wrapper
2602
- // establishes a formatting context, so it is a `needs-verification` flatten that the conservative
2603
- // production gate (`'provably-safe'`) REVERTS — every case here is a no-match. Op-level correctness
2604
- // is asserted by the invariant suite over every pattern.
2605
- test: {
2606
- noMatch: [
2607
- // The merge is real but not provably layout-neutral (the wrapper establishes a grid context),
2608
- // so under the conservative gate the nested containers are left in place.
2609
- '<div className="grid gap-2" data-x="1"><div className="grid grid-cols-2">X</div></div>',
2610
- // A non-grid wrapper does not match the grid-container signature → left unchanged anyway.
2611
- '<div className="block bg-blue-500"><div className="grid grid-cols-2">X</div></div>'
2612
- ]
2613
- }
2614
- });
2615
-
2616
- // ../patterns/src/library/flatten/passthrough-wrapper.pattern.ts
2617
- function metaOf2(node) {
2618
- const n = node;
2619
- return n.kind === "element" ? n.meta : null;
2620
- }
2621
- function elementOf(node) {
2622
- const n = node;
2623
- return n.kind === "element" ? n : null;
2667
+ function elementOf(node) {
2668
+ const n = node;
2669
+ return n.kind === "element" ? n : null;
2624
2670
  }
2625
2671
  var establishesContext = (node) => {
2626
2672
  const m = metaOf2(node);
@@ -2628,7 +2674,7 @@ var establishesContext = (node) => {
2628
2674
  return m.establishesBox || m.establishesFormattingContext || m.establishesStackingContext || m.isContainingBlock || m.declaresCustomProperties;
2629
2675
  };
2630
2676
  var hasSpreadAttrs2 = (node) => metaOf2(node)?.hasSpreadAttrs ?? false;
2631
- var isComponentNode2 = (node) => metaOf2(node)?.isComponent ?? false;
2677
+ var isComponentNode3 = (node) => metaOf2(node)?.isComponent ?? false;
2632
2678
  var hasOwnAttrs2 = (node) => {
2633
2679
  const el = elementOf(node);
2634
2680
  if (!el) return false;
@@ -2642,1247 +2688,141 @@ var targetedByStructuralPseudo3 = (node, ctx) => {
2642
2688
  };
2643
2689
  var passthroughWrapper = definePattern({
2644
2690
  name: "passthrough-wrapper",
2645
- category: "flatten/passthrough-wrapper",
2691
+ category: "flatten/wrapper/passthrough-wrapper",
2646
2692
  safety: 2,
2647
2693
  doc: {
2648
2694
  title: "Flatten passthrough wrapper",
2649
2695
  summary: "A div with no own visual/box style, no attributes beyond an inert class, exactly one element child, and no opacity barriers is removed; its sole child is hoisted in its place.",
2650
2696
  before: "<div><Child/></div>",
2651
2697
  after: "<Child/>",
2652
- safetyRationale: "Wrapper paints nothing and establishes no layout/paint/var context, carries no ref/handlers/dynamic-children/html/spread/component identity, owns no targetable attrs, and is not a combinator/structural-pseudo subject (reparenting changes no match-set); inheritable styles are folded onto the child before removal."
2653
- },
2654
- match: {
2655
- tag: "div",
2656
- onlyChild: "element",
2657
- paintsNothing: true,
2658
- where: [
2659
- not(establishesContext),
2660
- not(hasOwnAttrs2),
2661
- not(hasDynamicClasses),
2662
- not(hasSpreadAttrs2),
2663
- not(isComponentNode2),
2664
- not(targetedByStructuralPseudo3)
2665
- ]
2666
- },
2667
- rewrite: { flattenInto: "child" },
2668
- test: {
2669
- cases: [
2670
- {
2671
- // A plain, style-free wrapper paints nothing and establishes no context → a provably-safe
2672
- // flatten under the conservative gate: the wrapper is removed and its sole child hoisted.
2673
- before: '<div><a className="bg-red-200">Link</a></div>',
2674
- after: '<a className="bg-red-200">Link</a>'
2675
- }
2676
- ],
2677
- noMatch: [
2678
- // A ref pins the wrapper's element identity (a hard opacity barrier) → not a passthrough.
2679
- '<div ref={rootRef}><a className="bg-red-200">Link</a></div>',
2680
- // A `display:flex` wrapper establishes a formatting context, so removing its box is NOT
2681
- // provably layout-neutral → the conservative gate leaves it in place.
2682
- '<div className="flex"><a className="bg-red-200">Link</a></div>'
2683
- ]
2684
- }
2685
- });
2686
-
2687
- // ../patterns/src/library/flatten/redundant-fragment.pattern.ts
2688
- function parentIsRedundantFragment(node, ctx) {
2689
- const el = node;
2690
- if (el.kind !== "element") return false;
2691
- const parentId = el.parent;
2692
- if (parentId == null) return false;
2693
- const parent = ctx.doc.nodes.get(parentId);
2694
- if (!parent || parent.kind !== "fragment") return false;
2695
- if (parent.parent == null) return false;
2696
- if (parent.children.length !== 1) return false;
2697
- const m = parent.meta;
2698
- if (m.hasKey || m.hasRef || m.hasEventHandlers || m.hasDynamicChildren || m.hasDangerousHtml || m.hasSpreadAttrs || m.isComponent) {
2699
- return false;
2700
- }
2701
- if (m.targetedByCombinator || m.targetedByStructuralPseudo) return false;
2702
- const fid = parentId;
2703
- if (ctx.selectors.targetedByCombinator(fid) || ctx.selectors.targetedByStructuralPseudo(fid)) {
2704
- return false;
2705
- }
2706
- if (ctx.selectors.reparentImpact(fid).size > 0) return false;
2707
- return true;
2708
- }
2709
- var redundantFragment = definePattern({
2710
- name: "redundant-fragment",
2711
- category: "flatten/redundant-fragment",
2712
- safety: 1,
2713
- doc: {
2714
- title: "Flatten redundant single-child fragment",
2715
- summary: "A fragment whose only child is a single node is removed; the child is spliced up into the fragment's slot, preserving its IRNodeId, siblings, attributes and the CSS cascade.",
2716
- before: "<><Child/></>",
2717
- after: "<Child/>",
2718
- safetyRationale: "A fragment paints nothing and renders no box; with exactly one child its removal changes no sibling/structural-pseudo match-set. Keyed fragments and fragments carrying ref/handlers/dynamic-children/raw-html/spread are excluded as opacity barriers."
2719
- },
2720
- match: parentIsRedundantFragment,
2721
- rewrite: (ctx, rw) => {
2722
- const parentId = ctx.node.parent;
2723
- if (parentId == null) return null;
2724
- const fragment = ctx.doc.nodes.get(parentId);
2725
- if (!fragment || fragment.kind !== "fragment") return null;
2726
- return [rw.unwrap(fragment)];
2727
- },
2728
- test: {
2729
- cases: [
2730
- {
2731
- // A fragment renders no box, so unwrapping a single-child fragment is always layout-identical
2732
- // → a provably-safe flatten: the child is spliced up into the fragment's slot.
2733
- before: '<><span className="bg-red-200">Hi</span></>',
2734
- after: '<span className="bg-red-200">Hi</span>'
2735
- }
2736
- ],
2737
- noMatch: [
2738
- // Two children ⇒ not a single-child fragment, so the fragment is load-bearing and stays.
2739
- '<><span className="bg-red-200">A</span><span className="bg-green-200">B</span></>'
2740
- ]
2741
- }
2742
- });
2743
-
2744
- // ../patterns/src/library/flatten/redundant-inline-wrapper.pattern.ts
2745
- function asEl3(node) {
2746
- const n = node;
2747
- return n.kind === "element" ? n : null;
2748
- }
2749
- function metaOf3(node) {
2750
- return asEl3(node)?.meta ?? null;
2751
- }
2752
- var establishesContext2 = (node) => {
2753
- const m = metaOf3(node);
2754
- if (!m) return false;
2755
- return m.establishesBox || m.establishesFormattingContext || m.establishesStackingContext || m.isContainingBlock || m.declaresCustomProperties;
2756
- };
2757
- var hasSpreadAttrs3 = (node) => metaOf3(node)?.hasSpreadAttrs ?? false;
2758
- var isComponentNode3 = (node) => metaOf3(node)?.isComponent ?? false;
2759
- var hasOwnAttrs3 = (node) => {
2760
- const el = asEl3(node);
2761
- if (!el) return false;
2762
- return el.attrs.entries.size > 0 || el.attrs.spreads.length > 0;
2763
- };
2764
- var targetedByStructuralPseudo4 = (node, ctx) => {
2765
- const el = asEl3(node);
2766
- if (!el) return false;
2767
- if (el.meta.targetedByStructuralPseudo) return true;
2768
- return ctx.selectors.targetedByStructuralPseudo(el.id);
2769
- };
2770
- var DISPLAY3 = "display";
2771
- var hasNonInlineDisplay = (node, ctx) => {
2772
- const el = asEl3(node);
2773
- if (!el) return false;
2774
- const sm = ctx.computedOf(el) ?? el.computed;
2775
- for (const block of sm.blocks.values()) {
2776
- const decl = block.decls.get(DISPLAY3);
2777
- if (decl && String(decl.value) !== "inline") return true;
2778
- }
2779
- return false;
2780
- };
2781
- var redundantInlineWrapper = definePattern({
2782
- name: "redundant-inline-wrapper",
2783
- category: "flatten/redundant-inline-wrapper",
2784
- safety: 2,
2785
- doc: {
2786
- title: "Flatten redundant inline wrapper",
2787
- summary: "An inline span with no own visual/box style, no attributes beyond an inert class, exactly one element child, and no opacity barriers is removed; its sole child is hoisted in its place.",
2788
- before: "<span><Child/></span>",
2789
- after: "<Child/>",
2790
- safetyRationale: "An empty inline box paints nothing and establishes no layout/paint/var context; with the inline default display and a single element child its removal changes no paint and no flow. The span carries no ref/handlers/dynamic-children/html/spread/component identity, owns no targetable attrs, and is not a combinator/structural-pseudo subject; inheritable styles are folded onto the child before removal."
2791
- },
2792
- match: {
2793
- tag: "span",
2794
- onlyChild: "element",
2795
- paintsNothing: true,
2796
- where: [
2797
- not(hasNonInlineDisplay),
2798
- not(establishesContext2),
2799
- not(hasOwnAttrs3),
2800
- not(hasDynamicClasses),
2801
- not(hasSpreadAttrs3),
2802
- not(isComponentNode3),
2803
- not(targetedByStructuralPseudo4)
2804
- ]
2805
- },
2806
- rewrite: { flattenInto: "child" },
2807
- test: {
2808
- cases: [
2809
- {
2810
- // An empty inline span paints nothing and establishes no context → a provably-safe flatten:
2811
- // the span is removed and its sole child hoisted in place.
2812
- before: '<span><a className="text-blue-500">Link</a></span>',
2813
- after: '<a className="text-blue-500">Link</a>'
2814
- }
2815
- ],
2816
- noMatch: [
2817
- // A ref pins the span's element identity (a hard opacity barrier) → not a passthrough.
2818
- '<span ref={spanRef}><a className="text-blue-500">Link</a></span>',
2819
- // The span paints its own background (own visual style) → kept.
2820
- '<span className="bg-green-200"><a className="text-blue-500">Link</a></span>',
2821
- // Non-inline display (inline-block) participates in layout differently → kept.
2822
- '<span className="inline-block"><a className="text-blue-500">Link</a></span>'
2823
- ]
2824
- }
2825
- });
2826
-
2827
- // ../patterns/src/library/compress/border-radius-shorthand.pattern.ts
2828
- var CORNERS = [
2829
- "border-top-left-radius",
2830
- "border-top-right-radius",
2831
- "border-bottom-right-radius",
2832
- "border-bottom-left-radius"
2833
- ];
2834
- var CORNER_SET = new Set(CORNERS);
2835
- var BASE_KEY = conditionKey(BASE_CONDITION);
2836
- var RADIUS = "border-radius";
2837
- var NON_COLLAPSIBLE_VALUES = /* @__PURE__ */ new Set([
2838
- "initial",
2839
- "inherit",
2840
- "unset",
2841
- "revert",
2842
- "revert-layer"
2843
- ]);
2844
- function analyzeRadius(sm) {
2845
- const block = sm.blocks.get(BASE_KEY);
2846
- if (!block) return null;
2847
- const corners = [];
2848
- for (const corner of CORNERS) {
2849
- const decl = block.decls.get(corner);
2850
- if (!decl) return null;
2851
- corners.push(decl);
2852
- }
2853
- const important = corners[0].important;
2854
- if (!corners.every((d) => d.important === important)) return null;
2855
- const value = String(corners[0].value);
2856
- if (NON_COLLAPSIBLE_VALUES.has(value)) return null;
2857
- if (!corners.every((d) => String(d.value) === value)) return null;
2858
- const relative = corners.some((d) => d.relativeToParent);
2859
- return { value, important, relative };
2860
- }
2861
- function withFoldedRadius(sm, fold) {
2862
- const blocks = /* @__PURE__ */ new Map();
2863
- for (const [key, block] of sm.blocks) {
2864
- if (key !== BASE_KEY) {
2865
- blocks.set(key, block);
2866
- continue;
2867
- }
2868
- const decls = /* @__PURE__ */ new Map();
2869
- for (const [prop, decl] of block.decls) {
2870
- if (CORNER_SET.has(String(prop))) continue;
2871
- decls.set(prop, decl);
2872
- }
2873
- const shorthand = {
2874
- property: RADIUS,
2875
- value: fold.value,
2876
- important: fold.important,
2877
- relativeToParent: fold.relative,
2878
- inherited: false
2879
- // border-radius is never inherited
2880
- };
2881
- decls.set(shorthand.property, shorthand);
2882
- blocks.set(key, { condition: block.condition, decls });
2883
- }
2884
- return { blocks };
2885
- }
2886
- var borderRadiusShorthand = definePattern({
2887
- name: "border-radius-shorthand",
2888
- category: "compress/border-radius-shorthand",
2889
- safety: 1,
2890
- doc: {
2891
- title: "Collapse equal corner radii into border-radius",
2892
- summary: "An element whose four corner radii (border-*-radius longhands) are all equal is rewritten to the single Tailwind rounded-* utility (border-radius === the four equal corners).",
2893
- before: '<div class="rounded-tl-lg rounded-tr-lg rounded-br-lg rounded-bl-lg"/>',
2894
- after: '<div class="rounded-lg"/>',
2895
- safetyRationale: "`border-radius` is value-identical to four equal corner radii \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
2896
- },
2897
- rewrite: {
2898
- rewriteClasses(computed2) {
2899
- const fold = analyzeRadius(computed2);
2900
- return fold ? withFoldedRadius(computed2, fold) : null;
2901
- }
2902
- },
2903
- test: {
2904
- cases: [
2905
- {
2906
- // The four equal corner longhands collapse to a `border-radius` decl at the IR level; the
2907
- // minimizing reverse-emit then picks the single shortest utility (`rounded-lg`) that reproduces
2908
- // it, replacing the four `rounded-{tl,tr,br,bl}-lg` tokens. `bg-red-200` is preserved.
2909
- before: '<div className="rounded-tl-lg rounded-tr-lg rounded-br-lg rounded-bl-lg bg-red-200">box</div>',
2910
- after: '<div className="bg-red-200 rounded-lg">box</div>'
2911
- }
2912
- ],
2913
- // Corners differ (top corners vs bottom corners) → no all-equal collapse.
2914
- noMatch: ['<div className="rounded-t-lg rounded-b-sm bg-red-200">box</div>']
2915
- }
2916
- });
2917
-
2918
- // ../patterns/src/library/compress/border-shorthand.pattern.ts
2919
- var WIDTH_SIDES = [
2920
- "border-top-width",
2921
- "border-right-width",
2922
- "border-bottom-width",
2923
- "border-left-width"
2924
- ];
2925
- var WIDTH_SIDE_SET = new Set(WIDTH_SIDES);
2926
- var BASE_KEY2 = conditionKey(BASE_CONDITION);
2927
- var BORDER_WIDTH = "border-width";
2928
- function analyzeWidth(sm) {
2929
- const block = sm.blocks.get(BASE_KEY2);
2930
- if (!block) return null;
2931
- const sides = [];
2932
- for (const side of WIDTH_SIDES) {
2933
- const decl = block.decls.get(side);
2934
- if (!decl) return null;
2935
- sides.push(decl);
2936
- }
2937
- const [top, right, bottom, left] = sides;
2938
- if (!(top.important === right.important && right.important === bottom.important && bottom.important === left.important)) {
2939
- return null;
2940
- }
2941
- const tv = String(top.value);
2942
- const rv = String(right.value);
2943
- const bv = String(bottom.value);
2944
- const lv = String(left.value);
2945
- if (tv !== bv || lv !== rv) return null;
2946
- const value = tv === lv ? tv : `${tv} ${lv}`;
2947
- const relative = sides.some((d) => d.relativeToParent);
2948
- return { value, important: top.important, relative };
2949
- }
2950
- function withFoldedWidth(sm, fold) {
2951
- const blocks = /* @__PURE__ */ new Map();
2952
- for (const [key, block] of sm.blocks) {
2953
- if (key !== BASE_KEY2) {
2954
- blocks.set(key, block);
2955
- continue;
2956
- }
2957
- const decls = /* @__PURE__ */ new Map();
2958
- for (const [prop, decl] of block.decls) {
2959
- if (WIDTH_SIDE_SET.has(String(prop))) continue;
2960
- decls.set(prop, decl);
2961
- }
2962
- const shorthand = {
2963
- property: BORDER_WIDTH,
2964
- value: fold.value,
2965
- important: fold.important,
2966
- relativeToParent: fold.relative,
2967
- inherited: false
2968
- // border-width is never inherited
2969
- };
2970
- decls.set(shorthand.property, shorthand);
2971
- blocks.set(key, { condition: block.condition, decls });
2972
- }
2973
- return { blocks };
2974
- }
2975
- var borderShorthand = definePattern({
2976
- name: "border-shorthand",
2977
- category: "compress/border-shorthand",
2978
- safety: 1,
2979
- doc: {
2980
- title: "Collapse border-width longhands to shorthand",
2981
- summary: "Equal border width on all four sides (or matching x/y pairs) expressed as separate longhand declarations is collapsed to the shortest equivalent border-width shorthand (border-* / border-x-* border-y-*).",
2982
- before: '<div class="border-t-2 border-r-2 border-b-2 border-l-2"/>',
2983
- after: '<div class="border-2"/>',
2984
- safetyRationale: "A value-preserving re-serialization of the same computed border widths (style/color longhands untouched) \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
2985
- },
2986
- rewrite: {
2987
- rewriteClasses(computed2) {
2988
- const fold = analyzeWidth(computed2);
2989
- return fold ? withFoldedWidth(computed2, fold) : null;
2990
- }
2991
- },
2992
- test: {
2993
- cases: [
2994
- {
2995
- // The four equal width longhands collapse to a `border-width` shorthand at the IR level, and the
2996
- // minimizing reverse-emit picks the single shortest utility (`border-2`) that reproduces it,
2997
- // replacing the four `border-{t,r,b,l}-2` tokens. `bg-red-200` is preserved.
2998
- before: '<div className="border-t-2 border-r-2 border-b-2 border-l-2 bg-red-200">box</div>',
2999
- after: '<div className="bg-red-200 border-2">box</div>'
3000
- }
3001
- ],
3002
- // Asymmetric widths (top != bottom) cannot fold into a shorthand.
3003
- noMatch: ['<div className="border-t-2 border-r-4 border-b-8 border-l-4 bg-red-200">box</div>']
3004
- }
3005
- });
3006
-
3007
- // ../patterns/src/library/compress/dedupe-classes.pattern.ts
3008
- function findRedundantClasses(computed2) {
3009
- const winners = /* @__PURE__ */ new Set();
3010
- const shadowed = /* @__PURE__ */ new Set();
3011
- for (const block of computed2.blocks.values()) {
3012
- for (const decl of block.decls.values()) {
3013
- if (decl.origin && decl.origin.kind === "class") winners.add(decl.origin.className);
3014
- for (const o of decl.shadowed ?? []) {
3015
- if (o.kind === "class") shadowed.add(o.className);
3016
- }
3017
- }
3018
- }
3019
- return { winners, shadowed };
3020
- }
3021
- var dedupeClasses = definePattern({
3022
- name: "dedupe-classes",
3023
- category: "compress/dedupe-classes",
3024
- safety: 1,
3025
- doc: {
3026
- title: "Dedupe fully-overridden class tokens",
3027
- summary: "Drops class tokens whose every declaration is overridden by a later token resolving to the same property; the surviving token set produces a byte-for-byte identical computed style.",
3028
- before: '<p class="text-sm text-lg" />',
3029
- after: '<p class="text-lg" />',
3030
- safetyRationale: "A fully-overridden token contributes nothing to the computed style in any condition, so removing it changes no pixels \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
3031
- },
3032
- rewrite: {
3033
- dropClasses(computed2, ctx) {
3034
- const { winners, shadowed } = findRedundantClasses(computed2);
3035
- const drop = /* @__PURE__ */ new Set();
3036
- for (const cls of shadowed) {
3037
- if (winners.has(cls)) continue;
3038
- if (!ctx.resolver.selectorUsage(cls).droppable) continue;
3039
- drop.add(cls);
3040
- }
3041
- return drop;
3042
- }
3043
- },
3044
- test: {
3045
- cases: [
3046
- {
3047
- // `text-sm` is fully overridden by `text-lg` (both set font-size + line-height). The resolver
3048
- // records that shadowing in provenance and reports the Tailwind utility as droppable, so the
3049
- // pattern drops `text-sm`; the reverse-emit then re-derives the minimal set (`text-lg`).
3050
- before: '<p className="text-sm text-lg">Hi</p>',
3051
- after: '<p className="text-lg">Hi</p>'
3052
- }
3053
- ],
3054
- // Both tokens win a distinct property (no full override) → nothing to dedupe.
3055
- noMatch: ['<p className="text-lg font-bold">Hi</p>']
3056
- }
3057
- });
3058
-
3059
- // ../patterns/src/library/compress/gap-shorthand.pattern.ts
3060
- var ROW_GAP = "row-gap";
3061
- var COLUMN_GAP = "column-gap";
3062
- var GAP = "gap";
3063
- var BASE_KEY3 = conditionKey(BASE_CONDITION);
3064
- function withGapShorthand(sm, gapDecl) {
3065
- const blocks = /* @__PURE__ */ new Map();
3066
- for (const [key, block] of sm.blocks) {
3067
- if (key !== BASE_KEY3) {
3068
- blocks.set(key, block);
3069
- continue;
3070
- }
3071
- const decls = /* @__PURE__ */ new Map();
3072
- for (const [prop, decl] of block.decls) {
3073
- if (prop === ROW_GAP || prop === COLUMN_GAP) continue;
3074
- decls.set(prop, decl);
3075
- }
3076
- decls.set(gapDecl.property, gapDecl);
3077
- blocks.set(key, { condition: block.condition, decls });
3078
- }
3079
- return { blocks };
3080
- }
3081
- var gapShorthand = definePattern({
3082
- name: "gap-shorthand",
3083
- category: "compress/gap-shorthand",
3084
- safety: 1,
3085
- doc: {
3086
- title: "Collapse equal row/column gap into the `gap` shorthand",
3087
- summary: "An element whose computed row-gap and column-gap are equal has the two axis longhands collapsed into a single-value `gap` shorthand (Tailwind gap-x-* gap-y-* \u2192 gap-*).",
3088
- before: '<div style="row-gap:16px;column-gap:16px"/>',
3089
- after: '<div style="gap:16px"/>',
3090
- safetyRationale: "A single-value `gap` is value-identical to an equal row-gap+column-gap pair \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
3091
- },
3092
- rewrite: {
3093
- rewriteClasses(computed2) {
3094
- const base = computed2.blocks.get(BASE_KEY3);
3095
- if (!base) return null;
3096
- const rowGap = base.decls.get(ROW_GAP);
3097
- const colGap = base.decls.get(COLUMN_GAP);
3098
- if (!rowGap || !colGap) return null;
3099
- if (rowGap.important !== colGap.important) return null;
3100
- if (rowGap.value !== colGap.value) return null;
3101
- const gapDecl = {
3102
- property: GAP,
3103
- value: rowGap.value,
3104
- important: rowGap.important,
3105
- relativeToParent: rowGap.relativeToParent || colGap.relativeToParent,
3106
- inherited: false
3107
- // gap is not an inherited property
3108
- };
3109
- return withGapShorthand(computed2, gapDecl);
3110
- }
3111
- },
3112
- test: {
3113
- cases: [
3114
- {
3115
- // Equal row/column gap collapse to a `gap` decl at the IR level; the minimizing reverse-emit
3116
- // re-expands `gap` to row-gap+column-gap and picks the single utility covering both (`gap-4`),
3117
- // replacing the `gap-x-4`+`gap-y-4` pair. `bg-red-200` is preserved.
3118
- before: '<div className="gap-x-4 gap-y-4 bg-red-200">box</div>',
3119
- after: '<div className="bg-red-200 gap-4">box</div>'
3120
- }
3121
- ],
3122
- // Unequal axes (row-gap != column-gap) have no single-value `gap` equivalent → not collapsed.
3123
- noMatch: ['<div className="gap-x-2 gap-y-4 bg-red-200">box</div>']
3124
- }
3125
- });
3126
-
3127
- // ../patterns/src/library/compress/inset-shorthand.pattern.ts
3128
- var TOP = "top";
3129
- var RIGHT = "right";
3130
- var BOTTOM = "bottom";
3131
- var LEFT = "left";
3132
- var INSET = "inset";
3133
- var INSET_BLOCK = "inset-block";
3134
- var INSET_INLINE = "inset-inline";
3135
- function sameSide(a, b) {
3136
- return a !== void 0 && b !== void 0 && a.value === b.value && a.important === b.important;
3137
- }
3138
- function asProperty(src, property) {
3139
- return { ...src, property, inherited: normalizer.inherited.isInherited(property) };
3140
- }
3141
- function withBaseDecls(src, baseDecls) {
3142
- const blocks = /* @__PURE__ */ new Map();
3143
- for (const [key, block] of src.blocks) {
3144
- const decls = key === BASE_CONDITION_KEY ? baseDecls : new Map(block.decls);
3145
- blocks.set(key, { condition: block.condition, decls });
3146
- }
3147
- return { blocks };
3148
- }
3149
- var insetShorthand = definePattern({
3150
- name: "inset-shorthand",
3151
- category: "compress/inset-shorthand",
3152
- safety: 2,
3153
- doc: {
3154
- title: "Compress inset longhands into a shorthand",
3155
- summary: "top/right/bottom/left set to one value collapse to `inset`; a matching top/bottom or left/right pair collapses to `inset-block` / `inset-inline` (Tailwind inset-y / inset-x).",
3156
- before: '<div style="top:10px;right:10px;bottom:10px;left:10px"/>',
3157
- after: '<div style="inset:10px"/>',
3158
- safetyRationale: "Meaning-preserving inset shorthand compaction \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
3159
- },
3160
- rewrite: {
3161
- rewriteClasses(computed2) {
3162
- const base = computed2.blocks.get(BASE_CONDITION_KEY);
3163
- if (!base) return null;
3164
- const top = base.decls.get(TOP);
3165
- const right = base.decls.get(RIGHT);
3166
- const bottom = base.decls.get(BOTTOM);
3167
- const left = base.decls.get(LEFT);
3168
- const next = new Map(base.decls);
3169
- if (top && sameSide(top, right) && sameSide(top, bottom) && sameSide(top, left)) {
3170
- next.delete(TOP);
3171
- next.delete(RIGHT);
3172
- next.delete(BOTTOM);
3173
- next.delete(LEFT);
3174
- next.set(INSET, asProperty(top, INSET));
3175
- } else {
3176
- let collapsed = false;
3177
- if (sameSide(top, bottom)) {
3178
- next.delete(TOP);
3179
- next.delete(BOTTOM);
3180
- next.set(INSET_BLOCK, asProperty(top, INSET_BLOCK));
3181
- collapsed = true;
3182
- }
3183
- if (sameSide(left, right)) {
3184
- next.delete(LEFT);
3185
- next.delete(RIGHT);
3186
- next.set(INSET_INLINE, asProperty(left, INSET_INLINE));
3187
- collapsed = true;
3188
- }
3189
- if (!collapsed) return null;
3190
- }
3191
- return withBaseDecls(computed2, next);
3192
- }
3193
- },
3194
- test: {
3195
- cases: [
3196
- {
3197
- // The four equal inset longhands collapse to an `inset` shorthand at the IR level; the
3198
- // minimizing reverse-emit expands it back to top/right/bottom/left and picks the single utility
3199
- // covering all four (`inset-0`), replacing the four physical-side tokens. `bg-red-200` survives.
3200
- before: '<div className="top-0 right-0 bottom-0 left-0 bg-red-200">box</div>',
3201
- after: '<div className="bg-red-200 inset-0">box</div>'
3202
- }
3203
- ],
3204
- // No matching inset pair (all four distinct) → nothing collapses.
3205
- noMatch: ['<div className="top-0 right-1 bottom-2 left-3 bg-red-200">box</div>']
3206
- }
3207
- });
3208
-
3209
- // ../patterns/src/library/compress/margin-shorthand.pattern.ts
3210
- var MARGIN_SIDES = [
3211
- "margin-top",
3212
- "margin-right",
3213
- "margin-bottom",
3214
- "margin-left"
3215
- ];
3216
- var MARGIN_SIDE_SET = new Set(MARGIN_SIDES);
3217
- var BASE_KEY4 = conditionKey(BASE_CONDITION);
3218
- function collapseMarginValue(top, right, bottom, left) {
3219
- if (right === left) {
3220
- if (top === bottom) {
3221
- return top === right ? top : `${top} ${right}`;
3222
- }
3223
- return `${top} ${right} ${bottom}`;
3224
- }
3225
- return `${top} ${right} ${bottom} ${left}`;
3226
- }
3227
- function withFoldedMargin(sm, marginDecl) {
3228
- const blocks = /* @__PURE__ */ new Map();
3229
- for (const [key, block] of sm.blocks) {
3230
- if (key !== BASE_KEY4) {
3231
- blocks.set(key, block);
3232
- continue;
3233
- }
3234
- const decls = /* @__PURE__ */ new Map();
3235
- for (const [prop, decl] of block.decls) {
3236
- if (!MARGIN_SIDE_SET.has(String(prop))) decls.set(prop, decl);
3237
- }
3238
- decls.set(marginDecl.property, marginDecl);
3239
- blocks.set(key, { condition: block.condition, decls });
3240
- }
3241
- return { blocks };
3242
- }
3243
- var marginShorthand = definePattern({
3244
- name: "margin-shorthand",
3245
- category: "compress/margin-shorthand",
3246
- safety: 2,
3247
- doc: {
3248
- title: "Compress margin longhands into the `margin` shorthand",
3249
- summary: "An element with margin-top/right/bottom/left all set has them collapsed into the shortest legal `margin` shorthand (the m / mx / my forms); meaning is preserved, declaration count drops.",
3250
- before: '<div style="margin-top:8px;margin-right:8px;margin-bottom:8px;margin-left:8px"/>',
3251
- after: '<div style="margin:8px"/>',
3252
- safetyRationale: "A pure representation change of the same computed margins (no pixels move) \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
3253
- },
3254
- rewrite: {
3255
- rewriteClasses(computed2) {
3256
- const base = computed2.blocks.get(BASE_KEY4);
3257
- if (!base) return null;
3258
- const sides = MARGIN_SIDES.map((p) => base.decls.get(p));
3259
- if (sides.some((d) => d === void 0)) return null;
3260
- const [mt, mr, mb, ml] = sides;
3261
- if (mt.important || mr.important || mb.important || ml.important) return null;
3262
- const value = collapseMarginValue(
3263
- String(mt.value),
3264
- String(mr.value),
3265
- String(mb.value),
3266
- String(ml.value)
3267
- );
3268
- const marginDecl = {
3269
- property: "margin",
3270
- value,
3271
- important: false,
3272
- relativeToParent: mt.relativeToParent || mr.relativeToParent || mb.relativeToParent || ml.relativeToParent,
3273
- inherited: false
3274
- // margin is not an inherited property
3275
- };
3276
- return withFoldedMargin(computed2, marginDecl);
3277
- }
3278
- },
3279
- test: {
3280
- cases: [
3281
- {
3282
- // The four equal margin longhands collapse to a `margin` shorthand at the IR level, and the
3283
- // minimizing reverse-emit picks the single shortest utility (`m-2`) reproducing it, replacing
3284
- // the four `m{t,r,b,l}-2` tokens. `bg-red-200` is preserved.
3285
- before: '<div className="mt-2 mr-2 mb-2 ml-2 bg-red-200">box</div>',
3286
- after: '<div className="bg-red-200 m-2">box</div>'
3287
- }
3288
- ],
3289
- // Only two margin sides set → the four-longhand `margin` collapse does not apply.
3290
- noMatch: ['<div className="mt-2 mb-2 bg-red-200">box</div>']
3291
- }
3292
- });
3293
-
3294
- // ../patterns/src/library/compress/overflow-shorthand.pattern.ts
3295
- var OVERFLOW_X = "overflow-x";
3296
- var OVERFLOW_Y = "overflow-y";
3297
- var OVERFLOW = "overflow";
3298
- var BASE_KEY5 = conditionKey(BASE_CONDITION);
3299
- function withOverflowShorthand(sm, overflowDecl) {
3300
- const blocks = /* @__PURE__ */ new Map();
3301
- for (const [key, block] of sm.blocks) {
3302
- if (key !== BASE_KEY5) {
3303
- blocks.set(key, block);
3304
- continue;
3305
- }
3306
- const decls = /* @__PURE__ */ new Map();
3307
- for (const [prop, decl] of block.decls) {
3308
- if (prop === OVERFLOW_X || prop === OVERFLOW_Y) continue;
3309
- decls.set(prop, decl);
3310
- }
3311
- decls.set(overflowDecl.property, overflowDecl);
3312
- blocks.set(key, { condition: block.condition, decls });
3313
- }
3314
- return { blocks };
3315
- }
3316
- var overflowShorthand = definePattern({
3317
- name: "overflow-shorthand",
3318
- category: "compress/overflow-shorthand",
3319
- safety: 1,
3320
- doc: {
3321
- title: "Collapse equal overflow axes into the `overflow` shorthand",
3322
- summary: "An element whose computed overflow-x and overflow-y are equal has the two axis longhands collapsed into a single `overflow` shorthand (Tailwind overflow-x-* overflow-y-* \u2192 overflow-*).",
3323
- before: '<div style="overflow-x:auto;overflow-y:auto"/>',
3324
- after: '<div style="overflow:auto"/>',
3325
- safetyRationale: "A single-keyword `overflow` is value-identical to equal overflow-x+overflow-y \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
3326
- },
3327
- rewrite: {
3328
- rewriteClasses(computed2) {
3329
- const base = computed2.blocks.get(BASE_KEY5);
3330
- if (!base) return null;
3331
- const overflowX = base.decls.get(OVERFLOW_X);
3332
- const overflowY = base.decls.get(OVERFLOW_Y);
3333
- if (!overflowX || !overflowY) return null;
3334
- if (overflowX.important !== overflowY.important) return null;
3335
- if (overflowX.value !== overflowY.value) return null;
3336
- const overflowDecl = {
3337
- property: OVERFLOW,
3338
- value: overflowX.value,
3339
- important: overflowX.important,
3340
- relativeToParent: overflowX.relativeToParent || overflowY.relativeToParent,
3341
- inherited: false
3342
- // overflow is not an inherited property
3343
- };
3344
- return withOverflowShorthand(computed2, overflowDecl);
3345
- }
3346
- },
3347
- test: {
3348
- cases: [
3349
- {
3350
- // Equal overflow axes collapse to an `overflow` decl at the IR level; the minimizing
3351
- // reverse-emit picks the single utility covering both (`overflow-auto`), replacing the
3352
- // `overflow-x-auto`+`overflow-y-auto` pair. `bg-red-200` is preserved.
3353
- before: '<div className="overflow-x-auto overflow-y-auto bg-red-200">box</div>',
3354
- after: '<div className="bg-red-200 overflow-auto">box</div>'
3355
- }
3356
- ],
3357
- // Mismatched axes (overflow-x != overflow-y) have no single-keyword equivalent → not collapsed.
3358
- noMatch: ['<div className="overflow-x-auto overflow-y-hidden bg-red-200">box</div>']
3359
- }
3360
- });
3361
-
3362
- // ../patterns/src/library/compress/overscroll-behavior-shorthand.pattern.ts
3363
- var OVERSCROLL_X = "overscroll-behavior-x";
3364
- var OVERSCROLL_Y = "overscroll-behavior-y";
3365
- var OVERSCROLL = "overscroll-behavior";
3366
- var BASE_KEY6 = conditionKey(BASE_CONDITION);
3367
- var NON_COLLAPSIBLE_VALUES2 = /* @__PURE__ */ new Set([
3368
- "initial",
3369
- "inherit",
3370
- "unset",
3371
- "revert",
3372
- "revert-layer"
3373
- ]);
3374
- function withOverscrollShorthand(sm, shorthand) {
3375
- const blocks = /* @__PURE__ */ new Map();
3376
- for (const [key, block] of sm.blocks) {
3377
- if (key !== BASE_KEY6) {
3378
- blocks.set(key, block);
3379
- continue;
3380
- }
3381
- const decls = /* @__PURE__ */ new Map();
3382
- for (const [prop, decl] of block.decls) {
3383
- if (prop === OVERSCROLL_X || prop === OVERSCROLL_Y) continue;
3384
- decls.set(prop, decl);
3385
- }
3386
- decls.set(shorthand.property, shorthand);
3387
- blocks.set(key, { condition: block.condition, decls });
3388
- }
3389
- return { blocks };
3390
- }
3391
- var overscrollBehaviorShorthand = definePattern({
3392
- name: "overscroll-behavior-shorthand",
3393
- category: "compress/overscroll-behavior-shorthand",
3394
- safety: 1,
3395
- doc: {
3396
- title: "Collapse equal overscroll-behavior axes into overscroll-behavior",
3397
- summary: "An element whose computed overscroll-behavior-x and overscroll-behavior-y are equal has the two axis longhands collapsed into a single `overscroll-behavior` shorthand (Tailwind overscroll-x-* overscroll-y-* \u2192 overscroll-*).",
3398
- before: '<div style="overscroll-behavior-x:contain;overscroll-behavior-y:contain"/>',
3399
- after: '<div class="overscroll-contain"/>',
3400
- safetyRationale: "`overscroll-behavior` is value-identical to an equal x+y axis pair \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
3401
- },
3402
- rewrite: {
3403
- rewriteClasses(computed2) {
3404
- const base = computed2.blocks.get(BASE_KEY6);
3405
- if (!base) return null;
3406
- const x = base.decls.get(OVERSCROLL_X);
3407
- const y = base.decls.get(OVERSCROLL_Y);
3408
- if (!x || !y) return null;
3409
- if (x.important !== y.important) return null;
3410
- const value = String(x.value);
3411
- if (NON_COLLAPSIBLE_VALUES2.has(value)) return null;
3412
- if (value !== String(y.value)) return null;
3413
- const shorthand = {
3414
- property: OVERSCROLL,
3415
- value: x.value,
3416
- important: x.important,
3417
- relativeToParent: x.relativeToParent || y.relativeToParent,
3418
- inherited: false
3419
- // overscroll-behavior is not an inherited property
3420
- };
3421
- return withOverscrollShorthand(computed2, shorthand);
3422
- }
3423
- },
3424
- test: {
3425
- cases: [
3426
- {
3427
- // Equal x/y axes collapse to an `overscroll-behavior` decl at the IR level; the minimizing
3428
- // reverse-emit picks the single utility covering both (`overscroll-contain`), replacing the
3429
- // `overscroll-x-contain`+`overscroll-y-contain` pair. `bg-red-200` is preserved.
3430
- before: '<div className="overscroll-x-contain overscroll-y-contain bg-red-200">box</div>',
3431
- after: '<div className="bg-red-200 overscroll-contain">box</div>'
3432
- }
3433
- ],
3434
- // Axes differ (x != y) → no equal-axis collapse.
3435
- noMatch: ['<div className="overscroll-x-contain overscroll-y-auto bg-red-200">box</div>']
3436
- }
3437
- });
3438
-
3439
- // ../patterns/src/library/compress/padding-shorthand.pattern.ts
3440
- var PADDING_SIDES = [
3441
- "padding-top",
3442
- "padding-right",
3443
- "padding-bottom",
3444
- "padding-left"
3445
- ];
3446
- var PADDING_SIDE_SET = new Set(PADDING_SIDES);
3447
- var BASE_KEY7 = conditionKey(BASE_CONDITION);
3448
- function analyzePadding(sm) {
3449
- const block = sm.blocks.get(BASE_KEY7);
3450
- if (!block) return null;
3451
- const sides = [];
3452
- for (const side of PADDING_SIDES) {
3453
- const decl = block.decls.get(side);
3454
- if (!decl) return null;
3455
- sides.push(decl);
3456
- }
3457
- const [top, right, bottom, left] = sides;
3458
- if (!(top.important === right.important && right.important === bottom.important && bottom.important === left.important)) {
3459
- return null;
3460
- }
3461
- const tv = String(top.value);
3462
- const rv = String(right.value);
3463
- const bv = String(bottom.value);
3464
- const lv = String(left.value);
3465
- if (tv !== bv || lv !== rv) return null;
3466
- const value = tv === lv ? tv : `${tv} ${lv}`;
3467
- const relative = sides.some((d) => d.relativeToParent);
3468
- return { value, important: top.important, relative };
3469
- }
3470
- function withFoldedPadding(sm, fold) {
3471
- const blocks = /* @__PURE__ */ new Map();
3472
- for (const [key, block] of sm.blocks) {
3473
- if (key !== BASE_KEY7) {
3474
- blocks.set(key, block);
3475
- continue;
3476
- }
3477
- const decls = /* @__PURE__ */ new Map();
3478
- for (const [prop, decl] of block.decls) {
3479
- if (PADDING_SIDE_SET.has(String(prop))) continue;
3480
- decls.set(prop, decl);
3481
- }
3482
- const shorthand = {
3483
- property: "padding",
3484
- value: fold.value,
3485
- important: fold.important,
3486
- relativeToParent: fold.relative,
3487
- inherited: false
3488
- // padding is never inherited
3489
- };
3490
- decls.set(shorthand.property, shorthand);
3491
- blocks.set(key, { condition: block.condition, decls });
3492
- }
3493
- return { blocks };
3494
- }
3495
- var paddingShorthand = definePattern({
3496
- name: "padding-shorthand",
3497
- category: "compress/padding-shorthand",
3498
- safety: 1,
3499
- doc: {
3500
- title: "Collapse padding longhands to shorthand",
3501
- summary: "Equal padding on all four sides (or matching x/y pairs) expressed as separate longhand declarations is collapsed to the shortest equivalent padding shorthand (p-* / px-* py-*).",
3502
- before: '<div class="pt-4 pr-4 pb-4 pl-4"/>',
3503
- after: '<div class="p-4"/>',
3504
- safetyRationale: "A value-preserving re-serialization of the same computed padding on the same node \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
3505
- },
3506
- rewrite: {
3507
- rewriteClasses(computed2) {
3508
- const fold = analyzePadding(computed2);
3509
- return fold ? withFoldedPadding(computed2, fold) : null;
3510
- }
3511
- },
3512
- test: {
3513
- cases: [
3514
- {
3515
- // The four equal padding longhands collapse to a `padding` shorthand at the IR level, and the
3516
- // minimizing reverse-emit picks the single shortest utility (`p-4`) that reproduces it,
3517
- // replacing the four `p{t,r,b,l}-4` tokens. `bg-red-200` is preserved (its order is stable).
3518
- before: '<div className="pt-4 pr-4 pb-4 pl-4 bg-red-200">box</div>',
3519
- after: '<div className="bg-red-200 p-4">box</div>'
3520
- },
3521
- {
3522
- // A dynamic `{x}` child no longer blocks compress: only the element's OWN class tokens are
3523
- // rewritten (px-4 py-4 → p-4); the dynamic child is untouched by a class-only change. This is
3524
- // the real-app common case (most elements have dynamic content).
3525
- before: '<div className="px-4 py-4">{x}</div>',
3526
- after: '<div className="p-4">{x}</div>'
3527
- }
3528
- ],
3529
- // Asymmetric padding (top != bottom) cannot fold into a shorthand → left unchanged.
3530
- noMatch: ['<div className="pt-2 pr-4 pb-8 pl-4 bg-red-200">box</div>']
3531
- }
3532
- });
3533
-
3534
- // ../patterns/src/library/compress/place-shorthand.pattern.ts
3535
- var ALIGN_ITEMS2 = "align-items";
3536
- var JUSTIFY_ITEMS2 = "justify-items";
3537
- var PLACE_ITEMS2 = "place-items";
3538
- var ALIGN_CONTENT = "align-content";
3539
- var JUSTIFY_CONTENT2 = "justify-content";
3540
- var PLACE_CONTENT = "place-content";
3541
- var BASE_KEY8 = conditionKey(BASE_CONDITION);
3542
- function samePair(a, b) {
3543
- return a !== void 0 && b !== void 0 && a.value === b.value && a.important === b.important;
3544
- }
3545
- function placeDecl(property, align) {
3546
- return {
3547
- property,
3548
- value: align.value,
3549
- important: align.important,
3550
- relativeToParent: false,
3551
- // alignment keywords (center/start/stretch/…) are not length-relative
3552
- inherited: false
3553
- // none of the place-* alignment properties are inherited
3554
- };
3555
- }
3556
- function withBaseDecls2(sm, baseDecls) {
3557
- const blocks = /* @__PURE__ */ new Map();
3558
- for (const [key, block] of sm.blocks) {
3559
- const decls = key === BASE_KEY8 ? new Map(baseDecls) : block.decls;
3560
- blocks.set(key, { condition: block.condition, decls });
3561
- }
3562
- return { blocks };
3563
- }
3564
- var placeShorthand = definePattern({
3565
- name: "place-shorthand",
3566
- category: "compress/place-shorthand",
3567
- safety: 1,
3568
- doc: {
3569
- title: "Collapse matching alignment pairs into `place-*` shorthands",
3570
- summary: "When align-items equals justify-items they collapse to `place-items`; when align-content equals justify-content they collapse to `place-content`. The two collapses are independent.",
3571
- before: '<div style="align-items:center;justify-items:center"/>',
3572
- after: '<div style="place-items:center"/>',
3573
- safetyRationale: "A `place-*` shorthand is value-identical to its equal align/justify pair \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
3574
- },
3575
- rewrite: {
3576
- rewriteClasses(computed2) {
3577
- const base = computed2.blocks.get(BASE_KEY8);
3578
- if (!base) return null;
3579
- const alignItems = base.decls.get(ALIGN_ITEMS2);
3580
- const justifyItems = base.decls.get(JUSTIFY_ITEMS2);
3581
- const alignContent = base.decls.get(ALIGN_CONTENT);
3582
- const justifyContent = base.decls.get(JUSTIFY_CONTENT2);
3583
- const next = new Map(base.decls);
3584
- let collapsed = false;
3585
- if (samePair(alignItems, justifyItems)) {
3586
- next.delete(ALIGN_ITEMS2);
3587
- next.delete(JUSTIFY_ITEMS2);
3588
- next.set(PLACE_ITEMS2, placeDecl(PLACE_ITEMS2, alignItems));
3589
- collapsed = true;
3590
- }
3591
- if (samePair(alignContent, justifyContent)) {
3592
- next.delete(ALIGN_CONTENT);
3593
- next.delete(JUSTIFY_CONTENT2);
3594
- next.set(PLACE_CONTENT, placeDecl(PLACE_CONTENT, alignContent));
3595
- collapsed = true;
3596
- }
3597
- if (!collapsed) return null;
3598
- return withBaseDecls2(computed2, next);
3599
- }
3600
- },
3601
- test: {
3602
- cases: [
3603
- {
3604
- // The matching items pair collapses to a `place-items` decl at the IR level; the minimizing
3605
- // reverse-emit picks the single utility covering both (`place-items-center`), replacing the
3606
- // `items-center`+`justify-items-center` pair. `bg-red-200` is preserved.
3607
- before: '<div className="items-center justify-items-center bg-red-200">box</div>',
3608
- after: '<div className="bg-red-200 place-items-center">box</div>'
3609
- }
3610
- ],
3611
- // Mismatched alignment (align-items != justify-items, no content pair) → nothing collapses.
3612
- noMatch: ['<div className="items-center justify-items-start bg-red-200">box</div>']
3613
- }
3614
- });
3615
-
3616
- // ../patterns/src/library/compress/scroll-margin-shorthand.pattern.ts
3617
- var SCROLL_MARGIN_SIDES = [
3618
- "scroll-margin-top",
3619
- "scroll-margin-right",
3620
- "scroll-margin-bottom",
3621
- "scroll-margin-left"
3622
- ];
3623
- var SIDE_SET = new Set(SCROLL_MARGIN_SIDES);
3624
- var BASE_KEY9 = conditionKey(BASE_CONDITION);
3625
- var SCROLL_MARGIN = "scroll-margin";
3626
- var NON_COLLAPSIBLE_VALUES3 = /* @__PURE__ */ new Set([
3627
- "initial",
3628
- "inherit",
3629
- "unset",
3630
- "revert",
3631
- "revert-layer"
3632
- ]);
3633
- function analyzeScrollMargin(sm) {
3634
- const block = sm.blocks.get(BASE_KEY9);
3635
- if (!block) return null;
3636
- const sides = [];
3637
- for (const side of SCROLL_MARGIN_SIDES) {
3638
- const decl = block.decls.get(side);
3639
- if (!decl) return null;
3640
- sides.push(decl);
3641
- }
3642
- const important = sides[0].important;
3643
- if (!sides.every((d) => d.important === important)) return null;
3644
- const value = String(sides[0].value);
3645
- if (NON_COLLAPSIBLE_VALUES3.has(value)) return null;
3646
- if (!sides.every((d) => String(d.value) === value)) return null;
3647
- const relative = sides.some((d) => d.relativeToParent);
3648
- return { value, important, relative };
3649
- }
3650
- function withFoldedScrollMargin(sm, fold) {
3651
- const blocks = /* @__PURE__ */ new Map();
3652
- for (const [key, block] of sm.blocks) {
3653
- if (key !== BASE_KEY9) {
3654
- blocks.set(key, block);
3655
- continue;
3656
- }
3657
- const decls = /* @__PURE__ */ new Map();
3658
- for (const [prop, decl] of block.decls) {
3659
- if (SIDE_SET.has(String(prop))) continue;
3660
- decls.set(prop, decl);
3661
- }
3662
- const shorthand = {
3663
- property: SCROLL_MARGIN,
3664
- value: fold.value,
3665
- important: fold.important,
3666
- relativeToParent: fold.relative,
3667
- inherited: false
3668
- // scroll-margin is never inherited
3669
- };
3670
- decls.set(shorthand.property, shorthand);
3671
- blocks.set(key, { condition: block.condition, decls });
3672
- }
3673
- return { blocks };
3674
- }
3675
- var scrollMarginShorthand = definePattern({
3676
- name: "scroll-margin-shorthand",
3677
- category: "compress/scroll-margin-shorthand",
3678
- safety: 1,
3679
- doc: {
3680
- title: "Collapse equal scroll-margin sides into scroll-margin",
3681
- summary: "An element whose four scroll-margin sides are all equal is rewritten to the single Tailwind scroll-m-* utility (scroll-margin === the four equal sides).",
3682
- before: '<div class="scroll-mt-4 scroll-mr-4 scroll-mb-4 scroll-ml-4"/>',
3683
- after: '<div class="scroll-m-4"/>',
3684
- safetyRationale: "`scroll-margin` is value-identical to four equal scroll-margin sides \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
3685
- },
3686
- rewrite: {
3687
- rewriteClasses(computed2) {
3688
- const fold = analyzeScrollMargin(computed2);
3689
- return fold ? withFoldedScrollMargin(computed2, fold) : null;
3690
- }
3691
- },
3692
- test: {
3693
- cases: [
3694
- {
3695
- // The four equal scroll-margin longhands collapse to a `scroll-margin` decl at the IR level; the
3696
- // minimizing reverse-emit then picks the single shortest utility (`scroll-m-4`) that reproduces
3697
- // it, replacing the four `scroll-m{t,r,b,l}-4` tokens. `bg-red-200` is preserved.
3698
- before: '<div className="scroll-mt-4 scroll-mr-4 scroll-mb-4 scroll-ml-4 bg-red-200">box</div>',
3699
- after: '<div className="bg-red-200 scroll-m-4">box</div>'
3700
- }
3701
- ],
3702
- // Sides differ (top != bottom) → no all-equal collapse.
3703
- noMatch: ['<div className="scroll-mt-2 scroll-mr-4 scroll-mb-8 scroll-ml-4 bg-red-200">box</div>']
3704
- }
3705
- });
3706
-
3707
- // ../patterns/src/library/compress/scroll-padding-shorthand.pattern.ts
3708
- var SCROLL_PADDING_SIDES = [
3709
- "scroll-padding-top",
3710
- "scroll-padding-right",
3711
- "scroll-padding-bottom",
3712
- "scroll-padding-left"
3713
- ];
3714
- var SIDE_SET2 = new Set(SCROLL_PADDING_SIDES);
3715
- var BASE_KEY10 = conditionKey(BASE_CONDITION);
3716
- var SCROLL_PADDING = "scroll-padding";
3717
- var NON_COLLAPSIBLE_VALUES4 = /* @__PURE__ */ new Set([
3718
- "initial",
3719
- "inherit",
3720
- "unset",
3721
- "revert",
3722
- "revert-layer"
3723
- ]);
3724
- function analyzeScrollPadding(sm) {
3725
- const block = sm.blocks.get(BASE_KEY10);
3726
- if (!block) return null;
3727
- const sides = [];
3728
- for (const side of SCROLL_PADDING_SIDES) {
3729
- const decl = block.decls.get(side);
3730
- if (!decl) return null;
3731
- sides.push(decl);
3732
- }
3733
- const important = sides[0].important;
3734
- if (!sides.every((d) => d.important === important)) return null;
3735
- const value = String(sides[0].value);
3736
- if (NON_COLLAPSIBLE_VALUES4.has(value)) return null;
3737
- if (!sides.every((d) => String(d.value) === value)) return null;
3738
- const relative = sides.some((d) => d.relativeToParent);
3739
- return { value, important, relative };
3740
- }
3741
- function withFoldedScrollPadding(sm, fold) {
3742
- const blocks = /* @__PURE__ */ new Map();
3743
- for (const [key, block] of sm.blocks) {
3744
- if (key !== BASE_KEY10) {
3745
- blocks.set(key, block);
3746
- continue;
3747
- }
3748
- const decls = /* @__PURE__ */ new Map();
3749
- for (const [prop, decl] of block.decls) {
3750
- if (SIDE_SET2.has(String(prop))) continue;
3751
- decls.set(prop, decl);
3752
- }
3753
- const shorthand = {
3754
- property: SCROLL_PADDING,
3755
- value: fold.value,
3756
- important: fold.important,
3757
- relativeToParent: fold.relative,
3758
- inherited: false
3759
- // scroll-padding is never inherited
3760
- };
3761
- decls.set(shorthand.property, shorthand);
3762
- blocks.set(key, { condition: block.condition, decls });
3763
- }
3764
- return { blocks };
3765
- }
3766
- var scrollPaddingShorthand = definePattern({
3767
- name: "scroll-padding-shorthand",
3768
- category: "compress/scroll-padding-shorthand",
3769
- safety: 1,
3770
- doc: {
3771
- title: "Collapse equal scroll-padding sides into scroll-padding",
3772
- summary: "An element whose four scroll-padding sides are all equal is rewritten to the single Tailwind scroll-p-* utility (scroll-padding === the four equal sides).",
3773
- before: '<div class="scroll-pt-4 scroll-pr-4 scroll-pb-4 scroll-pl-4"/>',
3774
- after: '<div class="scroll-p-4"/>',
3775
- safetyRationale: "`scroll-padding` is value-identical to four equal scroll-padding sides \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
2698
+ safetyRationale: "Wrapper paints nothing and establishes no layout/paint/var context, carries no ref/handlers/dynamic-children/html/spread/component identity, owns no targetable attrs, and is not a combinator/structural-pseudo subject (reparenting changes no match-set); inheritable styles are folded onto the child before removal."
3776
2699
  },
3777
- rewrite: {
3778
- rewriteClasses(computed2) {
3779
- const fold = analyzeScrollPadding(computed2);
3780
- return fold ? withFoldedScrollPadding(computed2, fold) : null;
3781
- }
2700
+ match: {
2701
+ tag: "div",
2702
+ onlyChild: "element",
2703
+ paintsNothing: true,
2704
+ where: [
2705
+ not(establishesContext),
2706
+ not(hasOwnAttrs2),
2707
+ not(hasDynamicClasses),
2708
+ not(hasSpreadAttrs2),
2709
+ not(isComponentNode3),
2710
+ not(targetedByStructuralPseudo3)
2711
+ ]
3782
2712
  },
2713
+ rewrite: { flattenInto: "child" },
3783
2714
  test: {
3784
2715
  cases: [
3785
2716
  {
3786
- // The four equal scroll-padding longhands collapse to a `scroll-padding` decl at the IR level;
3787
- // the minimizing reverse-emit then picks the single shortest utility (`scroll-p-4`) that
3788
- // reproduces it, replacing the four `scroll-p{t,r,b,l}-4` tokens. `bg-red-200` is preserved.
3789
- before: '<div className="scroll-pt-4 scroll-pr-4 scroll-pb-4 scroll-pl-4 bg-red-200">box</div>',
3790
- after: '<div className="bg-red-200 scroll-p-4">box</div>'
2717
+ // A plain, style-free wrapper paints nothing and establishes no context a provably-safe
2718
+ // flatten under the conservative gate: the wrapper is removed and its sole child hoisted.
2719
+ before: '<div><a className="bg-red-200">Link</a></div>',
2720
+ after: '<a className="bg-red-200">Link</a>'
3791
2721
  }
3792
2722
  ],
3793
- // Sides differ (top != bottom) → no all-equal collapse.
3794
- noMatch: ['<div className="scroll-pt-2 scroll-pr-4 scroll-pb-8 scroll-pl-4 bg-red-200">box</div>']
2723
+ noMatch: [
2724
+ // A ref pins the wrapper's element identity (a hard opacity barrier) → not a passthrough.
2725
+ '<div ref={rootRef}><a className="bg-red-200">Link</a></div>',
2726
+ // A `display:flex` wrapper establishes a formatting context, so removing its box is NOT
2727
+ // provably layout-neutral → the conservative gate leaves it in place.
2728
+ '<div className="flex"><a className="bg-red-200">Link</a></div>'
2729
+ ]
3795
2730
  }
3796
2731
  });
3797
2732
 
3798
- // ../patterns/src/library/compress/size-shorthand.pattern.ts
3799
- var WIDTH = "width";
3800
- var HEIGHT = "height";
3801
- var SIZE = "size";
3802
- var NON_COLLAPSIBLE_VALUES5 = /* @__PURE__ */ new Set(["auto", "initial", "unset"]);
3803
- function baseBlock(sm) {
3804
- return sm.blocks.get(conditionKey(BASE_CONDITION));
3805
- }
3806
- function withSizeShorthand(sm, value, important) {
3807
- const baseKey = conditionKey(BASE_CONDITION);
3808
- const blocks = /* @__PURE__ */ new Map();
3809
- for (const [key, block] of sm.blocks) {
3810
- if (key !== baseKey) {
3811
- blocks.set(key, block);
3812
- continue;
3813
- }
3814
- const decls = new Map(block.decls);
3815
- decls.delete(WIDTH);
3816
- decls.delete(HEIGHT);
3817
- for (const decl of normalizer.normalizeDeclaration(String(SIZE), value, important)) {
3818
- decls.set(decl.property, decl);
3819
- }
3820
- blocks.set(key, { condition: block.condition, decls });
3821
- }
3822
- return { blocks };
2733
+ // ../patterns/src/library/wrapper/redundant-inline-wrapper.pattern.ts
2734
+ function asEl3(node) {
2735
+ const n = node;
2736
+ return n.kind === "element" ? n : null;
2737
+ }
2738
+ function metaOf3(node) {
2739
+ return asEl3(node)?.meta ?? null;
3823
2740
  }
3824
- var sizeShorthand = definePattern({
3825
- name: "size-shorthand",
3826
- category: "compress/size-shorthand",
2741
+ var establishesContext2 = (node) => {
2742
+ const m = metaOf3(node);
2743
+ if (!m) return false;
2744
+ return m.establishesBox || m.establishesFormattingContext || m.establishesStackingContext || m.isContainingBlock || m.declaresCustomProperties;
2745
+ };
2746
+ var hasSpreadAttrs3 = (node) => metaOf3(node)?.hasSpreadAttrs ?? false;
2747
+ var isComponentNode4 = (node) => metaOf3(node)?.isComponent ?? false;
2748
+ var hasOwnAttrs3 = (node) => {
2749
+ const el = asEl3(node);
2750
+ if (!el) return false;
2751
+ return el.attrs.entries.size > 0 || el.attrs.spreads.length > 0;
2752
+ };
2753
+ var targetedByStructuralPseudo4 = (node, ctx) => {
2754
+ const el = asEl3(node);
2755
+ if (!el) return false;
2756
+ if (el.meta.targetedByStructuralPseudo) return true;
2757
+ return ctx.selectors.targetedByStructuralPseudo(el.id);
2758
+ };
2759
+ var DISPLAY3 = "display";
2760
+ var hasNonInlineDisplay = (node, ctx) => {
2761
+ const el = asEl3(node);
2762
+ if (!el) return false;
2763
+ const sm = ctx.computedOf(el) ?? el.computed;
2764
+ for (const block of sm.blocks.values()) {
2765
+ const decl = block.decls.get(DISPLAY3);
2766
+ if (decl && String(decl.value) !== "inline") return true;
2767
+ }
2768
+ return false;
2769
+ };
2770
+ var redundantInlineWrapper = definePattern({
2771
+ name: "redundant-inline-wrapper",
2772
+ category: "flatten/wrapper/redundant-inline-wrapper",
3827
2773
  safety: 2,
3828
2774
  doc: {
3829
- title: "Collapse equal width/height into size-*",
3830
- summary: "An element whose computed width and height are equal is rewritten to the single Tailwind size-* utility (size-* === width + height at the same value).",
3831
- before: '<div style="width:1rem;height:1rem"/>',
3832
- after: '<div class="size-4"/>',
3833
- safetyRationale: "`size-*` is value-identical to equal width+height \u2014 a class-only change. It is safe even on an element with a ref, event handler, dynamic child, or dangerouslySetInnerHTML \u2014 a className rewrite touches none of them; only a dynamic/opaque class list or a combinator-subject class is excluded, so no behaviour or project selector is disturbed."
2775
+ title: "Flatten redundant inline wrapper",
2776
+ summary: "An inline span with no own visual/box style, no attributes beyond an inert class, exactly one element child, and no opacity barriers is removed; its sole child is hoisted in its place.",
2777
+ before: "<span><Child/></span>",
2778
+ after: "<Child/>",
2779
+ safetyRationale: "An empty inline box paints nothing and establishes no layout/paint/var context; with the inline default display and a single element child its removal changes no paint and no flow. The span carries no ref/handlers/dynamic-children/html/spread/component identity, owns no targetable attrs, and is not a combinator/structural-pseudo subject; inheritable styles are folded onto the child before removal."
3834
2780
  },
3835
- rewrite: {
3836
- rewriteClasses(computed2) {
3837
- const base = baseBlock(computed2);
3838
- const w = base?.decls.get(WIDTH);
3839
- const h = base?.decls.get(HEIGHT);
3840
- if (!w || !h) return null;
3841
- if (w.important !== h.important) return null;
3842
- if (NON_COLLAPSIBLE_VALUES5.has(String(w.value))) return null;
3843
- if (w.value !== h.value) return null;
3844
- return withSizeShorthand(computed2, String(w.value), w.important);
3845
- }
2781
+ match: {
2782
+ tag: "span",
2783
+ onlyChild: "element",
2784
+ paintsNothing: true,
2785
+ where: [
2786
+ not(hasNonInlineDisplay),
2787
+ not(establishesContext2),
2788
+ not(hasOwnAttrs3),
2789
+ not(hasDynamicClasses),
2790
+ not(hasSpreadAttrs3),
2791
+ not(isComponentNode4),
2792
+ not(targetedByStructuralPseudo4)
2793
+ ]
3846
2794
  },
2795
+ rewrite: { flattenInto: "child" },
3847
2796
  test: {
3848
2797
  cases: [
3849
2798
  {
3850
- // Equal width/height collapse to a `size` decl at the IR level; the minimizing reverse-emit
3851
- // expands `size` back to width+height, finds the single utility covering both (`size-10`), and
3852
- // replaces the `h-10`+`w-10` pair with it. `bg-red-200` is preserved.
3853
- before: '<div className="h-10 w-10 bg-red-200">box</div>',
3854
- after: '<div className="bg-red-200 size-10">box</div>'
2799
+ // An empty inline span paints nothing and establishes no context a provably-safe flatten:
2800
+ // the span is removed and its sole child hoisted in place.
2801
+ before: '<span><a className="text-blue-500">Link</a></span>',
2802
+ after: '<a className="text-blue-500">Link</a>'
3855
2803
  }
3856
2804
  ],
3857
- // Width and height differ → no equal-axis collapse.
3858
- noMatch: ['<div className="h-10 w-20 bg-red-200">box</div>']
2805
+ noMatch: [
2806
+ // A ref pins the span's element identity (a hard opacity barrier) → not a passthrough.
2807
+ '<span ref={spanRef}><a className="text-blue-500">Link</a></span>',
2808
+ // The span paints its own background (own visual style) → kept.
2809
+ '<span className="bg-green-200"><a className="text-blue-500">Link</a></span>',
2810
+ // Non-inline display (inline-block) participates in layout differently → kept.
2811
+ '<span className="inline-block"><a className="text-blue-500">Link</a></span>'
2812
+ ]
3859
2813
  }
3860
2814
  });
3861
2815
 
3862
2816
  // ../patterns/src/_registry.generated.ts
3863
2817
  var builtinPatterns = [
2818
+ flexCenterWrapper,
2819
+ redundantFragment,
2820
+ gridCenterWrapper,
3864
2821
  displayContentsWrapper,
3865
2822
  emptyStyleDiv,
3866
- flexCenterWrapper,
3867
- inlineFlexCenterWrapper,
3868
- nestedFlexMerge,
3869
- nestedGridMerge,
2823
+ inheritedOnlyWrapper,
3870
2824
  passthroughWrapper,
3871
- redundantFragment,
3872
- redundantInlineWrapper,
3873
- borderRadiusShorthand,
3874
- borderShorthand,
3875
- dedupeClasses,
3876
- gapShorthand,
3877
- insetShorthand,
3878
- marginShorthand,
3879
- overflowShorthand,
3880
- overscrollBehaviorShorthand,
3881
- paddingShorthand,
3882
- placeShorthand,
3883
- scrollMarginShorthand,
3884
- scrollPaddingShorthand,
3885
- sizeShorthand
2825
+ redundantInlineWrapper
3886
2826
  ];
3887
2827
 
3888
2828
  // ../resolver-tailwind/src/tailwind/fingerprint.ts
@@ -3933,14 +2873,312 @@ function synthesizeResidual(remaining, ctx) {
3933
2873
 
3934
2874
  // ../resolver-tailwind/src/tailwind/engine.ts
3935
2875
  var import_node_module = require("module");
2876
+ var path3 = __toESM(require("path"), 1);
2877
+
2878
+ // ../resolver-tailwind/src/tailwind/engine-v4.ts
2879
+ var import_node_fs2 = require("fs");
2880
+ var path2 = __toESM(require("path"), 1);
2881
+
2882
+ // ../resolver-tailwind/src/tailwind/v4-bridge.ts
2883
+ var import_node_child_process = require("child_process");
2884
+ var import_node_fs = require("fs");
2885
+ var import_node_os = require("os");
3936
2886
  var path = __toESM(require("path"), 1);
2887
+ var CHILD_SOURCE = String.raw`
2888
+ import { createRequire } from 'node:module';
2889
+ import { pathToFileURL } from 'node:url';
2890
+ import * as fs from 'node:fs';
2891
+ import * as path from 'node:path';
2892
+
2893
+ function out(obj) { process.stdout.write(JSON.stringify(obj)); process.exit(0); }
2894
+
2895
+ let payload;
2896
+ try { payload = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); }
2897
+ catch { out({ ok: false }); }
2898
+
2899
+ const projectRoot = payload.projectRoot;
2900
+ const entries = payload.entries || [];
2901
+ const req = createRequire(path.join(projectRoot, '__domflax_tw4__.js'));
2902
+
2903
+ async function importFrom(id) {
2904
+ const resolved = req.resolve(id);
2905
+ return import(pathToFileURL(resolved).href);
2906
+ }
2907
+
2908
+ // Primary loader: @tailwindcss/node (the companion every v4 build tool installs). It resolves
2909
+ // '@import "tailwindcss"' and @theme against the project on disk.
2910
+ async function loadViaNode() {
2911
+ let mod;
2912
+ try { mod = await importFrom('@tailwindcss/node'); } catch { return null; }
2913
+ if (!mod || typeof mod.__unstable__loadDesignSystem !== 'function') return null;
2914
+ for (const e of entries) {
2915
+ try { return await mod.__unstable__loadDesignSystem(e.css, { base: e.base }); } catch {}
2916
+ }
2917
+ return null;
2918
+ }
2919
+
2920
+ // Secondary loader: bare 'tailwindcss' with a filesystem stylesheet resolver (best-effort).
2921
+ async function loadViaCore() {
2922
+ let tw;
2923
+ try { tw = await importFrom('tailwindcss'); } catch { return null; }
2924
+ if (!tw || typeof tw.__unstable__loadDesignSystem !== 'function') return null;
2925
+ const loadStylesheet = async (id, base) => {
2926
+ const r = createRequire(path.join(base, '__domflax_tw4__.js'));
2927
+ let p;
2928
+ const tries = id === 'tailwindcss' ? ['tailwindcss/index.css', 'tailwindcss'] : [id, id + '/index.css'];
2929
+ for (const t of tries) { try { p = r.resolve(t); break; } catch {} }
2930
+ if (!p) p = path.resolve(base, id);
2931
+ return { path: p, base: path.dirname(p), content: fs.readFileSync(p, 'utf8') };
2932
+ };
2933
+ const loadModule = async (id, base) => {
2934
+ const r = createRequire(path.join(base, '__domflax_tw4__.js'));
2935
+ const p = r.resolve(id);
2936
+ return { path: p, base: path.dirname(p), module: (await import(pathToFileURL(p).href)).default };
2937
+ };
2938
+ for (const e of entries) {
2939
+ try { return await tw.__unstable__loadDesignSystem(e.css, { base: e.base, loadStylesheet, loadModule }); } catch {}
2940
+ }
2941
+ return null;
2942
+ }
2943
+
2944
+ const ds = (await loadViaNode()) || (await loadViaCore());
2945
+ if (!ds) out({ ok: false });
2946
+
2947
+ let names = [];
2948
+ try {
2949
+ names = ds.getClassList().map((e) => (Array.isArray(e) ? e[0] : e)).filter((n) => typeof n === 'string');
2950
+ } catch { out({ ok: false }); }
2951
+
2952
+ let css = [];
2953
+ try { css = ds.candidatesToCss(names); } catch { out({ ok: false }); }
2954
+
2955
+ const result = [];
2956
+ for (let i = 0; i < names.length; i += 1) {
2957
+ const c = css[i];
2958
+ if (typeof c === 'string' && c.length > 0) result.push([names[i], c]);
2959
+ }
2960
+ out({ ok: true, entries: result });
2961
+ `;
2962
+ function runV4Bridge(payload) {
2963
+ let dir = null;
2964
+ try {
2965
+ dir = (0, import_node_fs.mkdtempSync)(path.join((0, import_node_os.tmpdir)(), "domflax-tw4-"));
2966
+ const scriptPath = path.join(dir, "bridge.mjs");
2967
+ const payloadPath = path.join(dir, "payload.json");
2968
+ (0, import_node_fs.writeFileSync)(scriptPath, CHILD_SOURCE, "utf8");
2969
+ (0, import_node_fs.writeFileSync)(payloadPath, JSON.stringify(payload), "utf8");
2970
+ const stdout = (0, import_node_child_process.execFileSync)(process.execPath, [scriptPath, payloadPath], {
2971
+ cwd: payload.projectRoot,
2972
+ encoding: "utf8",
2973
+ timeout: 9e4,
2974
+ maxBuffer: 256 * 1024 * 1024,
2975
+ stdio: ["ignore", "pipe", "ignore"]
2976
+ });
2977
+ const parsed = JSON.parse(stdout);
2978
+ if (!parsed.ok || !Array.isArray(parsed.entries) || parsed.entries.length === 0) return null;
2979
+ const entries = parsed.entries.filter(
2980
+ (e) => Array.isArray(e) && typeof e[0] === "string" && typeof e[1] === "string"
2981
+ );
2982
+ return entries.length > 0 ? { entries } : null;
2983
+ } catch {
2984
+ return null;
2985
+ } finally {
2986
+ if (dir) {
2987
+ try {
2988
+ (0, import_node_fs.rmSync)(dir, { recursive: true, force: true });
2989
+ } catch {
2990
+ }
2991
+ }
2992
+ }
2993
+ }
2994
+
2995
+ // ../resolver-tailwind/src/tailwind/v4-css.ts
2996
+ function stripComments(src) {
2997
+ return src.replace(/\/\*[\s\S]*?\*\//g, "");
2998
+ }
2999
+ function toDecl(buffer) {
3000
+ const buf = buffer.trim();
3001
+ if (buf.length === 0 || buf[0] === "@") return null;
3002
+ const colon = buf.indexOf(":");
3003
+ if (colon <= 0) return null;
3004
+ const prop = buf.slice(0, colon).trim();
3005
+ let value = buf.slice(colon + 1).trim();
3006
+ if (prop.length === 0 || value.length === 0) return null;
3007
+ let important = false;
3008
+ const bang = /!\s*important\s*$/i.exec(value);
3009
+ if (bang) {
3010
+ important = true;
3011
+ value = value.slice(0, bang.index).trim();
3012
+ }
3013
+ return { type: "decl", prop, value, important };
3014
+ }
3015
+ function splitAtRule(prelude) {
3016
+ const m = /^@([A-Za-z-]+)\s*([\s\S]*)$/.exec(prelude);
3017
+ if (!m) return { name: prelude.slice(1).trim(), params: "" };
3018
+ return { name: m[1].toLowerCase(), params: m[2].trim() };
3019
+ }
3020
+ function parseBlock(src, start) {
3021
+ const nodes = [];
3022
+ let buf = "";
3023
+ let i = start;
3024
+ while (i < src.length) {
3025
+ const c = src[i];
3026
+ if (c === "{") {
3027
+ const prelude = buf.trim();
3028
+ buf = "";
3029
+ const inner = parseBlock(src, i + 1);
3030
+ i = inner.next;
3031
+ if (prelude.startsWith("@")) {
3032
+ const { name, params } = splitAtRule(prelude);
3033
+ nodes.push({ type: "atrule", name, params, nodes: inner.nodes });
3034
+ } else if (prelude.length > 0) {
3035
+ nodes.push({ type: "rule", selector: prelude, nodes: inner.nodes });
3036
+ }
3037
+ } else if (c === "}") {
3038
+ const d = toDecl(buf);
3039
+ if (d) nodes.push(d);
3040
+ return { nodes, next: i + 1 };
3041
+ } else if (c === ";") {
3042
+ const d = toDecl(buf);
3043
+ if (d) nodes.push(d);
3044
+ buf = "";
3045
+ i += 1;
3046
+ } else {
3047
+ buf += c;
3048
+ i += 1;
3049
+ }
3050
+ }
3051
+ const tail = toDecl(buf);
3052
+ if (tail) nodes.push(tail);
3053
+ return { nodes, next: i };
3054
+ }
3055
+ function resolveNesting(child, parent) {
3056
+ const c = child.trim();
3057
+ if (parent.length === 0) return c;
3058
+ if (c.includes("&")) return c.split("&").join(parent);
3059
+ return `${parent} ${c}`;
3060
+ }
3061
+ var DROP_ATRULES = /* @__PURE__ */ new Set(["property", "keyframes", "font-face", "charset", "import"]);
3062
+ function flattenNodes(nodes, selector, at, out) {
3063
+ const own = [];
3064
+ for (const n of nodes) if (n.type === "decl") own.push(n);
3065
+ if (own.length > 0 && selector.length > 0) out.push({ selector, at: [...at], decls: own });
3066
+ for (const n of nodes) {
3067
+ if (n.type === "rule") {
3068
+ flattenNodes(n.nodes, resolveNesting(n.selector, selector), at, out);
3069
+ } else if (n.type === "atrule") {
3070
+ if (n.name === "media") {
3071
+ flattenNodes(n.nodes, selector, [...at, { name: "media", params: n.params }], out);
3072
+ } else if (n.name === "layer") {
3073
+ flattenNodes(n.nodes, selector, at, out);
3074
+ } else if (!DROP_ATRULES.has(n.name)) {
3075
+ flattenNodes(n.nodes, selector, [...at, { name: n.name, params: n.params }], out);
3076
+ }
3077
+ }
3078
+ }
3079
+ }
3080
+ function leafToNode(leaf) {
3081
+ const declNodes = leaf.decls.map((d) => ({
3082
+ type: "decl",
3083
+ prop: d.prop,
3084
+ value: d.value,
3085
+ important: d.important
3086
+ }));
3087
+ let node = { type: "rule", selector: leaf.selector, nodes: declNodes };
3088
+ for (let i = leaf.at.length - 1; i >= 0; i -= 1) {
3089
+ node = { type: "atrule", name: leaf.at[i].name, params: leaf.at[i].params, nodes: [node] };
3090
+ }
3091
+ return node;
3092
+ }
3093
+ function parseUtilityCss(css) {
3094
+ try {
3095
+ const { nodes } = parseBlock(stripComments(css), 0);
3096
+ const leaves = [];
3097
+ flattenNodes(nodes, "", [], leaves);
3098
+ return leaves.map(leafToNode);
3099
+ } catch {
3100
+ return [];
3101
+ }
3102
+ }
3103
+
3104
+ // ../resolver-tailwind/src/tailwind/engine-v4.ts
3105
+ var SEARCH_DIRS = ["", "src", "app", "styles", "src/styles", "src/app", "app/styles", "assets/css", "css"];
3106
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "out", ".next", "coverage"]);
3107
+ var ENTRY_RE = /@import\s+["']tailwindcss["']|@tailwind\b|@theme\b/;
3108
+ function scanDir(dir) {
3109
+ let names;
3110
+ try {
3111
+ names = (0, import_node_fs2.readdirSync)(dir);
3112
+ } catch {
3113
+ return null;
3114
+ }
3115
+ for (const name of names) {
3116
+ if (!name.toLowerCase().endsWith(".css")) continue;
3117
+ const file = path2.join(dir, name);
3118
+ try {
3119
+ if (!(0, import_node_fs2.statSync)(file).isFile()) continue;
3120
+ const css = (0, import_node_fs2.readFileSync)(file, "utf8");
3121
+ if (ENTRY_RE.test(css)) return { css, base: path2.dirname(file) };
3122
+ } catch {
3123
+ }
3124
+ }
3125
+ return null;
3126
+ }
3127
+ function findCssEntries(projectRoot) {
3128
+ const out = [];
3129
+ const seen = /* @__PURE__ */ new Set();
3130
+ for (const rel of SEARCH_DIRS) {
3131
+ const dir = path2.resolve(projectRoot, rel);
3132
+ if (seen.has(dir) || [...SKIP_DIRS].some((s) => dir.includes(`${path2.sep}${s}`))) continue;
3133
+ seen.add(dir);
3134
+ const hit = scanDir(dir);
3135
+ if (hit) {
3136
+ out.push(hit);
3137
+ break;
3138
+ }
3139
+ }
3140
+ out.push({ css: '@import "tailwindcss";', base: projectRoot });
3141
+ return out;
3142
+ }
3143
+ function makeV4Engine(entries, version) {
3144
+ const cssByClass = new Map(entries.map(([name, css]) => [name, css]));
3145
+ const nodeCache = /* @__PURE__ */ new Map();
3146
+ const nodesFor = (token) => {
3147
+ let cached = nodeCache.get(token);
3148
+ if (!cached) {
3149
+ const css = cssByClass.get(token);
3150
+ cached = css ? parseUtilityCss(css) : [];
3151
+ nodeCache.set(token, cached);
3152
+ }
3153
+ return cached;
3154
+ };
3155
+ return {
3156
+ version,
3157
+ context: {
3158
+ // The resolver keeps only string entries; we hand it the concrete class names directly.
3159
+ getClassList: () => [...cssByClass.keys()]
3160
+ },
3161
+ generate(candidates) {
3162
+ const out = [];
3163
+ for (const c of candidates) for (const n of nodesFor(c)) out.push(n);
3164
+ return out;
3165
+ }
3166
+ };
3167
+ }
3168
+ function loadV4Engine(projectRoot, version) {
3169
+ const snapshot = runV4Bridge({ projectRoot, entries: findCssEntries(projectRoot) });
3170
+ if (!snapshot) return null;
3171
+ return makeV4Engine(snapshot.entries, version);
3172
+ }
3173
+
3174
+ // ../resolver-tailwind/src/tailwind/engine.ts
3937
3175
  function moduleBase() {
3938
3176
  return typeof __filename === "string" ? __filename : importMetaUrl;
3939
3177
  }
3940
3178
  function projectRequire(projectRoot) {
3941
3179
  const bases = [];
3942
- if (projectRoot) bases.push(path.join(projectRoot, "__domflax__.js"));
3943
- bases.push(path.join(process.cwd(), "__domflax__.js"));
3180
+ if (projectRoot) bases.push(path3.join(projectRoot, "__domflax__.js"));
3181
+ bases.push(path3.join(process.cwd(), "__domflax__.js"));
3944
3182
  bases.push(moduleBase());
3945
3183
  for (const base of bases) {
3946
3184
  try {
@@ -3952,14 +3190,36 @@ function projectRequire(projectRoot) {
3952
3190
  }
3953
3191
  return null;
3954
3192
  }
3193
+ var FIRST_UNSUPPORTED_MAJOR = 4;
3194
+ function majorOf(version) {
3195
+ const m = /^\s*(\d+)/.exec(version);
3196
+ return m ? Number(m[1]) : null;
3197
+ }
3955
3198
  function loadEngine(options) {
3956
3199
  const req = projectRequire(options.projectRoot);
3957
- if (!req) return null;
3200
+ if (!req) return { engine: null, version: null, unsupportedMajor: null };
3201
+ let version = null;
3202
+ try {
3203
+ version = req("tailwindcss/package.json").version;
3204
+ } catch {
3205
+ return { engine: null, version: null, unsupportedMajor: null };
3206
+ }
3207
+ const major = majorOf(version);
3208
+ if (major !== null && major >= FIRST_UNSUPPORTED_MAJOR) {
3209
+ const projectRoot = options.projectRoot ?? process.cwd();
3210
+ let v4 = null;
3211
+ try {
3212
+ v4 = loadV4Engine(projectRoot, version);
3213
+ } catch {
3214
+ v4 = null;
3215
+ }
3216
+ if (v4) return { engine: v4, version, unsupportedMajor: null };
3217
+ return { engine: null, version, unsupportedMajor: major };
3218
+ }
3958
3219
  try {
3959
3220
  const resolveConfig = req("tailwindcss/resolveConfig.js");
3960
3221
  const { createContext } = req("tailwindcss/lib/lib/setupContextUtils.js");
3961
3222
  const { generateRules } = req("tailwindcss/lib/lib/generateRules.js");
3962
- const pkg = req("tailwindcss/package.json");
3963
3223
  let userConfig = options.config ?? { content: [{ raw: "" }] };
3964
3224
  if (options.configPath !== void 0) {
3965
3225
  const loadConfig = req("tailwindcss/loadConfig.js");
@@ -3968,15 +3228,19 @@ function loadEngine(options) {
3968
3228
  const resolved = resolveConfig(userConfig);
3969
3229
  const context = createContext(resolved);
3970
3230
  return {
3971
- version: pkg.version,
3972
- context,
3973
- generate(candidates) {
3974
- const rules = generateRules(new Set(candidates), context);
3975
- return rules.map(([, node]) => node);
3976
- }
3231
+ engine: {
3232
+ version,
3233
+ context,
3234
+ generate(candidates) {
3235
+ const rules = generateRules(new Set(candidates), context);
3236
+ return rules.map(([, node]) => node);
3237
+ }
3238
+ },
3239
+ version,
3240
+ unsupportedMajor: null
3977
3241
  };
3978
3242
  } catch {
3979
- return null;
3243
+ return { engine: null, version, unsupportedMajor: null };
3980
3244
  }
3981
3245
  }
3982
3246
 
@@ -4155,22 +3419,39 @@ var DROPPABLE_USAGE = {
4155
3419
  };
4156
3420
 
4157
3421
  // ../resolver-tailwind/src/tailwind/resolver.ts
3422
+ var warnedUnsupported = /* @__PURE__ */ new Set();
4158
3423
  var TailwindResolver = class {
4159
3424
  id = "tailwind";
4160
3425
  provider;
4161
3426
  fingerprint;
3427
+ /**
3428
+ * SAFETY (Layer 1): the detected Tailwind MAJOR when the project's version is one this resolver
3429
+ * cannot drive (v4+), else `null`. When set, {@link resolve} reports every token as unknown, so
3430
+ * downstream files are left unchanged (never mis-optimized). Exposed for diagnostics/tests.
3431
+ */
3432
+ unsupportedMajor;
4162
3433
  #engine;
4163
3434
  /** Per-token extraction cache (engine output is pure for a fixed config). */
4164
3435
  #tokenCache = /* @__PURE__ */ new Map();
4165
3436
  /** Per-class-set forward-resolution cache. */
4166
3437
  #resolveCache = /* @__PURE__ */ new Map();
4167
- /** Lazily built reverse index for {@link emit}. */
3438
+ /** Lazily built reverse index for the greedy {@link emit} fallback. */
4168
3439
  #reverseIndex = null;
3440
+ /** Lazily built cover vocabulary (base-condition tuple sets) for the exact-cover engine. */
3441
+ #coverVocab = null;
4169
3442
  constructor(config = {}) {
4170
- this.#engine = loadEngine(config);
4171
- this.provider = config.provider ?? (this.#engine ? `tailwindcss@${this.#engine.version}` : "tailwindcss");
3443
+ const loaded = loadEngine(config);
3444
+ this.#engine = loaded.engine;
3445
+ this.unsupportedMajor = loaded.unsupportedMajor;
3446
+ this.provider = config.provider ?? (loaded.version ? `tailwindcss@${loaded.version}` : "tailwindcss");
4172
3447
  const seed = JSON.stringify(config.config ?? {}) + (config.configPath ?? "");
4173
3448
  this.fingerprint = config.fingerprint ?? `${this.provider}/${fnv1a(seed)}`;
3449
+ if (this.unsupportedMajor !== null && !warnedUnsupported.has(this.provider)) {
3450
+ warnedUnsupported.add(this.provider);
3451
+ console.warn(
3452
+ `domflax: detected Tailwind v${this.unsupportedMajor} (${this.provider}) but could not load its design system (is @tailwindcss/node installed?); classes cannot be resolved, so files are left unchanged to avoid unsafe edits.`
3453
+ );
3454
+ }
4174
3455
  }
4175
3456
  /** Engine-backed, cached single-token extraction. */
4176
3457
  #extract(token) {
@@ -4279,9 +3560,75 @@ var TailwindResolver = class {
4279
3560
  this.#reverseIndex = index;
4280
3561
  return index;
4281
3562
  }
3563
+ /**
3564
+ * The cover vocabulary: every base-condition, plain-subject utility mapped to the {@link tupleKey}s
3565
+ * of its full normalized-longhand declaration set. Built once from a SINGLE engine `generate` over
3566
+ * the enumerable class list (grouped by selector), so it is the same cost as {@link #buildReverseIndex}.
3567
+ * This is what the provider-uniform exact-cover engine searches; the element's own droppable tokens
3568
+ * are members of it, guaranteeing feasibility. Variant / combinator / pseudo utilities are excluded
3569
+ * (their effect is not the element's own base box), so a target carrying such conditions simply finds
3570
+ * no cover and falls back to the greedy emit.
3571
+ */
3572
+ #buildCoverVocab() {
3573
+ if (this.#coverVocab) return this.#coverVocab;
3574
+ const baseCk = String(conditionKey(BASE_CONDITION));
3575
+ const out = [];
3576
+ if (this.#engine) {
3577
+ try {
3578
+ const classes = this.#engine.context.getClassList().filter((c) => typeof c === "string");
3579
+ const nodes = this.#engine.generate(classes);
3580
+ for (const node of nodes) {
3581
+ if (node.type !== "rule") continue;
3582
+ const rule = node;
3583
+ const parsed = parseSelector(rule.selector);
3584
+ if (parsed.kind !== "simple" || parsed.states.length > 0 || parsed.pseudoElement !== "") {
3585
+ continue;
3586
+ }
3587
+ const className = unescapeClass(rule.selector);
3588
+ if (className === null) continue;
3589
+ const tuples = [];
3590
+ const seen = /* @__PURE__ */ new Set();
3591
+ for (const child of rule.nodes ?? []) {
3592
+ if (child.type !== "decl") continue;
3593
+ const d = child;
3594
+ if (typeof d.value !== "string") continue;
3595
+ for (const decl of normalizer.normalizeDeclaration(d.prop, d.value, d.important === true)) {
3596
+ const k = tupleKey(baseCk, String(decl.property), String(decl.value), decl.important);
3597
+ if (!seen.has(k)) {
3598
+ seen.add(k);
3599
+ tuples.push(k);
3600
+ }
3601
+ }
3602
+ }
3603
+ if (tuples.length > 0) out.push({ token: className, tuples });
3604
+ }
3605
+ } catch {
3606
+ }
3607
+ }
3608
+ this.#coverVocab = out;
3609
+ return out;
3610
+ }
3611
+ /**
3612
+ * Try the minimal-string exact-cover engine over the WHOLE utility vocabulary. On success the chosen
3613
+ * set is verified by the mandatory CORRECTNESS BACKSTOP — re-resolve it and assert it reproduces the
3614
+ * target's tuples EXACTLY — before it is returned; any mismatch (or no cover / oversize universe)
3615
+ * yields `null` so {@link emit} uses its greedy fallback. Never returns a set that misrepresents `U`.
3616
+ */
3617
+ #tryCover(normalized, norm) {
3618
+ const universe = styleMapTuples(normalized, norm);
3619
+ if (universe.length === 0) return { classes: [], exact: true, warnings: [] };
3620
+ const chosen = minStringCover(universe, this.#buildCoverVocab());
3621
+ if (!chosen || chosen.length === 0) return null;
3622
+ const reTuples = new Set(styleMapTuples(this.resolve({ classes: chosen }).styles, norm));
3623
+ if (reTuples.size !== universe.length) return null;
3624
+ for (const t of universe) if (!reTuples.has(t)) return null;
3625
+ return { classes: chosen, exact: true, warnings: [] };
3626
+ }
4282
3627
  emit(styles, ctx) {
4283
3628
  const norm = ctx.normalizer ?? normalizer;
4284
3629
  const normalized = norm.normalizeStyleMap(styles);
3630
+ const cover = this.#tryCover(normalized, norm);
3631
+ if (cover) return cover;
4285
3632
  const base = normalized.blocks.get(conditionKey(BASE_CONDITION));
4286
3633
  if (!base || base.decls.size === 0) return { classes: [], exact: true, warnings: [] };
4287
3634
  const hasNonBase = normalized.blocks.size > 1;
@@ -4311,13 +3658,13 @@ var TailwindResolver = class {
4311
3658
  let bestCover = 0;
4312
3659
  for (const entry of candidates) {
4313
3660
  const [token, declMap] = entry;
4314
- let cover = 0;
4315
- for (const prop of declMap.keys()) if (remaining.has(prop)) cover += 1;
4316
- if (cover === 0) continue;
4317
- const better = best === null || cover > bestCover || cover === bestCover && declMap.size < best[1].size || cover === bestCover && declMap.size === best[1].size && token < best[0];
3661
+ let cover2 = 0;
3662
+ for (const prop of declMap.keys()) if (remaining.has(prop)) cover2 += 1;
3663
+ if (cover2 === 0) continue;
3664
+ const better = best === null || cover2 > bestCover || cover2 === bestCover && declMap.size < best[1].size || cover2 === bestCover && declMap.size === best[1].size && token < best[0];
4318
3665
  if (better) {
4319
3666
  best = entry;
4320
- bestCover = cover;
3667
+ bestCover = cover2;
4321
3668
  }
4322
3669
  }
4323
3670
  if (!best) break;
@@ -4388,14 +3735,14 @@ var LEGACY_PSEUDO_ELEMENTS2 = /* @__PURE__ */ new Set([
4388
3735
 
4389
3736
  // ../resolver-css/src/engine.ts
4390
3737
  var import_node_module2 = require("module");
4391
- var path2 = __toESM(require("path"), 1);
3738
+ var path4 = __toESM(require("path"), 1);
4392
3739
  function moduleBase2() {
4393
3740
  return typeof __filename === "string" ? __filename : importMetaUrl;
4394
3741
  }
4395
3742
  function loadPostcssEngine(projectRoot) {
4396
3743
  const bases = [];
4397
- if (projectRoot) bases.push(path2.join(projectRoot, "__domflax__.js"));
4398
- bases.push(path2.join(process.cwd(), "__domflax__.js"));
3744
+ if (projectRoot) bases.push(path4.join(projectRoot, "__domflax__.js"));
3745
+ bases.push(path4.join(process.cwd(), "__domflax__.js"));
4399
3746
  bases.push(moduleBase2());
4400
3747
  for (const base of bases) {
4401
3748
  try {
@@ -4448,15 +3795,15 @@ function collectDecls(rule) {
4448
3795
  }
4449
3796
 
4450
3797
  // ../resolver-css/src/misc-helpers.ts
4451
- var import_node_fs = require("fs");
3798
+ var import_node_fs3 = require("fs");
4452
3799
  function isPlainClassToken(token) {
4453
3800
  return token.length > 0 && !/[\s.#>+~:[\]()]/.test(token);
4454
3801
  }
4455
- function readCssPath(path3) {
3802
+ function readCssPath(path5) {
4456
3803
  try {
4457
- return { id: path3, css: (0, import_node_fs.readFileSync)(path3, "utf8") };
3804
+ return { id: path5, css: (0, import_node_fs3.readFileSync)(path5, "utf8") };
4458
3805
  } catch (cause) {
4459
- throw new Error(`resolver-css: cannot read CSS file "${path3}"`, { cause });
3806
+ throw new Error(`resolver-css: cannot read CSS file "${path5}"`, { cause });
4460
3807
  }
4461
3808
  }
4462
3809
  function deriveFingerprint(provider, files) {
@@ -4511,6 +3858,8 @@ var CustomCSSResolver = class {
4511
3858
  /** Distinct COMPLEX selectors (combinator or structural pseudo), sorted. */
4512
3859
  #complex;
4513
3860
  #reverse = null;
3861
+ /** Lazily built cover vocabulary (full condition-keyed tuple sets) for the exact-cover engine. */
3862
+ #coverVocab = null;
4514
3863
  constructor(cssFiles = [], options = {}) {
4515
3864
  ensurePostcss(options.projectRoot);
4516
3865
  const fromDisk = (options.files ?? []).map(readCssPath);
@@ -4543,8 +3892,23 @@ var CustomCSSResolver = class {
4543
3892
  }
4544
3893
  emit(styles, ctx) {
4545
3894
  const norm = ctx.normalizer ?? normalizer;
3895
+ const normalized = norm.normalizeStyleMap(styles);
3896
+ const universe = styleMapTuples(normalized, norm);
3897
+ if (universe.length === 0) return { classes: [], exact: true, warnings: [] };
3898
+ const chosen = minStringCover(universe, this.#buildCoverVocab());
3899
+ if (chosen && chosen.length > 0) {
3900
+ const reTuples = new Set(styleMapTuples(this.resolve({ classes: chosen }).styles, norm));
3901
+ let ok = reTuples.size === universe.length;
3902
+ if (ok) {
3903
+ for (const t of universe) if (!reTuples.has(t)) {
3904
+ ok = false;
3905
+ break;
3906
+ }
3907
+ }
3908
+ if (ok) return { classes: chosen, exact: true, warnings: [] };
3909
+ }
4546
3910
  const remaining = /* @__PURE__ */ new Map();
4547
- for (const [ck, block] of norm.normalizeStyleMap(styles).blocks) {
3911
+ for (const [ck, block] of normalized.blocks) {
4548
3912
  for (const [prop, decl] of block.decls) {
4549
3913
  remaining.set(`${ck} ${prop}`, String(decl.value));
4550
3914
  }
@@ -4754,7 +4118,23 @@ var CustomCSSResolver = class {
4754
4118
  if (rawBlocks.size === 0) return emptyStyleMap();
4755
4119
  return normalizer.normalizeStyleMap({ blocks: rawBlocks });
4756
4120
  }
4757
- /** Build (once) the reverse index used by {@link emit}. */
4121
+ /**
4122
+ * Build (once) the cover vocabulary for the exact-cover engine: every forward-resolvable class
4123
+ * mapped to the {@link styleMapTuples} of its full (condition-keyed, `!important`-aware) declaration
4124
+ * set. Unlike {@link #reverseIndex} this carries ALL style conditions and the important flag, so the
4125
+ * engine can pick a custom class covering hover/media declarations too.
4126
+ */
4127
+ #buildCoverVocab() {
4128
+ if (this.#coverVocab) return this.#coverVocab;
4129
+ const out = [];
4130
+ for (const token of this.#classIndex.keys()) {
4131
+ const tuples = styleMapTuples(this.#resolveTokens([token], [token]), normalizer);
4132
+ if (tuples.length > 0) out.push({ token, tuples });
4133
+ }
4134
+ this.#coverVocab = out;
4135
+ return out;
4136
+ }
4137
+ /** Build (once) the reverse index used by the greedy {@link emit} fallback. */
4758
4138
  #reverseIndex() {
4759
4139
  if (this.#reverse) return this.#reverse;
4760
4140
  const out = [];
@@ -4964,9 +4344,10 @@ function doParse(code, ctx) {
4964
4344
  native: document2
4965
4345
  };
4966
4346
  doc.sources.set(FILE_ID, sourceFile);
4967
- const resolveComputed = (tokens, tag, nodeId) => {
4347
+ const resolveComputed = (tokens, tag, nodeId, meta) => {
4968
4348
  if (tokens.length === 0) return emptyStyleMap();
4969
4349
  const res = ctx.resolver.resolve({ classes: tokens, element: { tagName: tag, namespace: "html" } });
4350
+ if (res.unknown.length > 0) meta.hasUnresolvedClasses = true;
4970
4351
  for (const w of res.warnings) {
4971
4352
  diagnostics.push({
4972
4353
  code: "DF_STYLE_CONFLICT_UNRESOLVED",
@@ -5056,7 +4437,7 @@ function doParse(code, ctx) {
5056
4437
  order.push(a.name);
5057
4438
  }
5058
4439
  const attrs = { entries, spreads: [], order };
5059
- const computed2 = resolveComputed(classTokens, tag, id);
4440
+ const computed2 = resolveComputed(classTokens, tag, id, meta);
5060
4441
  const children = [];
5061
4442
  if (!opaqueSubtree) {
5062
4443
  for (const c of node.childNodes ?? []) appendChild(c, id, children);
@@ -5459,6 +4840,7 @@ function doParse2(code, ctx) {
5459
4840
  element: { tagName: tag, namespace: component ? void 0 : "html" }
5460
4841
  });
5461
4842
  computed2 = ctx.normalizer.normalizeStyleMap(res.styles);
4843
+ if (res.unknown.length > 0) meta.hasUnresolvedClasses = true;
5462
4844
  for (const w of res.warnings) {
5463
4845
  diagnostics.push({
5464
4846
  code: "DF_STYLE_CONFLICT_UNRESOLVED",
@@ -5501,13 +4883,13 @@ function doParse2(code, ctx) {
5501
4883
  };
5502
4884
  const roots = [];
5503
4885
  traverse(ast, {
5504
- JSXElement(path3) {
5505
- roots.push(path3.node);
5506
- path3.skip();
4886
+ JSXElement(path5) {
4887
+ roots.push(path5.node);
4888
+ path5.skip();
5507
4889
  },
5508
- JSXFragment(path3) {
5509
- roots.push(path3.node);
5510
- path3.skip();
4890
+ JSXFragment(path5) {
4891
+ roots.push(path5.node);
4892
+ path5.skip();
5511
4893
  }
5512
4894
  });
5513
4895
  const rootFrag = doc.nodes.get(doc.root);
@@ -5781,6 +5163,27 @@ function createJsxBackend() {
5781
5163
  }
5782
5164
 
5783
5165
  // src/pipeline-run.ts
5166
+ function bytes(s) {
5167
+ return Buffer.byteLength(s, "utf8");
5168
+ }
5169
+ function countClassTokens(code) {
5170
+ let total = 0;
5171
+ const re = /\b(?:className|class)\s*=\s*"([^"]*)"/g;
5172
+ let m;
5173
+ while ((m = re.exec(code)) !== null) {
5174
+ total += m[1].split(/\s+/).filter((t) => t.length > 0).length;
5175
+ }
5176
+ return total;
5177
+ }
5178
+ function computeStats(code, out, nodesIn, nodesOut) {
5179
+ const classesBefore = countClassTokens(code);
5180
+ const classesAfter = countClassTokens(out);
5181
+ return {
5182
+ nodesRemoved: Math.max(0, nodesIn - nodesOut),
5183
+ classesSaved: Math.max(0, classesBefore - classesAfter),
5184
+ bytesSaved: bytes(code) - bytes(out)
5185
+ };
5186
+ }
5784
5187
  function jsxKindOf(id) {
5785
5188
  const clean = id.split("?", 1)[0] ?? id;
5786
5189
  if (clean.endsWith(".tsx")) return "tsx";
@@ -5856,8 +5259,10 @@ function finishPipeline(optimized, id, resolver) {
5856
5259
  }
5857
5260
  function runJsxPipeline(code, id, kind, resolver, patterns, safety) {
5858
5261
  const { doc, ctx, passes } = preparePipeline(code, id, kind, resolver, patterns, safety, "provably-safe");
5262
+ const nodesIn = doc.nodes.size;
5859
5263
  const { doc: optimized } = runPasses(doc, passes, ctx);
5860
- return finishPipeline(optimized, id, resolver);
5264
+ const out = finishPipeline(optimized, id, resolver);
5265
+ return { code: out, stats: computeStats(code, out, nodesIn, optimized.nodes.size) };
5861
5266
  }
5862
5267
  function prepareHtml(code, id, resolver, patterns, safety, gate) {
5863
5268
  const parsed = createHtmlFrontend().parse(code, {
@@ -5898,8 +5303,37 @@ function finishHtmlPipeline(optimized, id, resolver) {
5898
5303
  }
5899
5304
  function runHtmlPipeline(code, id, resolver, patterns, safety) {
5900
5305
  const { doc, ctx, passes } = prepareHtml(code, id, resolver, patterns, safety, "provably-safe");
5306
+ const nodesIn = doc.nodes.size;
5901
5307
  const { doc: optimized } = runPasses(doc, passes, ctx);
5902
- return finishHtmlPipeline(optimized, id, resolver);
5308
+ const out = finishHtmlPipeline(optimized, id, resolver);
5309
+ return { code: out, stats: computeStats(code, out, nodesIn, optimized.nodes.size) };
5310
+ }
5311
+
5312
+ // src/summary.ts
5313
+ function zeroStats() {
5314
+ return { nodesRemoved: 0, classesSaved: 0, bytesSaved: 0 };
5315
+ }
5316
+ function emptyTotals() {
5317
+ return { files: 0, nodesRemoved: 0, classesCompressed: 0, bytesSaved: 0 };
5318
+ }
5319
+ function addStats(t, s, changed) {
5320
+ if (!changed) return;
5321
+ t.files += 1;
5322
+ t.nodesRemoved += s.nodesRemoved;
5323
+ t.classesCompressed += s.classesSaved;
5324
+ t.bytesSaved += s.bytesSaved;
5325
+ }
5326
+ var RULE = ` ${"\u2500".repeat(32)}`;
5327
+ var TOTALS_KEY = /* @__PURE__ */ Symbol.for("domflax.buildTotals");
5328
+ function accumulateOnCompilation(compilation, stats, changed) {
5329
+ if (compilation === null || typeof compilation !== "object") return;
5330
+ const bag = compilation;
5331
+ let totals = bag[TOTALS_KEY];
5332
+ if (!totals) {
5333
+ totals = emptyTotals();
5334
+ bag[TOTALS_KEY] = totals;
5335
+ }
5336
+ addStats(totals, stats, changed);
5903
5337
  }
5904
5338
 
5905
5339
  // src/index.ts
@@ -5937,17 +5371,17 @@ function createDomflax(options = {}) {
5937
5371
  },
5938
5372
  patterns,
5939
5373
  transform(code, id) {
5940
- if (!isSupported(id, resolved.include)) return { code, map: null };
5374
+ if (!isSupported(id, resolved.include)) return { code, map: null, stats: zeroStats() };
5941
5375
  const kind = jsxKindOf(id);
5942
5376
  if (kind !== null) {
5943
5377
  const out = runJsxPipeline(code, id, kind, getResolver(), patterns, resolved.safety);
5944
- return { code: out, map: null };
5378
+ return { code: out.code, map: null, stats: out.stats };
5945
5379
  }
5946
5380
  if (htmlKindOf(id) !== null) {
5947
5381
  const out = runHtmlPipeline(code, id, getResolver(), patterns, resolved.safety);
5948
- return { code: out, map: null };
5382
+ return { code: out.code, map: null, stats: out.stats };
5949
5383
  }
5950
- return { code, map: null };
5384
+ return { code, map: null, stats: zeroStats() };
5951
5385
  }
5952
5386
  };
5953
5387
  }
@@ -5966,6 +5400,8 @@ function engineFor(options) {
5966
5400
  function domflaxLoader(source) {
5967
5401
  const options = this.getOptions?.() ?? {};
5968
5402
  const engine = engineFor(options);
5969
- return engine.transform(source, this.resourcePath).code;
5403
+ const out = engine.transform(source, this.resourcePath);
5404
+ accumulateOnCompilation(this._compilation, out.stats, out.code !== source);
5405
+ return out.code;
5970
5406
  }
5971
5407
  //# sourceMappingURL=webpack-loader.cjs.map