@luna_ui/luna 0.3.3 → 0.3.5

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 (174) hide show
  1. package/dist/cli.mjs +1264 -27
  2. package/dist/css/index.d.ts +194 -0
  3. package/dist/css/index.js +721 -0
  4. package/dist/css/runtime.d.ts +92 -0
  5. package/dist/css/runtime.js +179 -0
  6. package/dist/index.js +1 -1
  7. package/dist/jsx-dev-runtime.js +1 -1
  8. package/dist/jsx-runtime.d.ts +5 -0
  9. package/dist/jsx-runtime.js +1 -1
  10. package/dist/src-CHiGeWfy.js +1 -0
  11. package/dist/vite-plugin.d.ts +122 -0
  12. package/dist/vite-plugin.js +1518 -0
  13. package/package.json +16 -2
  14. package/src/css/extract.ts +798 -0
  15. package/src/css/index.ts +10 -0
  16. package/src/css/inject.ts +205 -0
  17. package/src/css/inline.ts +182 -0
  18. package/src/css/minify.ts +70 -0
  19. package/src/css/optimizer.ts +6 -0
  20. package/src/css/runtime.ts +344 -0
  21. package/src/css-optimizer/README.md +353 -0
  22. package/src/css-optimizer/cooccurrence.ts +100 -0
  23. package/src/css-optimizer/core.ts +263 -0
  24. package/src/css-optimizer/extractors.ts +243 -0
  25. package/src/css-optimizer/hash.ts +54 -0
  26. package/src/css-optimizer/index.ts +129 -0
  27. package/src/css-optimizer/merge.ts +109 -0
  28. package/src/css-optimizer/moonbit-analyzer.ts +210 -0
  29. package/src/css-optimizer/parser.ts +120 -0
  30. package/src/css-optimizer/pattern.ts +171 -0
  31. package/src/css-optimizer/transformers.ts +301 -0
  32. package/src/css-optimizer/types.ts +128 -0
  33. package/src/event-utils.ts +227 -0
  34. package/src/index.ts +890 -0
  35. package/src/jsx-dev-runtime.ts +2 -0
  36. package/src/jsx-runtime.ts +398 -0
  37. package/src/vite-plugin.ts +718 -0
  38. package/tests/__screenshots__/context.test.ts/Context-API-context-with-reactive-effects-context-value-accessible-in-effect-1.png +0 -0
  39. package/tests/__screenshots__/dom.test.ts/DOM-API-For-component--SolidJS-style--For-updates-when-signal-changes-1.png +0 -0
  40. package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-accepts-children-as-function-1.png +0 -0
  41. package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-toggles-visibility-1.png +0 -0
  42. package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-attribute-1.png +0 -0
  43. package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-style-1.png +0 -0
  44. package/tests/__screenshots__/dom.test.ts/DOM-API-createElementNs--SVG-support--createElementNs-with-dynamic-attribute-1.png +0 -0
  45. package/tests/__screenshots__/dom.test.ts/DOM-API-effect-with-DOM-effect-tracks-signal-changes-1.png +0 -0
  46. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-clear-to-empty-1.png +0 -0
  47. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-empty-array-1.png +0 -0
  48. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-removes-items-1.png +0 -0
  49. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-renders-initial-list-1.png +0 -0
  50. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-updates-when-items-change-1.png +0 -0
  51. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-empty-to-non-empty-transition-in-SVG-1.png +0 -0
  52. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-reordering-in-SVG-1.png +0 -0
  53. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-updates-SVG-elements-when-signal-changes-1.png +0 -0
  54. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-with-nested-SVG-groups-1.png +0 -0
  55. package/tests/__screenshots__/dom.test.ts/DOM-API-ref-callback--JSX-style--ref-callback-with-nested-elements-1.png +0 -0
  56. package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-creates-a-node-1.png +0 -0
  57. package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-with-false-condition-creates-placeholder-1.png +0 -0
  58. package/tests/__screenshots__/dom.test.ts/DOM-API-text-nodes-textDyn-creates-reactive-text-node-1.png +0 -0
  59. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-forEach-renders-correctly-without-show--initial-items--1.png +0 -0
  60. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-forEach-with-context-renders-correctly-without-show-1.png +0 -0
  61. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-nested-components-with-context--forEach--and-show-1.png +0 -0
  62. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-show-and-forEach-inherit-context-from-Owner--fixed--1.png +0 -0
  63. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-show-and-forEach-work-together--context-uses-default--1.png +0 -0
  64. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Context---ForEach-integration-forEach-items-can-access-context-1.png +0 -0
  65. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-renders-initial-list-1.png +0 -0
  66. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-updates-when-signal-changes-1.png +0 -0
  67. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-with-object-items-1.png +0 -0
  68. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-hides-when-condition-is-false-1.png +0 -0
  69. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-renders-when-condition-is-true-1.png +0 -0
  70. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-from-false-to-true-1.png +0 -0
  71. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-reactively-1.png +0 -0
  72. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--event-listener-pattern--Solid-js-docs-example--1.png +0 -0
  73. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--multiple-cleanups-in-component-body--LIFO-order--1.png +0 -0
  74. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-in-component-body-runs-on-unmount-1.png +0 -0
  75. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-works-with-For-loop-items--component-body-style--1.png +0 -0
  76. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--timer-cleanup-pattern--Solid-js-style--1.png +0 -0
  77. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Effects-effect-cleanup-runs-before-re-run-1.png +0 -0
  78. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-large-list-update-1.png +0 -0
  79. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-nested-batch-operations-1.png +0 -0
  80. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-rapid-sequential-updates-1.png +0 -0
  81. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-Show-component---visible-1.png +0 -0
  82. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-show-hide-element---visible-1.png +0 -0
  83. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-fragment-with-list-1.png +0 -0
  84. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-nested-fragments-1.png +0 -0
  85. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-simple-fragment-1.png +0 -0
  86. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-conditional-toggle-updates-1.png +0 -0
  87. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-addition-updates-match-1.png +0 -0
  88. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-removal-updates-match-1.png +0 -0
  89. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-text-updates-match-1.png +0 -0
  90. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-className-updates-1.png +0 -0
  91. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-style-updates-1.png +0 -0
  92. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-multiple-dynamic-attributes-1.png +0 -0
  93. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-deeply-nested-conditionals-1.png +0 -0
  94. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-empty-to-populated-1.png +0 -0
  95. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-populated-to-empty-1.png +0 -0
  96. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-cleanup-order-1.png +0 -0
  97. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-with-inner-signal-change-1.png +0 -0
  98. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-is-called-when-effect-re-runs-1.png +0 -0
  99. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-with-resource-simulation-1.png +0 -0
  100. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-multiple-children--no-wrapper--1.png +0 -0
  101. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-no-children-1.png +0 -0
  102. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-fragment-with-list-1.png +0 -0
  103. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-nested-Fragments-work-correctly-1.png +0 -0
  104. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-complex-reordering-with-additions-and-removals-1.png +0 -0
  105. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-insert-in-middle-1.png +0 -0
  106. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-remove-from-middle-1.png +0 -0
  107. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-reverse-list-order-1.png +0 -0
  108. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-shuffle-list-1.png +0 -0
  109. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-Show-component-renders-when-condition-is-true-1.png +0 -0
  110. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-renders-content-when-initially-true-1.png +0 -0
  111. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-toggles-visibility-dynamically-1.png +0 -0
  112. package/tests/__screenshots__/preact-signals-comparison.test.ts/Memo-Dependency-Chain-conditional-memo-dependencies-1.png +0 -0
  113. package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-basic-signal-get-set-produces-same-values-1.png +0 -0
  114. package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-batch-updates-produce-same-final-values-1.png +0 -0
  115. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-peek-reads-value-without-tracking-1.png +0 -0
  116. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-selective-tracking-with-untrack-1.png +0 -0
  117. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-untrack-prevents-dependency-tracking-1.png +0 -0
  118. package/tests/__screenshots__/resource.test.ts/Resource-API--SolidJS-style--reactivity-accessor-is-reactive-1.png +0 -0
  119. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-empty-string-for-non-failure-1.png +0 -0
  120. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-undefined-for-non-failure-1.png +0 -0
  121. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsFailure-and-stateError-1.png +0 -0
  122. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsPending-1.png +0 -0
  123. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsSuccess-and-stateValue-1.png +0 -0
  124. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateValue-returns-undefined-for-non-success-1.png +0 -0
  125. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-reject-transitions-to-failure-1.png +0 -0
  126. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-resolve-transitions-to-success-1.png +0 -0
  127. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-returns-resource--resolve--and-reject-functions-1.png +0 -0
  128. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-starts-in-pending-state-1.png +0 -0
  129. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-async-resolve-works-1.png +0 -0
  130. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-starts-in-pending-state-1.png +0 -0
  131. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-failure-on-reject-1.png +0 -0
  132. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-success-on-resolve-1.png +0 -0
  133. package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-can-wrap-fetch-like-async-operations-1.png +0 -0
  134. package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-works-with-setTimeout-simulation-1.png +0 -0
  135. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourceGet-tracks-dependencies-1.png +0 -0
  136. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourcePeek-does-not-track-dependencies-1.png +0 -0
  137. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceRefetch-refetch-resets-to-pending-and-re-runs-fetcher-1.png +0 -0
  138. package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-children-to-body-by-default-1.png +0 -0
  139. package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-to-selector-mount-target-1.png +0 -0
  140. package/tests/__screenshots__/solidjs-api.test.ts/SolidJS-API-compatibility-createEffect-tracks-dependencies-automatically-1.png +0 -0
  141. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-accessor-condition-in-Match-1.png +0 -0
  142. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-multiple-Match-components-1.png +0 -0
  143. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-single-Match-and-fallback-1.png +0 -0
  144. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-components-Switch-updates-DOM-when-signal-changes-1.png +0 -0
  145. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-multiple-dependencies-1.png +0 -0
  146. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-single-dependency-1.png +0 -0
  147. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-with-defer-option-skips-initial-run-1.png +0 -0
  148. package/tests/__screenshots__/store.test.ts/createStore-Arrays-array-updates-work-1.png +0 -0
  149. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-only-triggers-when-accessed-property-changes-1.png +0 -0
  150. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-parent-path-change-notifies-child-accessors-1.png +0 -0
  151. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-nested-property-access-1.png +0 -0
  152. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-property-access-in-effects-1.png +0 -0
  153. package/tests/context.test.ts +118 -0
  154. package/tests/css-optimizer-extractors.test.ts +264 -0
  155. package/tests/css-optimizer-integration.test.ts +566 -0
  156. package/tests/css-optimizer-transformers.test.ts +301 -0
  157. package/tests/css-optimizer.test.ts +646 -0
  158. package/tests/css-runtime.bench.ts +442 -0
  159. package/tests/css-runtime.test.ts +342 -0
  160. package/tests/dom.test.ts +872 -0
  161. package/tests/integration.test.ts +405 -0
  162. package/tests/issue-5-for-infinite-loop.test.ts +516 -0
  163. package/tests/jsx-runtime.test.tsx +393 -0
  164. package/tests/lifecycle.test.ts +833 -0
  165. package/tests/move-before.bench.ts +304 -0
  166. package/tests/preact-signals-comparison.test.ts +1608 -0
  167. package/tests/resource.test.ts +160 -0
  168. package/tests/router.test.ts +117 -0
  169. package/tests/show-initial-mount-leak.test.tsx +182 -0
  170. package/tests/solidjs-api.test.ts +659 -0
  171. package/tests/static-perf.bench.ts +64 -0
  172. package/tests/store.test.ts +263 -0
  173. package/tests/tsx-syntax.test.tsx +404 -0
  174. package/dist/src-DGWY0NYx.js +0 -1
