@luna_ui/luna 0.3.4 → 0.4.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 (182) 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-CyEkcO3_.d.ts → index-CDWzWF-h.d.ts} +2 -2
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js +1 -1
  9. package/dist/jsx-dev-runtime.js +1 -1
  10. package/dist/jsx-runtime.d.ts +1 -1
  11. package/dist/jsx-runtime.js +1 -1
  12. package/dist/src-DEjrAhrg.js +1 -0
  13. package/dist/vite-plugin.d.ts +122 -0
  14. package/dist/vite-plugin.js +1518 -0
  15. package/package.json +16 -2
  16. package/src/css/extract.ts +798 -0
  17. package/src/css/index.ts +10 -0
  18. package/src/css/inject.ts +205 -0
  19. package/src/css/inline.ts +182 -0
  20. package/src/css/minify.ts +70 -0
  21. package/src/css/optimizer.ts +6 -0
  22. package/src/css/runtime.ts +344 -0
  23. package/src/css-optimizer/README.md +353 -0
  24. package/src/css-optimizer/cooccurrence.ts +100 -0
  25. package/src/css-optimizer/core.ts +263 -0
  26. package/src/css-optimizer/extractors.ts +243 -0
  27. package/src/css-optimizer/hash.ts +54 -0
  28. package/src/css-optimizer/index.ts +129 -0
  29. package/src/css-optimizer/merge.ts +109 -0
  30. package/src/css-optimizer/moonbit-analyzer.ts +210 -0
  31. package/src/css-optimizer/parser.ts +120 -0
  32. package/src/css-optimizer/pattern.ts +171 -0
  33. package/src/css-optimizer/transformers.ts +301 -0
  34. package/src/css-optimizer/types.ts +128 -0
  35. package/src/event-utils.ts +227 -0
  36. package/src/hydration/createHydrator.ts +62 -0
  37. package/src/hydration/delegate.ts +62 -0
  38. package/src/hydration/drag.ts +214 -0
  39. package/src/hydration/index.ts +12 -0
  40. package/src/hydration/keyboard.ts +64 -0
  41. package/src/hydration/toggle.ts +101 -0
  42. package/src/index.ts +890 -0
  43. package/src/jsx-dev-runtime.ts +2 -0
  44. package/src/jsx-runtime.ts +398 -0
  45. package/src/vite-plugin.ts +718 -0
  46. package/tests/__screenshots__/context.test.ts/Context-API-context-with-reactive-effects-context-value-accessible-in-effect-1.png +0 -0
  47. package/tests/__screenshots__/dom.test.ts/DOM-API-For-component--SolidJS-style--For-updates-when-signal-changes-1.png +0 -0
  48. package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-accepts-children-as-function-1.png +0 -0
  49. package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-toggles-visibility-1.png +0 -0
  50. package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-attribute-1.png +0 -0
  51. package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-style-1.png +0 -0
  52. package/tests/__screenshots__/dom.test.ts/DOM-API-createElementNs--SVG-support--createElementNs-with-dynamic-attribute-1.png +0 -0
  53. package/tests/__screenshots__/dom.test.ts/DOM-API-effect-with-DOM-effect-tracks-signal-changes-1.png +0 -0
  54. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-clear-to-empty-1.png +0 -0
  55. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-empty-array-1.png +0 -0
  56. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-removes-items-1.png +0 -0
  57. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-renders-initial-list-1.png +0 -0
  58. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-updates-when-items-change-1.png +0 -0
  59. 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
  60. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-reordering-in-SVG-1.png +0 -0
  61. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-updates-SVG-elements-when-signal-changes-1.png +0 -0
  62. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-with-nested-SVG-groups-1.png +0 -0
  63. package/tests/__screenshots__/dom.test.ts/DOM-API-ref-callback--JSX-style--ref-callback-with-nested-elements-1.png +0 -0
  64. package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-creates-a-node-1.png +0 -0
  65. package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-with-false-condition-creates-placeholder-1.png +0 -0
  66. package/tests/__screenshots__/dom.test.ts/DOM-API-text-nodes-textDyn-creates-reactive-text-node-1.png +0 -0
  67. 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
  68. 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
  69. 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
  70. 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
  71. 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
  72. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Context---ForEach-integration-forEach-items-can-access-context-1.png +0 -0
  73. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-renders-initial-list-1.png +0 -0
  74. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-updates-when-signal-changes-1.png +0 -0
  75. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-with-object-items-1.png +0 -0
  76. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-hides-when-condition-is-false-1.png +0 -0
  77. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-renders-when-condition-is-true-1.png +0 -0
  78. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-from-false-to-true-1.png +0 -0
  79. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-reactively-1.png +0 -0
  80. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--event-listener-pattern--Solid-js-docs-example--1.png +0 -0
  81. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--multiple-cleanups-in-component-body--LIFO-order--1.png +0 -0
  82. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-in-component-body-runs-on-unmount-1.png +0 -0
  83. 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
  84. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--timer-cleanup-pattern--Solid-js-style--1.png +0 -0
  85. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Effects-effect-cleanup-runs-before-re-run-1.png +0 -0
  86. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-large-list-update-1.png +0 -0
  87. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-nested-batch-operations-1.png +0 -0
  88. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-rapid-sequential-updates-1.png +0 -0
  89. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-Show-component---visible-1.png +0 -0
  90. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-show-hide-element---visible-1.png +0 -0
  91. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-fragment-with-list-1.png +0 -0
  92. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-nested-fragments-1.png +0 -0
  93. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-simple-fragment-1.png +0 -0
  94. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-conditional-toggle-updates-1.png +0 -0
  95. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-addition-updates-match-1.png +0 -0
  96. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-removal-updates-match-1.png +0 -0
  97. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-text-updates-match-1.png +0 -0
  98. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-className-updates-1.png +0 -0
  99. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-style-updates-1.png +0 -0
  100. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-multiple-dynamic-attributes-1.png +0 -0
  101. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-deeply-nested-conditionals-1.png +0 -0
  102. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-empty-to-populated-1.png +0 -0
  103. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-populated-to-empty-1.png +0 -0
  104. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-cleanup-order-1.png +0 -0
  105. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-with-inner-signal-change-1.png +0 -0
  106. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-is-called-when-effect-re-runs-1.png +0 -0
  107. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-with-resource-simulation-1.png +0 -0
  108. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-multiple-children--no-wrapper--1.png +0 -0
  109. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-no-children-1.png +0 -0
  110. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-fragment-with-list-1.png +0 -0
  111. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-nested-Fragments-work-correctly-1.png +0 -0
  112. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-complex-reordering-with-additions-and-removals-1.png +0 -0
  113. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-insert-in-middle-1.png +0 -0
  114. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-remove-from-middle-1.png +0 -0
  115. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-reverse-list-order-1.png +0 -0
  116. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-shuffle-list-1.png +0 -0
  117. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-Show-component-renders-when-condition-is-true-1.png +0 -0
  118. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-renders-content-when-initially-true-1.png +0 -0
  119. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-toggles-visibility-dynamically-1.png +0 -0
  120. package/tests/__screenshots__/preact-signals-comparison.test.ts/Memo-Dependency-Chain-conditional-memo-dependencies-1.png +0 -0
  121. package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-basic-signal-get-set-produces-same-values-1.png +0 -0
  122. package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-batch-updates-produce-same-final-values-1.png +0 -0
  123. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-peek-reads-value-without-tracking-1.png +0 -0
  124. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-selective-tracking-with-untrack-1.png +0 -0
  125. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-untrack-prevents-dependency-tracking-1.png +0 -0
  126. package/tests/__screenshots__/resource.test.ts/Resource-API--SolidJS-style--reactivity-accessor-is-reactive-1.png +0 -0
  127. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-empty-string-for-non-failure-1.png +0 -0
  128. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-undefined-for-non-failure-1.png +0 -0
  129. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsFailure-and-stateError-1.png +0 -0
  130. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsPending-1.png +0 -0
  131. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsSuccess-and-stateValue-1.png +0 -0
  132. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateValue-returns-undefined-for-non-success-1.png +0 -0
  133. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-reject-transitions-to-failure-1.png +0 -0
  134. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-resolve-transitions-to-success-1.png +0 -0
  135. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-returns-resource--resolve--and-reject-functions-1.png +0 -0
  136. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-starts-in-pending-state-1.png +0 -0
  137. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-async-resolve-works-1.png +0 -0
  138. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-starts-in-pending-state-1.png +0 -0
  139. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-failure-on-reject-1.png +0 -0
  140. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-success-on-resolve-1.png +0 -0
  141. package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-can-wrap-fetch-like-async-operations-1.png +0 -0
  142. package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-works-with-setTimeout-simulation-1.png +0 -0
  143. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourceGet-tracks-dependencies-1.png +0 -0
  144. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourcePeek-does-not-track-dependencies-1.png +0 -0
  145. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceRefetch-refetch-resets-to-pending-and-re-runs-fetcher-1.png +0 -0
  146. package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-children-to-body-by-default-1.png +0 -0
  147. package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-to-selector-mount-target-1.png +0 -0
  148. package/tests/__screenshots__/solidjs-api.test.ts/SolidJS-API-compatibility-createEffect-tracks-dependencies-automatically-1.png +0 -0
  149. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-accessor-condition-in-Match-1.png +0 -0
  150. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-multiple-Match-components-1.png +0 -0
  151. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-single-Match-and-fallback-1.png +0 -0
  152. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-components-Switch-updates-DOM-when-signal-changes-1.png +0 -0
  153. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-multiple-dependencies-1.png +0 -0
  154. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-single-dependency-1.png +0 -0
  155. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-with-defer-option-skips-initial-run-1.png +0 -0
  156. package/tests/__screenshots__/store.test.ts/createStore-Arrays-array-updates-work-1.png +0 -0
  157. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-only-triggers-when-accessed-property-changes-1.png +0 -0
  158. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-parent-path-change-notifies-child-accessors-1.png +0 -0
  159. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-nested-property-access-1.png +0 -0
  160. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-property-access-in-effects-1.png +0 -0
  161. package/tests/context.test.ts +118 -0
  162. package/tests/css-optimizer-extractors.test.ts +264 -0
  163. package/tests/css-optimizer-integration.test.ts +566 -0
  164. package/tests/css-optimizer-transformers.test.ts +301 -0
  165. package/tests/css-optimizer.test.ts +646 -0
  166. package/tests/css-runtime.bench.ts +442 -0
  167. package/tests/css-runtime.test.ts +342 -0
  168. package/tests/dom.test.ts +872 -0
  169. package/tests/integration.test.ts +405 -0
  170. package/tests/issue-5-for-infinite-loop.test.ts +516 -0
  171. package/tests/jsx-runtime.test.tsx +393 -0
  172. package/tests/lifecycle.test.ts +833 -0
  173. package/tests/move-before.bench.ts +304 -0
  174. package/tests/preact-signals-comparison.test.ts +1608 -0
  175. package/tests/resource.test.ts +160 -0
  176. package/tests/router.test.ts +117 -0
  177. package/tests/show-initial-mount-leak.test.tsx +182 -0
  178. package/tests/solidjs-api.test.ts +659 -0
  179. package/tests/static-perf.bench.ts +64 -0
  180. package/tests/store.test.ts +263 -0
  181. package/tests/tsx-syntax.test.tsx +404 -0
  182. package/dist/src-BDdxGwvq.js +0 -1
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Co-occurrence analysis for CSS classes
3
+ */
4
+
5
+ import type { ClassUsage, CoOccurrence } from "./types.js";
6
+
7
+ /**
8
+ * Build a co-occurrence matrix from class usages
9
+ * Returns a nested map: classA -> classB -> count
10
+ * Classes are stored in alphabetical order (A < B)
11
+ */
12
+ export function buildCooccurrenceMatrix(
13
+ usages: ClassUsage[]
14
+ ): Map<string, Map<string, number>> {
15
+ const matrix = new Map<string, Map<string, number>>();
16
+
17
+ for (const usage of usages) {
18
+ const classes = usage.classes;
19
+ const n = classes.length;
20
+
21
+ // Count all pairs (i, j) where i < j
22
+ for (let i = 0; i < n; i++) {
23
+ for (let j = i + 1; j < n; j++) {
24
+ const a = classes[i];
25
+ const b = classes[j];
26
+
27
+ // Ensure consistent ordering (a < b alphabetically)
28
+ const [first, second] = a < b ? [a, b] : [b, a];
29
+
30
+ if (!matrix.has(first)) {
31
+ matrix.set(first, new Map());
32
+ }
33
+ const inner = matrix.get(first)!;
34
+ inner.set(second, (inner.get(second) || 0) + 1);
35
+ }
36
+ }
37
+ }
38
+
39
+ return matrix;
40
+ }
41
+
42
+ /**
43
+ * Convert matrix to a flat array of co-occurrences
44
+ * Sorted by frequency descending
45
+ */
46
+ export function matrixToCooccurrences(
47
+ matrix: Map<string, Map<string, number>>
48
+ ): CoOccurrence[] {
49
+ const result: CoOccurrence[] = [];
50
+
51
+ for (const [classA, inner] of matrix) {
52
+ for (const [classB, frequency] of inner) {
53
+ result.push({ classA, classB, frequency });
54
+ }
55
+ }
56
+
57
+ // Sort by frequency descending
58
+ result.sort((a, b) => b.frequency - a.frequency);
59
+
60
+ return result;
61
+ }
62
+
63
+ /**
64
+ * Get the top N co-occurring pairs
65
+ */
66
+ export function getTopCooccurrences(
67
+ cooccurrences: CoOccurrence[],
68
+ n: number
69
+ ): CoOccurrence[] {
70
+ return cooccurrences.slice(0, n);
71
+ }
72
+
73
+ /**
74
+ * Build an adjacency list for graph algorithms
75
+ * Only includes pairs with frequency >= minFrequency
76
+ */
77
+ export function buildAdjacencyList(
78
+ cooccurrences: CoOccurrence[],
79
+ minFrequency: number
80
+ ): Map<string, Array<{ target: string; weight: number }>> {
81
+ const adj = new Map<string, Array<{ target: string; weight: number }>>();
82
+
83
+ for (const co of cooccurrences) {
84
+ if (co.frequency >= minFrequency) {
85
+ // Add edge a -> b
86
+ if (!adj.has(co.classA)) {
87
+ adj.set(co.classA, []);
88
+ }
89
+ adj.get(co.classA)!.push({ target: co.classB, weight: co.frequency });
90
+
91
+ // Add edge b -> a (undirected graph)
92
+ if (!adj.has(co.classB)) {
93
+ adj.set(co.classB, []);
94
+ }
95
+ adj.get(co.classB)!.push({ target: co.classA, weight: co.frequency });
96
+ }
97
+ }
98
+
99
+ return adj;
100
+ }
@@ -0,0 +1,263 @@
1
+ /**
2
+ * CSS Co-occurrence Optimizer - Core Module
3
+ *
4
+ * Framework-agnostic core functions for CSS class co-occurrence analysis.
5
+ * This module contains pure functions with no framework dependencies.
6
+ */
7
+
8
+ import type { ClassUsage, MergePattern, OptimizeResult } from "./types.js";
9
+ import { hashMergedClassName } from "./hash.js";
10
+ import { findFrequentPatterns } from "./pattern.js";
11
+
12
+ /**
13
+ * Core optimization options (framework-agnostic)
14
+ */
15
+ export interface CoreOptimizeOptions {
16
+ /** Minimum frequency for a pattern to be merged (default: 2) */
17
+ minFrequency?: number;
18
+ /** Maximum pattern size to consider (default: 5) */
19
+ maxPatternSize?: number;
20
+ /** Pretty print CSS output (default: false) */
21
+ pretty?: boolean;
22
+ /** Enable verbose logging (default: false) */
23
+ verbose?: boolean;
24
+ }
25
+
26
+ /**
27
+ * Core optimization result
28
+ */
29
+ export interface CoreOptimizeResult {
30
+ /** Optimized CSS string */
31
+ css: string;
32
+ /** Mapping from sorted class combination to merged class name */
33
+ mergeMap: Map<string, string>;
34
+ /** Detected patterns */
35
+ patterns: MergePattern[];
36
+ /** Statistics */
37
+ stats: {
38
+ originalClasses: number;
39
+ mergedPatterns: number;
40
+ estimatedBytesSaved: number;
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Core optimizer - performs pattern analysis and CSS generation
46
+ *
47
+ * This is the framework-agnostic core that:
48
+ * 1. Analyzes class usages to find frequent patterns
49
+ * 2. Generates merged CSS rules
50
+ * 3. Returns a merge map for HTML transformation
51
+ *
52
+ * @param usages - Array of class usages extracted from source
53
+ * @param css - Original CSS content
54
+ * @param classToDeclaration - Map from class name to CSS declaration
55
+ * @param options - Optimization options
56
+ */
57
+ export function optimizeCore(
58
+ usages: ClassUsage[],
59
+ css: string,
60
+ classToDeclaration: Map<string, string>,
61
+ options: CoreOptimizeOptions = {}
62
+ ): CoreOptimizeResult {
63
+ const {
64
+ minFrequency = 2,
65
+ maxPatternSize = 5,
66
+ pretty = false,
67
+ verbose = false,
68
+ } = options;
69
+
70
+ const log = (msg: string) => {
71
+ if (verbose) {
72
+ console.log(`[css-optimizer] ${msg}`);
73
+ }
74
+ };
75
+
76
+ // Early return for empty input
77
+ if (usages.length === 0) {
78
+ return {
79
+ css,
80
+ mergeMap: new Map(),
81
+ patterns: [],
82
+ stats: {
83
+ originalClasses: classToDeclaration.size,
84
+ mergedPatterns: 0,
85
+ estimatedBytesSaved: 0,
86
+ },
87
+ };
88
+ }
89
+
90
+ log(`Analyzing ${usages.length} class usage sites`);
91
+
92
+ // Step 1: Find frequent patterns
93
+ const patterns = findFrequentPatterns(usages, minFrequency, maxPatternSize);
94
+ log(`Found ${patterns.length} frequent patterns`);
95
+
96
+ // Step 2: Generate merged classes
97
+ const mergeMap = new Map<string, string>();
98
+ const cssRules: string[] = [];
99
+ const usedClasses = new Set<string>();
100
+ let totalBytesSaved = 0;
101
+
102
+ for (const pattern of patterns) {
103
+ // Collect declarations for all classes in pattern
104
+ const declarations: string[] = [];
105
+ let allFound = true;
106
+
107
+ for (const cls of pattern.originalClasses) {
108
+ const decl = classToDeclaration.get(cls);
109
+ if (decl) {
110
+ declarations.push(decl);
111
+ } else {
112
+ allFound = false;
113
+ break;
114
+ }
115
+ }
116
+
117
+ if (!allFound || declarations.length === 0) {
118
+ continue;
119
+ }
120
+
121
+ // Check for conflicts (class already used in another merge)
122
+ let hasConflict = false;
123
+ for (const cls of pattern.originalClasses) {
124
+ if (usedClasses.has(cls)) {
125
+ hasConflict = true;
126
+ break;
127
+ }
128
+ }
129
+
130
+ if (hasConflict) {
131
+ continue;
132
+ }
133
+
134
+ // Generate merged class name
135
+ const mergedClass = hashMergedClassName(declarations);
136
+
137
+ pattern.mergedClass = mergedClass;
138
+ pattern.declarations = declarations;
139
+
140
+ // Create merge key (sorted classes joined by space)
141
+ const mergeKey = pattern.originalClasses.join(" ");
142
+ mergeMap.set(mergeKey, mergedClass);
143
+
144
+ // Mark classes as used
145
+ for (const cls of pattern.originalClasses) {
146
+ usedClasses.add(cls);
147
+ }
148
+
149
+ // Generate CSS rule
150
+ const declStr = declarations.join(";");
151
+ if (pretty) {
152
+ cssRules.push(`.${mergedClass} { ${declStr.replace(/;/g, "; ")} }`);
153
+ } else {
154
+ cssRules.push(`.${mergedClass}{${declStr}}`);
155
+ }
156
+
157
+ totalBytesSaved += pattern.bytesSaved;
158
+ }
159
+
160
+ // Step 3: Filter out merged classes from original CSS
161
+ const originalRules: string[] = [];
162
+ const rulePattern = /\.([a-zA-Z_][a-zA-Z0-9_-]*)\{([^}]+)\}/g;
163
+
164
+ let match: RegExpExecArray | null;
165
+ rulePattern.lastIndex = 0;
166
+ while ((match = rulePattern.exec(css)) !== null) {
167
+ const cls = match[1];
168
+ if (!usedClasses.has(cls)) {
169
+ originalRules.push(match[0]);
170
+ }
171
+ }
172
+
173
+ // Preserve @media rules
174
+ const mediaPattern = /@media[^{]+\{[^}]+\}/g;
175
+ mediaPattern.lastIndex = 0;
176
+ while ((match = mediaPattern.exec(css)) !== null) {
177
+ originalRules.push(match[0]);
178
+ }
179
+
180
+ // Preserve pseudo-class rules
181
+ const pseudoPattern = /\.([a-zA-Z_][a-zA-Z0-9_-]*)(:[a-zA-Z-]+)\{([^}]+)\}/g;
182
+ pseudoPattern.lastIndex = 0;
183
+ while ((match = pseudoPattern.exec(css)) !== null) {
184
+ originalRules.push(match[0]);
185
+ }
186
+
187
+ // Combine merged + remaining original rules
188
+ const separator = pretty ? "\n" : "";
189
+ const optimizedCss = [...cssRules, ...originalRules].join(separator);
190
+
191
+ const actualPatterns = patterns.filter((p) => p.mergedClass !== "");
192
+
193
+ log(`Merged ${actualPatterns.length} patterns`);
194
+ log(`Estimated savings: ${totalBytesSaved} bytes`);
195
+
196
+ return {
197
+ css: optimizedCss,
198
+ mergeMap,
199
+ patterns: actualPatterns,
200
+ stats: {
201
+ originalClasses: classToDeclaration.size,
202
+ mergedPatterns: actualPatterns.length,
203
+ estimatedBytesSaved: totalBytesSaved,
204
+ },
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Apply merge map to class list
210
+ *
211
+ * Takes a list of classes and returns the optimized list
212
+ * with merged classes replacing original combinations.
213
+ *
214
+ * @param classes - Array of class names
215
+ * @param mergeMap - Merge map from optimizeCore
216
+ * @param classPrefix - Prefix for classes to consider for merging
217
+ */
218
+ export function applyMergeToClasses(
219
+ classes: string[],
220
+ mergeMap: Map<string, string>,
221
+ classPrefix = "_"
222
+ ): string[] {
223
+ if (mergeMap.size === 0) {
224
+ return classes;
225
+ }
226
+
227
+ // Separate prefixed and non-prefixed classes
228
+ const prefixedClasses = classes.filter((c) => c.startsWith(classPrefix));
229
+ const otherClasses = classes.filter((c) => !c.startsWith(classPrefix));
230
+
231
+ // Sort for consistent matching
232
+ prefixedClasses.sort();
233
+
234
+ let result = [...prefixedClasses];
235
+ const merged: string[] = [];
236
+
237
+ // Sort merge keys by length descending (longer patterns first)
238
+ const sortedKeys = [...mergeMap.keys()].sort((a, b) => b.length - a.length);
239
+
240
+ // Try to apply each merge pattern
241
+ for (const key of sortedKeys) {
242
+ const patternClasses = key.split(" ");
243
+ const mergedClass = mergeMap.get(key)!;
244
+
245
+ // Check if all pattern classes are present
246
+ let allPresent = true;
247
+ for (const cls of patternClasses) {
248
+ if (!result.includes(cls)) {
249
+ allPresent = false;
250
+ break;
251
+ }
252
+ }
253
+
254
+ if (allPresent) {
255
+ // Remove pattern classes and add merged class
256
+ result = result.filter((c) => !patternClasses.includes(c));
257
+ merged.push(mergedClass);
258
+ }
259
+ }
260
+
261
+ // Return: merged classes + remaining prefixed + other classes
262
+ return [...merged, ...result, ...otherClasses];
263
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * CSS Co-occurrence Optimizer - Extractors
3
+ *
4
+ * Pluggable class extraction strategies for different source formats.
5
+ * Each extractor takes source content and returns ClassUsage arrays.
6
+ */
7
+
8
+ import type { ClassUsage } from "./types.js";
9
+
10
+ /**
11
+ * Extractor interface - implement this for custom source formats
12
+ */
13
+ export interface ClassExtractor {
14
+ /** Extractor name for debugging */
15
+ name: string;
16
+
17
+ /**
18
+ * Extract class usages from source content
19
+ * @param content - Source content (HTML, JSX, etc.)
20
+ * @param options - Extractor-specific options
21
+ */
22
+ extract(content: string, options?: ExtractorOptions): ClassUsage[];
23
+ }
24
+
25
+ /**
26
+ * Common extractor options
27
+ */
28
+ export interface ExtractorOptions {
29
+ /** Only extract classes starting with this prefix (default: "_") */
30
+ classPrefix?: string;
31
+ /** Minimum classes per element to include (default: 2) */
32
+ minClasses?: number;
33
+ /** Source identifier for debugging */
34
+ source?: string;
35
+ }
36
+
37
+ /**
38
+ * HTML Class Extractor
39
+ *
40
+ * Extracts class usages from HTML content by parsing class="" attributes.
41
+ * Works with static HTML, SSR output, template strings, etc.
42
+ */
43
+ export class HtmlExtractor implements ClassExtractor {
44
+ name = "html";
45
+
46
+ private pattern = /class\s*=\s*"([^"]+)"/g;
47
+
48
+ extract(content: string, options: ExtractorOptions = {}): ClassUsage[] {
49
+ const { classPrefix = "_", minClasses = 2, source = "html" } = options;
50
+
51
+ const usages: ClassUsage[] = [];
52
+ let match: RegExpExecArray | null;
53
+
54
+ this.pattern.lastIndex = 0;
55
+ while ((match = this.pattern.exec(content)) !== null) {
56
+ const classValue = match[1].trim();
57
+ if (!classValue) continue;
58
+
59
+ // Split by whitespace and filter by prefix
60
+ const classes = classValue
61
+ .split(/\s+/)
62
+ .filter((c) => c.startsWith(classPrefix) && c.length > classPrefix.length);
63
+
64
+ // Only include elements with minClasses or more matching classes
65
+ if (classes.length >= minClasses) {
66
+ classes.sort();
67
+ usages.push({ classes, source: `${source}:${match.index}` });
68
+ }
69
+ }
70
+
71
+ return usages;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * JSX Class Extractor (simplified)
77
+ *
78
+ * Extracts class usages from JSX/TSX by finding className attributes.
79
+ * Note: This is a simplified regex-based extractor. For full AST support,
80
+ * consider using @babel/parser or TypeScript compiler.
81
+ */
82
+ export class JsxExtractor implements ClassExtractor {
83
+ name = "jsx";
84
+
85
+ // Match className="..." and className={'...'}
86
+ private stringPattern = /className\s*=\s*"([^"]+)"/g;
87
+ private templatePattern = /className\s*=\s*\{['"`]([^'"`]+)['"`]\}/g;
88
+
89
+ extract(content: string, options: ExtractorOptions = {}): ClassUsage[] {
90
+ const { classPrefix = "_", minClasses = 2, source = "jsx" } = options;
91
+
92
+ const usages: ClassUsage[] = [];
93
+
94
+ // Extract from string literals
95
+ this.extractFromPattern(content, this.stringPattern, classPrefix, minClasses, source, usages);
96
+ // Extract from template literals
97
+ this.extractFromPattern(content, this.templatePattern, classPrefix, minClasses, source, usages);
98
+
99
+ return usages;
100
+ }
101
+
102
+ private extractFromPattern(
103
+ content: string,
104
+ pattern: RegExp,
105
+ classPrefix: string,
106
+ minClasses: number,
107
+ source: string,
108
+ usages: ClassUsage[]
109
+ ): void {
110
+ let match: RegExpExecArray | null;
111
+
112
+ pattern.lastIndex = 0;
113
+ while ((match = pattern.exec(content)) !== null) {
114
+ const classValue = match[1].trim();
115
+ if (!classValue) continue;
116
+
117
+ const classes = classValue
118
+ .split(/\s+/)
119
+ .filter((c) => c.startsWith(classPrefix) && c.length > classPrefix.length);
120
+
121
+ if (classes.length >= minClasses) {
122
+ classes.sort();
123
+ usages.push({ classes, source: `${source}:${match.index}` });
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Svelte Class Extractor (simplified)
131
+ *
132
+ * Extracts class usages from Svelte templates.
133
+ * Handles both class="..." and class:directive syntax.
134
+ */
135
+ export class SvelteExtractor implements ClassExtractor {
136
+ name = "svelte";
137
+
138
+ private classPattern = /class\s*=\s*"([^"]+)"/g;
139
+
140
+ extract(content: string, options: ExtractorOptions = {}): ClassUsage[] {
141
+ const { classPrefix = "_", minClasses = 2, source = "svelte" } = options;
142
+
143
+ const usages: ClassUsage[] = [];
144
+ let match: RegExpExecArray | null;
145
+
146
+ this.classPattern.lastIndex = 0;
147
+ while ((match = this.classPattern.exec(content)) !== null) {
148
+ const classValue = match[1].trim();
149
+ if (!classValue) continue;
150
+
151
+ // Handle Svelte's {expression} syntax by removing them
152
+ const cleanValue = classValue.replace(/\{[^}]+\}/g, " ");
153
+
154
+ const classes = cleanValue
155
+ .split(/\s+/)
156
+ .filter((c) => c.startsWith(classPrefix) && c.length > classPrefix.length);
157
+
158
+ if (classes.length >= minClasses) {
159
+ classes.sort();
160
+ usages.push({ classes, source: `${source}:${match.index}` });
161
+ }
162
+ }
163
+
164
+ return usages;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Multi-source extractor
170
+ *
171
+ * Combines usages from multiple sources/extractors.
172
+ * Useful for analyzing entire projects with mixed file types.
173
+ */
174
+ export class MultiExtractor implements ClassExtractor {
175
+ name = "multi";
176
+
177
+ private extractors: Map<string, ClassExtractor>;
178
+
179
+ constructor() {
180
+ this.extractors = new Map<string, ClassExtractor>([
181
+ ["html", new HtmlExtractor()],
182
+ ["jsx", new JsxExtractor()],
183
+ ["tsx", new JsxExtractor()],
184
+ ["svelte", new SvelteExtractor()],
185
+ ]);
186
+ }
187
+
188
+ /**
189
+ * Register a custom extractor for a file extension
190
+ */
191
+ register(extension: string, extractor: ClassExtractor): void {
192
+ this.extractors.set(extension, extractor);
193
+ }
194
+
195
+ /**
196
+ * Extract from content using the appropriate extractor
197
+ */
198
+ extract(content: string, options: ExtractorOptions = {}): ClassUsage[] {
199
+ // Default to HTML extractor
200
+ const htmlExtractor = this.extractors.get("html")!;
201
+ return htmlExtractor.extract(content, options);
202
+ }
203
+
204
+ /**
205
+ * Extract from content with explicit file type
206
+ */
207
+ extractWithType(
208
+ content: string,
209
+ fileType: string,
210
+ options: ExtractorOptions = {}
211
+ ): ClassUsage[] {
212
+ const extractor = this.extractors.get(fileType) || this.extractors.get("html")!;
213
+ return extractor.extract(content, options);
214
+ }
215
+
216
+ /**
217
+ * Extract from multiple files
218
+ */
219
+ extractFromFiles(
220
+ files: Array<{ content: string; path: string }>,
221
+ options: ExtractorOptions = {}
222
+ ): ClassUsage[] {
223
+ const allUsages: ClassUsage[] = [];
224
+
225
+ for (const file of files) {
226
+ const ext = file.path.split(".").pop() || "html";
227
+ const extractor = this.extractors.get(ext) || this.extractors.get("html")!;
228
+ const usages = extractor.extract(file.content, {
229
+ ...options,
230
+ source: file.path,
231
+ });
232
+ allUsages.push(...usages);
233
+ }
234
+
235
+ return allUsages;
236
+ }
237
+ }
238
+
239
+ // Default extractors
240
+ export const htmlExtractor = new HtmlExtractor();
241
+ export const jsxExtractor = new JsxExtractor();
242
+ export const svelteExtractor = new SvelteExtractor();
243
+ export const multiExtractor = new MultiExtractor();
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Hash functions for CSS class name generation
3
+ */
4
+
5
+ /**
6
+ * DJB2 hash function
7
+ * A simple but effective string hashing algorithm
8
+ */
9
+ export function djb2Hash(s: string): number {
10
+ let hash = 5381;
11
+ for (let i = 0; i < s.length; i++) {
12
+ const c = s.charCodeAt(i);
13
+ // hash * 33 + c (using unsigned 32-bit arithmetic)
14
+ hash = ((hash << 5) + hash + c) >>> 0;
15
+ }
16
+ return hash;
17
+ }
18
+
19
+ /**
20
+ * Convert a number to base36 string (0-9a-z)
21
+ * Takes lower 24 bits to keep class names short (4-5 chars)
22
+ */
23
+ export function toBase36(n: number): string {
24
+ const chars = "0123456789abcdefghijklmnopqrstuvwxyz";
25
+ // Take lower 24 bits
26
+ n = n & 0xffffff;
27
+ if (n === 0) return "0";
28
+ let result = "";
29
+ while (n > 0) {
30
+ result = chars[n % 36] + result;
31
+ n = Math.floor(n / 36);
32
+ }
33
+ return result;
34
+ }
35
+
36
+ /**
37
+ * Generate a class name from a CSS declaration string
38
+ * @param decl - CSS declaration like "display:flex"
39
+ * @param prefix - Class name prefix (default: "_")
40
+ */
41
+ export function hashClassName(decl: string, prefix = "_"): string {
42
+ const hash = djb2Hash(decl);
43
+ return prefix + toBase36(hash);
44
+ }
45
+
46
+ /**
47
+ * Generate a merged class name from combined declarations
48
+ * @param declarations - Array of CSS declarations
49
+ * @param prefix - Class name prefix (default: "_m" for merged)
50
+ */
51
+ export function hashMergedClassName(declarations: string[], prefix = "_m"): string {
52
+ const combined = declarations.sort().join(";");
53
+ return prefix + toBase36(djb2Hash(combined));
54
+ }