@@ -0,0 +1,646 @@
1
+ /**
2
+ * Comprehensive tests for CSS Co-occurrence Optimizer
3
+ */
4
+
5
+ import { describe, test, expect } from "vitest";
6
+ import {
7
+ // Hash utilities
8
+ djb2Hash,
9
+ toBase36,
10
+ hashClassName,
11
+ hashMergedClassName,
12
+ // Parsing
13
+ extractClassUsages,
14
+ parseCssRules,
15
+ buildClassToDeclarationMap,
16
+ extractUniqueClasses,
17
+ // Co-occurrence
18
+ buildCooccurrenceMatrix,
19
+ matrixToCooccurrences,
20
+ getTopCooccurrences,
21
+ buildAdjacencyList,
22
+ // Pattern mining
23
+ findFrequentPatterns,
24
+ removeSubsumedPatterns,
25
+ groupByClassSet,
26
+ // Main API
27
+ optimizeCss,
28
+ optimizeHtml,
29
+ } from "../src/css-optimizer/index.js";
30
+
31
+ // =============================================================================
32
+ // Hash Tests
33
+ // =============================================================================
34
+
35
+ describe("Hash utilities", () => {
36
+ test("djb2Hash produces consistent hashes", () => {
37
+ expect(djb2Hash("display:flex")).toBe(djb2Hash("display:flex"));
38
+ expect(djb2Hash("display:flex")).not.toBe(djb2Hash("display:block"));
39
+ });
40
+
41
+ test("djb2Hash handles empty string", () => {
42
+ expect(djb2Hash("")).toBe(5381);
43
+ });
44
+
45
+ test("toBase36 converts correctly", () => {
46
+ expect(toBase36(0)).toBe("0");
47
+ expect(toBase36(35)).toBe("z");
48
+ expect(toBase36(36)).toBe("10");
49
+ expect(toBase36(100)).toBe("2s");
50
+ });
51
+
52
+ test("hashClassName generates prefixed class names", () => {
53
+ const cls = hashClassName("display:flex");
54
+ expect(cls).toMatch(/^_[a-z0-9]+$/);
55
+ expect(cls.length).toBeGreaterThan(1);
56
+ expect(cls.length).toBeLessThanOrEqual(8);
57
+ });
58
+
59
+ test("hashClassName with custom prefix", () => {
60
+ const cls = hashClassName("display:flex", "css-");
61
+ expect(cls).toMatch(/^css-[a-z0-9]+$/);
62
+ });
63
+
64
+ test("hashMergedClassName generates merged class names", () => {
65
+ const cls = hashMergedClassName(["display:flex", "color:red"]);
66
+ expect(cls).toMatch(/^_m[a-z0-9]+$/);
67
+ });
68
+
69
+ test("hashMergedClassName is order-independent", () => {
70
+ const cls1 = hashMergedClassName(["display:flex", "color:red"]);
71
+ const cls2 = hashMergedClassName(["color:red", "display:flex"]);
72
+ expect(cls1).toBe(cls2);
73
+ });
74
+ });
75
+
76
+ // =============================================================================
77
+ // Parser Tests
78
+ // =============================================================================
79
+
80
+ describe("Parser utilities", () => {
81
+ describe("extractClassUsages", () => {
82
+ test("extracts class combinations from HTML", () => {
83
+ const html = `
84
+ <div class="_a _b _c">Content</div>
85
+ <span class="_x _y">Other</span>
86
+ `;
87
+ const usages = extractClassUsages(html);
88
+
89
+ expect(usages).toHaveLength(2);
90
+ expect(usages[0].classes).toEqual(["_a", "_b", "_c"]);
91
+ expect(usages[1].classes).toEqual(["_x", "_y"]);
92
+ });
93
+
94
+ test("ignores single class elements", () => {
95
+ const html = `<div class="_single">Content</div>`;
96
+ const usages = extractClassUsages(html);
97
+ expect(usages).toHaveLength(0);
98
+ });
99
+
100
+ test("ignores non-prefixed classes", () => {
101
+ const html = `<div class="normal-class _luna another">Content</div>`;
102
+ const usages = extractClassUsages(html, "html", "_");
103
+ expect(usages).toHaveLength(0); // Only 1 _-prefixed class
104
+ });
105
+
106
+ test("sorts classes alphabetically", () => {
107
+ const html = `<div class="_z _a _m">Content</div>`;
108
+ const usages = extractClassUsages(html);
109
+ expect(usages[0].classes).toEqual(["_a", "_m", "_z"]);
110
+ });
111
+
112
+ test("uses custom prefix filter", () => {
113
+ const html = `<div class="tw-a tw-b regular">Content</div>`;
114
+ const usages = extractClassUsages(html, "html", "tw-");
115
+ expect(usages).toHaveLength(1);
116
+ expect(usages[0].classes).toEqual(["tw-a", "tw-b"]);
117
+ });
118
+
119
+ test("includes source location", () => {
120
+ const html = `<div class="_a _b">Content</div>`;
121
+ const usages = extractClassUsages(html, "test.html");
122
+ expect(usages[0].source).toContain("test.html");
123
+ });
124
+ });
125
+
126
+ describe("parseCssRules", () => {
127
+ test("parses simple CSS rules", () => {
128
+ const css = "._a{display:flex}._b{color:red}";
129
+ const rules = parseCssRules(css);
130
+
131
+ expect(rules).toHaveLength(2);
132
+ expect(rules[0]).toEqual({ selector: "_a", declarations: "display:flex" });
133
+ expect(rules[1]).toEqual({ selector: "_b", declarations: "color:red" });
134
+ });
135
+
136
+ test("handles multi-declaration rules", () => {
137
+ const css = "._a{display:flex;align-items:center;gap:10px}";
138
+ const rules = parseCssRules(css);
139
+
140
+ expect(rules).toHaveLength(1);
141
+ expect(rules[0].declarations).toBe("display:flex;align-items:center;gap:10px");
142
+ });
143
+
144
+ test("handles whitespace in CSS", () => {
145
+ const css = "._a { display: flex; }";
146
+ const rules = parseCssRules(css);
147
+
148
+ expect(rules).toHaveLength(1);
149
+ expect(rules[0].declarations.trim()).toContain("display");
150
+ });
151
+ });
152
+
153
+ describe("buildClassToDeclarationMap", () => {
154
+ test("builds correct mapping", () => {
155
+ const css = "._a{display:flex}._b{color:red}";
156
+ const map = buildClassToDeclarationMap(css);
157
+
158
+ expect(map.get("_a")).toBe("display:flex");
159
+ expect(map.get("_b")).toBe("color:red");
160
+ });
161
+
162
+ test("filters by prefix", () => {
163
+ const css = "._a{display:flex}.normal{color:red}";
164
+ const map = buildClassToDeclarationMap(css, "_");
165
+
166
+ expect(map.has("_a")).toBe(true);
167
+ expect(map.has("normal")).toBe(false);
168
+ });
169
+ });
170
+
171
+ describe("extractUniqueClasses", () => {
172
+ test("extracts all unique classes", () => {
173
+ const html = `
174
+ <div class="_a _b _c">Content</div>
175
+ <span class="_b _d">Other</span>
176
+ `;
177
+ const classes = extractUniqueClasses(html);
178
+
179
+ expect(classes.size).toBe(4);
180
+ expect(classes.has("_a")).toBe(true);
181
+ expect(classes.has("_b")).toBe(true);
182
+ expect(classes.has("_c")).toBe(true);
183
+ expect(classes.has("_d")).toBe(true);
184
+ });
185
+ });
186
+ });
187
+
188
+ // =============================================================================
189
+ // Co-occurrence Tests
190
+ // =============================================================================
191
+
192
+ describe("Co-occurrence analysis", () => {
193
+ describe("buildCooccurrenceMatrix", () => {
194
+ test("counts pair co-occurrences", () => {
195
+ const usages = [
196
+ { classes: ["_a", "_b", "_c"], source: "test1" },
197
+ { classes: ["_a", "_b"], source: "test2" },
198
+ { classes: ["_a", "_c"], source: "test3" },
199
+ ];
200
+
201
+ const matrix = buildCooccurrenceMatrix(usages);
202
+
203
+ // _a and _b appear together 2 times
204
+ expect(matrix.get("_a")?.get("_b")).toBe(2);
205
+ // _a and _c appear together 2 times
206
+ expect(matrix.get("_a")?.get("_c")).toBe(2);
207
+ // _b and _c appear together 1 time
208
+ expect(matrix.get("_b")?.get("_c")).toBe(1);
209
+ });
210
+
211
+ test("maintains alphabetical order", () => {
212
+ const usages = [{ classes: ["_z", "_a"], source: "test" }];
213
+ const matrix = buildCooccurrenceMatrix(usages);
214
+
215
+ // Should be stored as _a -> _z, not _z -> _a
216
+ expect(matrix.has("_a")).toBe(true);
217
+ expect(matrix.get("_a")?.has("_z")).toBe(true);
218
+ expect(matrix.has("_z")).toBe(false);
219
+ });
220
+
221
+ test("handles empty input", () => {
222
+ const matrix = buildCooccurrenceMatrix([]);
223
+ expect(matrix.size).toBe(0);
224
+ });
225
+ });
226
+
227
+ describe("matrixToCooccurrences", () => {
228
+ test("converts matrix to sorted array", () => {
229
+ const usages = [
230
+ { classes: ["_a", "_b"], source: "1" },
231
+ { classes: ["_a", "_b"], source: "2" },
232
+ { classes: ["_x", "_y"], source: "3" },
233
+ ];
234
+ const matrix = buildCooccurrenceMatrix(usages);
235
+ const cooccurrences = matrixToCooccurrences(matrix);
236
+
237
+ expect(cooccurrences[0].frequency).toBeGreaterThanOrEqual(cooccurrences[1]?.frequency || 0);
238
+ expect(cooccurrences.find((c) => c.classA === "_a" && c.classB === "_b")?.frequency).toBe(2);
239
+ });
240
+ });
241
+
242
+ describe("getTopCooccurrences", () => {
243
+ test("returns top N pairs", () => {
244
+ const cooccurrences = [
245
+ { classA: "_a", classB: "_b", frequency: 10 },
246
+ { classA: "_c", classB: "_d", frequency: 5 },
247
+ { classA: "_e", classB: "_f", frequency: 3 },
248
+ ];
249
+
250
+ const top2 = getTopCooccurrences(cooccurrences, 2);
251
+ expect(top2).toHaveLength(2);
252
+ expect(top2[0].frequency).toBe(10);
253
+ expect(top2[1].frequency).toBe(5);
254
+ });
255
+ });
256
+
257
+ describe("buildAdjacencyList", () => {
258
+ test("builds bidirectional adjacency list", () => {
259
+ const cooccurrences = [{ classA: "_a", classB: "_b", frequency: 5 }];
260
+ const adj = buildAdjacencyList(cooccurrences, 1);
261
+
262
+ expect(adj.get("_a")?.find((e) => e.target === "_b")).toBeDefined();
263
+ expect(adj.get("_b")?.find((e) => e.target === "_a")).toBeDefined();
264
+ });
265
+
266
+ test("respects minFrequency filter", () => {
267
+ const cooccurrences = [
268
+ { classA: "_a", classB: "_b", frequency: 5 },
269
+ { classA: "_c", classB: "_d", frequency: 1 },
270
+ ];
271
+ const adj = buildAdjacencyList(cooccurrences, 3);
272
+
273
+ expect(adj.has("_a")).toBe(true);
274
+ expect(adj.has("_c")).toBe(false);
275
+ });
276
+ });
277
+ });
278
+
279
+ // =============================================================================
280
+ // Pattern Mining Tests
281
+ // =============================================================================
282
+
283
+ describe("Pattern mining", () => {
284
+ describe("findFrequentPatterns", () => {
285
+ test("finds pair patterns", () => {
286
+ const usages = [
287
+ { classes: ["_a", "_b"], source: "1" },
288
+ { classes: ["_a", "_b"], source: "2" },
289
+ { classes: ["_c", "_d"], source: "3" },
290
+ ];
291
+
292
+ const patterns = findFrequentPatterns(usages, 2, 2);
293
+
294
+ expect(patterns.length).toBeGreaterThanOrEqual(1);
295
+ const abPattern = patterns.find(
296
+ (p) => p.originalClasses.includes("_a") && p.originalClasses.includes("_b")
297
+ );
298
+ expect(abPattern).toBeDefined();
299
+ expect(abPattern?.frequency).toBe(2);
300
+ });
301
+
302
+ test("finds triple patterns", () => {
303
+ const usages = [
304
+ { classes: ["_a", "_b", "_c"], source: "1" },
305
+ { classes: ["_a", "_b", "_c"], source: "2" },
306
+ { classes: ["_a", "_b", "_c"], source: "3" },
307
+ ];
308
+
309
+ const patterns = findFrequentPatterns(usages, 3, 3);
310
+
311
+ const triplePattern = patterns.find((p) => p.originalClasses.length === 3);
312
+ expect(triplePattern).toBeDefined();
313
+ expect(triplePattern?.frequency).toBe(3);
314
+ });
315
+
316
+ test("respects minFrequency", () => {
317
+ const usages = [
318
+ { classes: ["_a", "_b"], source: "1" },
319
+ { classes: ["_c", "_d"], source: "2" },
320
+ ];
321
+
322
+ const patterns = findFrequentPatterns(usages, 2, 2);
323
+ expect(patterns.length).toBe(0); // Each pair appears only once
324
+ });
325
+
326
+ test("respects maxPatternSize", () => {
327
+ const usages = [
328
+ { classes: ["_a", "_b", "_c", "_d"], source: "1" },
329
+ { classes: ["_a", "_b", "_c", "_d"], source: "2" },
330
+ ];
331
+
332
+ const patterns2 = findFrequentPatterns(usages, 2, 2);
333
+ const patterns4 = findFrequentPatterns(usages, 2, 4);
334
+
335
+ expect(patterns2.every((p) => p.originalClasses.length <= 2)).toBe(true);
336
+ expect(patterns4.some((p) => p.originalClasses.length > 2)).toBe(true);
337
+ });
338
+
339
+ test("calculates bytesSaved estimate", () => {
340
+ const usages = [
341
+ { classes: ["_a", "_b", "_c"], source: "1" },
342
+ { classes: ["_a", "_b", "_c"], source: "2" },
343
+ ];
344
+
345
+ const patterns = findFrequentPatterns(usages, 2, 3);
346
+ const pattern = patterns.find((p) => p.originalClasses.length === 3);
347
+
348
+ expect(pattern?.bytesSaved).toBeGreaterThan(0);
349
+ });
350
+ });
351
+
352
+ describe("removeSubsumedPatterns", () => {
353
+ test("removes subset patterns", () => {
354
+ const patterns = [
355
+ { originalClasses: ["_a", "_b"], frequency: 5, mergedClass: "", declarations: [], bytesSaved: 100 },
356
+ { originalClasses: ["_a", "_b", "_c"], frequency: 5, mergedClass: "", declarations: [], bytesSaved: 200 },
357
+ ];
358
+
359
+ const result = removeSubsumedPatterns(patterns);
360
+
361
+ // Should keep only the larger pattern
362
+ expect(result.length).toBe(1);
363
+ expect(result[0].originalClasses).toEqual(["_a", "_b", "_c"]);
364
+ });
365
+
366
+ test("keeps patterns with significantly different frequencies", () => {
367
+ const patterns = [
368
+ { originalClasses: ["_a", "_b"], frequency: 10, mergedClass: "", declarations: [], bytesSaved: 100 },
369
+ { originalClasses: ["_a", "_b", "_c"], frequency: 2, mergedClass: "", declarations: [], bytesSaved: 50 },
370
+ ];
371
+
372
+ const result = removeSubsumedPatterns(patterns);
373
+
374
+ // Should keep both because frequency difference is significant
375
+ expect(result.length).toBe(2);
376
+ });
377
+ });
378
+
379
+ describe("groupByClassSet", () => {
380
+ test("groups usages by exact class set", () => {
381
+ const usages = [
382
+ { classes: ["_a", "_b"], source: "1" },
383
+ { classes: ["_a", "_b"], source: "2" },
384
+ { classes: ["_c", "_d"], source: "3" },
385
+ ];
386
+
387
+ const groups = groupByClassSet(usages);
388
+
389
+ expect(groups.size).toBe(2);
390
+ expect(groups.get("_a|_b")?.length).toBe(2);
391
+ expect(groups.get("_c|_d")?.length).toBe(1);
392
+ });
393
+ });
394
+ });
395
+
396
+ // =============================================================================
397
+ // Main API Tests
398
+ // =============================================================================
399
+
400
+ describe("Main API", () => {
401
+ describe("optimizeCss", () => {
402
+ test("merges frequently co-occurring classes", () => {
403
+ const css = "._a{display:flex}._b{align-items:center}._c{color:red}";
404
+ const html = `
405
+ <div class="_a _b">Content</div>
406
+ <div class="_a _b">More</div>
407
+ <span class="_c">Other</span>
408
+ `;
409
+ const mapping = {
410
+ "display:flex": "_a",
411
+ "align-items:center": "_b",
412
+ "color:red": "_c",
413
+ };
414
+
415
+ const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
416
+
417
+ expect(result.patterns.length).toBeGreaterThanOrEqual(1);
418
+ expect(result.mergeMap.size).toBeGreaterThanOrEqual(1);
419
+ expect(result.stats.mergedPatterns).toBeGreaterThanOrEqual(1);
420
+ });
421
+
422
+ test("generates valid merged CSS", () => {
423
+ const css = "._a{display:flex}._b{align-items:center}";
424
+ const html = `
425
+ <div class="_a _b">1</div>
426
+ <div class="_a _b">2</div>
427
+ `;
428
+ const mapping = {
429
+ "display:flex": "_a",
430
+ "align-items:center": "_b",
431
+ };
432
+
433
+ const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
434
+
435
+ // Check that merged class rule is generated
436
+ if (result.patterns.length > 0) {
437
+ const mergedClass = result.patterns[0].mergedClass;
438
+ expect(result.css).toContain(`.${mergedClass}{`);
439
+ expect(result.css).toContain("display:flex");
440
+ expect(result.css).toContain("align-items:center");
441
+ }
442
+ });
443
+
444
+ test("removes merged classes from original CSS", () => {
445
+ const css = "._a{display:flex}._b{align-items:center}._c{color:red}";
446
+ const html = `
447
+ <div class="_a _b">1</div>
448
+ <div class="_a _b">2</div>
449
+ `;
450
+ const mapping = {
451
+ "display:flex": "_a",
452
+ "align-items:center": "_b",
453
+ "color:red": "_c",
454
+ };
455
+
456
+ const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
457
+
458
+ if (result.patterns.length > 0) {
459
+ // Merged classes should be removed
460
+ expect(result.css).not.toMatch(/\._a\{display:flex\}/);
461
+ expect(result.css).not.toMatch(/\._b\{align-items:center\}/);
462
+ // Unmerged class should remain
463
+ expect(result.css).toContain("._c{color:red}");
464
+ }
465
+ });
466
+
467
+ test("handles no patterns found", () => {
468
+ const css = "._a{display:flex}._b{color:red}";
469
+ const html = `
470
+ <div class="_a">Single</div>
471
+ <span class="_b">Other</span>
472
+ `;
473
+ const mapping = {
474
+ "display:flex": "_a",
475
+ "color:red": "_b",
476
+ };
477
+
478
+ const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
479
+
480
+ expect(result.patterns).toHaveLength(0);
481
+ expect(result.css).toBe(css); // Unchanged
482
+ });
483
+
484
+ test("preserves pseudo-class rules", () => {
485
+ const css = "._a{color:blue}._a:hover{color:red}";
486
+ const html = `<div class="_a _b">1</div><div class="_a _b">2</div>`;
487
+ const mapping = {
488
+ "color:blue": "_a",
489
+ "display:flex": "_b",
490
+ };
491
+
492
+ const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
493
+
494
+ expect(result.css).toContain(":hover");
495
+ });
496
+
497
+ test("respects pretty option", () => {
498
+ const css = "._a{display:flex}._b{color:red}";
499
+ const html = `<div class="_a _b">1</div><div class="_a _b">2</div>`;
500
+ const mapping = {
501
+ "display:flex": "_a",
502
+ "color:red": "_b",
503
+ };
504
+
505
+ const result = optimizeCss(css, html, mapping, { minFrequency: 2, pretty: true });
506
+
507
+ if (result.patterns.length > 0) {
508
+ expect(result.css).toContain(" { ");
509
+ expect(result.css).toContain(" }");
510
+ }
511
+ });
512
+ });
513
+
514
+ describe("optimizeHtml", () => {
515
+ test("replaces class combinations with merged class", () => {
516
+ const html = `<div class="_a _b _c">Content</div>`;
517
+ const mergeMap = new Map([["_a _b", "_merged"]]);
518
+
519
+ const result = optimizeHtml(html, mergeMap);
520
+
521
+ expect(result).toContain("_merged");
522
+ expect(result).toContain("_c"); // Remaining class
523
+ expect(result).not.toContain('"_a _b _c"');
524
+ });
525
+
526
+ test("handles multiple elements", () => {
527
+ const html = `
528
+ <div class="_a _b">First</div>
529
+ <span class="_a _b">Second</span>
530
+ <p class="_c _d">Third</p>
531
+ `;
532
+ const mergeMap = new Map([["_a _b", "_merged"]]);
533
+
534
+ const result = optimizeHtml(html, mergeMap);
535
+
536
+ expect((result.match(/_merged/g) || []).length).toBe(2);
537
+ expect(result).toContain("_c _d"); // Unchanged
538
+ });
539
+
540
+ test("handles non-prefixed classes", () => {
541
+ const html = `<div class="_a _b regular-class">Content</div>`;
542
+ const mergeMap = new Map([["_a _b", "_merged"]]);
543
+
544
+ const result = optimizeHtml(html, mergeMap);
545
+
546
+ expect(result).toContain("_merged");
547
+ expect(result).toContain("regular-class");
548
+ });
549
+
550
+ test("applies longer patterns first", () => {
551
+ const html = `<div class="_a _b _c">Content</div>`;
552
+ const mergeMap = new Map([
553
+ ["_a _b", "_m1"],
554
+ ["_a _b _c", "_m2"],
555
+ ]);
556
+
557
+ const result = optimizeHtml(html, mergeMap);
558
+
559
+ expect(result).toContain("_m2");
560
+ expect(result).not.toContain("_m1");
561
+ });
562
+
563
+ test("returns unchanged HTML when no patterns match", () => {
564
+ const html = `<div class="_x _y">Content</div>`;
565
+ const mergeMap = new Map([["_a _b", "_merged"]]);
566
+
567
+ const result = optimizeHtml(html, mergeMap);
568
+
569
+ expect(result).toBe(html);
570
+ });
571
+
572
+ test("returns unchanged HTML with empty mergeMap", () => {
573
+ const html = `<div class="_a _b">Content</div>`;
574
+ const mergeMap = new Map<string, string>();
575
+
576
+ const result = optimizeHtml(html, mergeMap);
577
+
578
+ expect(result).toBe(html);
579
+ });
580
+ });
581
+ });
582
+
583
+ // =============================================================================
584
+ // Edge Cases and Integration Tests
585
+ // =============================================================================
586
+
587
+ describe("Edge cases", () => {
588
+ test("handles empty HTML", () => {
589
+ const result = optimizeCss("._a{display:flex}", "", { "display:flex": "_a" });
590
+ expect(result.patterns).toHaveLength(0);
591
+ });
592
+
593
+ test("handles empty CSS", () => {
594
+ const result = optimizeCss("", '<div class="_a _b">Test</div>', {});
595
+ expect(result.css).toBe("");
596
+ });
597
+
598
+ test("handles special characters in class names", () => {
599
+ const html = `<div class="_a-1 _b_2">Content</div>`;
600
+ const usages = extractClassUsages(html);
601
+ expect(usages[0].classes).toContain("_a-1");
602
+ expect(usages[0].classes).toContain("_b_2");
603
+ });
604
+
605
+ test("handles very long class lists", () => {
606
+ const classes = Array.from({ length: 20 }, (_, i) => `_c${i}`);
607
+ const html = `<div class="${classes.join(" ")}">Content</div>`;
608
+ const usages = extractClassUsages(html);
609
+
610
+ expect(usages[0].classes.length).toBe(20);
611
+ });
612
+
613
+ test("handles duplicate classes in single element", () => {
614
+ const html = `<div class="_a _b _a">Content</div>`;
615
+ const usages = extractClassUsages(html);
616
+
617
+ // Should contain 3 total classes but sorted
618
+ expect(usages[0].classes).toContain("_a");
619
+ expect(usages[0].classes).toContain("_b");
620
+ });
621
+
622
+ test("conflicting patterns are handled correctly", () => {
623
+ // When _a, _b, _c all appear together multiple times
624
+ // AND _a, _b appear together multiple times
625
+ // Only one should be used (no overlap)
626
+ const html = `
627
+ <div class="_a _b _c">1</div>
628
+ <div class="_a _b _c">2</div>
629
+ <div class="_a _b">3</div>
630
+ <div class="_a _b">4</div>
631
+ `;
632
+ const css = "._a{a:1}._b{b:2}._c{c:3}";
633
+ const mapping = { "a:1": "_a", "b:2": "_b", "c:3": "_c" };
634
+
635
+ const result = optimizeCss(css, html, mapping, { minFrequency: 2, maxPatternSize: 3 });
636
+
637
+ // Each class should only be used in at most one pattern
638
+ const usedClasses = new Set<string>();
639
+ for (const pattern of result.patterns) {
640
+ for (const cls of pattern.originalClasses) {
641
+ expect(usedClasses.has(cls)).toBe(false);
642
+ usedClasses.add(cls);
643
+ }
644
+ }
645
+ });
646
+ });