@luna_ui/luna 0.3.4 → 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 (172) 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.js +1 -1
  9. package/dist/{src-BDdxGwvq.js → src-CHiGeWfy.js} +1 -1
  10. package/dist/vite-plugin.d.ts +122 -0
  11. package/dist/vite-plugin.js +1518 -0
  12. package/package.json +16 -2
  13. package/src/css/extract.ts +798 -0
  14. package/src/css/index.ts +10 -0
  15. package/src/css/inject.ts +205 -0
  16. package/src/css/inline.ts +182 -0
  17. package/src/css/minify.ts +70 -0
  18. package/src/css/optimizer.ts +6 -0
  19. package/src/css/runtime.ts +344 -0
  20. package/src/css-optimizer/README.md +353 -0
  21. package/src/css-optimizer/cooccurrence.ts +100 -0
  22. package/src/css-optimizer/core.ts +263 -0
  23. package/src/css-optimizer/extractors.ts +243 -0
  24. package/src/css-optimizer/hash.ts +54 -0
  25. package/src/css-optimizer/index.ts +129 -0
  26. package/src/css-optimizer/merge.ts +109 -0
  27. package/src/css-optimizer/moonbit-analyzer.ts +210 -0
  28. package/src/css-optimizer/parser.ts +120 -0
  29. package/src/css-optimizer/pattern.ts +171 -0
  30. package/src/css-optimizer/transformers.ts +301 -0
  31. package/src/css-optimizer/types.ts +128 -0
  32. package/src/event-utils.ts +227 -0
  33. package/src/index.ts +890 -0
  34. package/src/jsx-dev-runtime.ts +2 -0
  35. package/src/jsx-runtime.ts +398 -0
  36. package/src/vite-plugin.ts +718 -0
  37. package/tests/__screenshots__/context.test.ts/Context-API-context-with-reactive-effects-context-value-accessible-in-effect-1.png +0 -0
  38. package/tests/__screenshots__/dom.test.ts/DOM-API-For-component--SolidJS-style--For-updates-when-signal-changes-1.png +0 -0
  39. package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-accepts-children-as-function-1.png +0 -0
  40. package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-toggles-visibility-1.png +0 -0
  41. package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-attribute-1.png +0 -0
  42. package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-style-1.png +0 -0
  43. package/tests/__screenshots__/dom.test.ts/DOM-API-createElementNs--SVG-support--createElementNs-with-dynamic-attribute-1.png +0 -0
  44. package/tests/__screenshots__/dom.test.ts/DOM-API-effect-with-DOM-effect-tracks-signal-changes-1.png +0 -0
  45. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-clear-to-empty-1.png +0 -0
  46. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-empty-array-1.png +0 -0
  47. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-removes-items-1.png +0 -0
  48. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-renders-initial-list-1.png +0 -0
  49. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-updates-when-items-change-1.png +0 -0
  50. 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
  51. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-reordering-in-SVG-1.png +0 -0
  52. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-updates-SVG-elements-when-signal-changes-1.png +0 -0
  53. package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-with-nested-SVG-groups-1.png +0 -0
  54. package/tests/__screenshots__/dom.test.ts/DOM-API-ref-callback--JSX-style--ref-callback-with-nested-elements-1.png +0 -0
  55. package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-creates-a-node-1.png +0 -0
  56. package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-with-false-condition-creates-placeholder-1.png +0 -0
  57. package/tests/__screenshots__/dom.test.ts/DOM-API-text-nodes-textDyn-creates-reactive-text-node-1.png +0 -0
  58. 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
  59. 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
  60. 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
  61. 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
  62. 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
  63. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Context---ForEach-integration-forEach-items-can-access-context-1.png +0 -0
  64. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-renders-initial-list-1.png +0 -0
  65. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-updates-when-signal-changes-1.png +0 -0
  66. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-with-object-items-1.png +0 -0
  67. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-hides-when-condition-is-false-1.png +0 -0
  68. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-renders-when-condition-is-true-1.png +0 -0
  69. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-from-false-to-true-1.png +0 -0
  70. package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-reactively-1.png +0 -0
  71. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--event-listener-pattern--Solid-js-docs-example--1.png +0 -0
  72. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--multiple-cleanups-in-component-body--LIFO-order--1.png +0 -0
  73. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-in-component-body-runs-on-unmount-1.png +0 -0
  74. 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
  75. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--timer-cleanup-pattern--Solid-js-style--1.png +0 -0
  76. package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Effects-effect-cleanup-runs-before-re-run-1.png +0 -0
  77. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-large-list-update-1.png +0 -0
  78. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-nested-batch-operations-1.png +0 -0
  79. package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-rapid-sequential-updates-1.png +0 -0
  80. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-Show-component---visible-1.png +0 -0
  81. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-show-hide-element---visible-1.png +0 -0
  82. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-fragment-with-list-1.png +0 -0
  83. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-nested-fragments-1.png +0 -0
  84. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-simple-fragment-1.png +0 -0
  85. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-conditional-toggle-updates-1.png +0 -0
  86. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-addition-updates-match-1.png +0 -0
  87. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-removal-updates-match-1.png +0 -0
  88. package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-text-updates-match-1.png +0 -0
  89. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-className-updates-1.png +0 -0
  90. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-style-updates-1.png +0 -0
  91. package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-multiple-dynamic-attributes-1.png +0 -0
  92. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-deeply-nested-conditionals-1.png +0 -0
  93. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-empty-to-populated-1.png +0 -0
  94. package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-populated-to-empty-1.png +0 -0
  95. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-cleanup-order-1.png +0 -0
  96. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-with-inner-signal-change-1.png +0 -0
  97. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-is-called-when-effect-re-runs-1.png +0 -0
  98. package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-with-resource-simulation-1.png +0 -0
  99. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-multiple-children--no-wrapper--1.png +0 -0
  100. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-no-children-1.png +0 -0
  101. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-fragment-with-list-1.png +0 -0
  102. package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-nested-Fragments-work-correctly-1.png +0 -0
  103. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-complex-reordering-with-additions-and-removals-1.png +0 -0
  104. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-insert-in-middle-1.png +0 -0
  105. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-remove-from-middle-1.png +0 -0
  106. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-reverse-list-order-1.png +0 -0
  107. package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-shuffle-list-1.png +0 -0
  108. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-Show-component-renders-when-condition-is-true-1.png +0 -0
  109. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-renders-content-when-initially-true-1.png +0 -0
  110. package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-toggles-visibility-dynamically-1.png +0 -0
  111. package/tests/__screenshots__/preact-signals-comparison.test.ts/Memo-Dependency-Chain-conditional-memo-dependencies-1.png +0 -0
  112. package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-basic-signal-get-set-produces-same-values-1.png +0 -0
  113. package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-batch-updates-produce-same-final-values-1.png +0 -0
  114. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-peek-reads-value-without-tracking-1.png +0 -0
  115. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-selective-tracking-with-untrack-1.png +0 -0
  116. package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-untrack-prevents-dependency-tracking-1.png +0 -0
  117. package/tests/__screenshots__/resource.test.ts/Resource-API--SolidJS-style--reactivity-accessor-is-reactive-1.png +0 -0
  118. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-empty-string-for-non-failure-1.png +0 -0
  119. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-undefined-for-non-failure-1.png +0 -0
  120. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsFailure-and-stateError-1.png +0 -0
  121. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsPending-1.png +0 -0
  122. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsSuccess-and-stateValue-1.png +0 -0
  123. package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateValue-returns-undefined-for-non-success-1.png +0 -0
  124. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-reject-transitions-to-failure-1.png +0 -0
  125. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-resolve-transitions-to-success-1.png +0 -0
  126. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-returns-resource--resolve--and-reject-functions-1.png +0 -0
  127. package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-starts-in-pending-state-1.png +0 -0
  128. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-async-resolve-works-1.png +0 -0
  129. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-starts-in-pending-state-1.png +0 -0
  130. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-failure-on-reject-1.png +0 -0
  131. package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-success-on-resolve-1.png +0 -0
  132. package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-can-wrap-fetch-like-async-operations-1.png +0 -0
  133. package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-works-with-setTimeout-simulation-1.png +0 -0
  134. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourceGet-tracks-dependencies-1.png +0 -0
  135. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourcePeek-does-not-track-dependencies-1.png +0 -0
  136. package/tests/__screenshots__/resource.test.ts/Resource-API-resourceRefetch-refetch-resets-to-pending-and-re-runs-fetcher-1.png +0 -0
  137. package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-children-to-body-by-default-1.png +0 -0
  138. package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-to-selector-mount-target-1.png +0 -0
  139. package/tests/__screenshots__/solidjs-api.test.ts/SolidJS-API-compatibility-createEffect-tracks-dependencies-automatically-1.png +0 -0
  140. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-accessor-condition-in-Match-1.png +0 -0
  141. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-multiple-Match-components-1.png +0 -0
  142. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-single-Match-and-fallback-1.png +0 -0
  143. package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-components-Switch-updates-DOM-when-signal-changes-1.png +0 -0
  144. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-multiple-dependencies-1.png +0 -0
  145. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-single-dependency-1.png +0 -0
  146. package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-with-defer-option-skips-initial-run-1.png +0 -0
  147. package/tests/__screenshots__/store.test.ts/createStore-Arrays-array-updates-work-1.png +0 -0
  148. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-only-triggers-when-accessed-property-changes-1.png +0 -0
  149. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-parent-path-change-notifies-child-accessors-1.png +0 -0
  150. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-nested-property-access-1.png +0 -0
  151. package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-property-access-in-effects-1.png +0 -0
  152. package/tests/context.test.ts +118 -0
  153. package/tests/css-optimizer-extractors.test.ts +264 -0
  154. package/tests/css-optimizer-integration.test.ts +566 -0
  155. package/tests/css-optimizer-transformers.test.ts +301 -0
  156. package/tests/css-optimizer.test.ts +646 -0
  157. package/tests/css-runtime.bench.ts +442 -0
  158. package/tests/css-runtime.test.ts +342 -0
  159. package/tests/dom.test.ts +872 -0
  160. package/tests/integration.test.ts +405 -0
  161. package/tests/issue-5-for-infinite-loop.test.ts +516 -0
  162. package/tests/jsx-runtime.test.tsx +393 -0
  163. package/tests/lifecycle.test.ts +833 -0
  164. package/tests/move-before.bench.ts +304 -0
  165. package/tests/preact-signals-comparison.test.ts +1608 -0
  166. package/tests/resource.test.ts +160 -0
  167. package/tests/router.test.ts +117 -0
  168. package/tests/show-initial-mount-leak.test.tsx +182 -0
  169. package/tests/solidjs-api.test.ts +659 -0
  170. package/tests/static-perf.bench.ts +64 -0
  171. package/tests/store.test.ts +263 -0
  172. package/tests/tsx-syntax.test.tsx +404 -0
@@ -0,0 +1,718 @@
1
+ /**
2
+ * Luna CSS Vite Plugin
3
+ *
4
+ * Automatically extracts and injects Luna CSS utilities during development and build.
5
+ */
6
+
7
+ import type { Plugin, ResolvedConfig, HtmlTagDescriptor } from "vite";
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import { extract, extractSplit, type SplitExtractResult } from "./css/extract.js";
11
+ import { optimizeCss, optimizeHtml, type OptimizeOptions } from "./css/optimizer.js";
12
+
13
+ export type OutputMode = "inline" | "external" | "auto";
14
+
15
+ export interface LunaCssPluginOptions {
16
+ /**
17
+ * Source directories to extract CSS from
18
+ * @default ["src"]
19
+ */
20
+ src?: string | string[];
21
+
22
+ /**
23
+ * Output mode: "inline" embeds in HTML, "external" creates .css file, "auto" chooses by size
24
+ * @default "auto"
25
+ */
26
+ mode?: OutputMode;
27
+
28
+ /**
29
+ * Size threshold in bytes for "auto" mode
30
+ * @default 4096
31
+ */
32
+ threshold?: number;
33
+
34
+ /**
35
+ * CSS filename for external mode (without hash, Vite will add hash in build)
36
+ * @default "luna.css"
37
+ */
38
+ cssFileName?: string;
39
+
40
+ /**
41
+ * Enable verbose logging
42
+ * @default false
43
+ */
44
+ verbose?: boolean;
45
+
46
+ /**
47
+ * Enable CSS splitting by directory
48
+ * When enabled, generates per-directory CSS chunks + shared CSS
49
+ * @default false
50
+ */
51
+ split?: boolean;
52
+
53
+ /**
54
+ * Minimum usage count for a declaration to be considered "shared"
55
+ * Only used when split is enabled
56
+ * @default 3
57
+ */
58
+ sharedThreshold?: number;
59
+
60
+ /**
61
+ * Include runtime CSS fallback in dev mode
62
+ * Generates CSS dynamically for missing rules with console warnings
63
+ * The runtime uses import.meta.env.DEV for tree-shaking in production
64
+ * @default true in dev, false in build
65
+ */
66
+ devRuntime?: boolean;
67
+
68
+ /**
69
+ * Inject dev runtime script into HTML automatically
70
+ * When false, users must manually import "virtual:luna-css-runtime"
71
+ * @default true
72
+ */
73
+ injectRuntime?: boolean;
74
+
75
+ /**
76
+ * Experimental features
77
+ */
78
+ experimental?: {
79
+ /**
80
+ * Enable CSS co-occurrence optimization
81
+ * Merges frequently co-occurring classes into single combined classes
82
+ * @default false
83
+ */
84
+ optimize?: boolean;
85
+
86
+ /**
87
+ * Minimum frequency for a pattern to be merged
88
+ * @default 2
89
+ */
90
+ optimizeMinFrequency?: number;
91
+
92
+ /**
93
+ * Maximum pattern size to consider for merging
94
+ * @default 5
95
+ */
96
+ optimizeMaxPatternSize?: number;
97
+ };
98
+ }
99
+
100
+ // Markers for CSS injection (legacy mode)
101
+ const CSS_START_MARKER = "/* LUNA_CSS_START */";
102
+ const CSS_END_MARKER = "/* LUNA_CSS_END */";
103
+
104
+ // Virtual module IDs
105
+ const VIRTUAL_CSS_ID = "virtual:luna.css";
106
+ const VIRTUAL_SHARED_CSS_ID = "virtual:luna-shared.css";
107
+ const VIRTUAL_RUNTIME_ID = "virtual:luna-css-runtime";
108
+ const RESOLVED_VIRTUAL_CSS_ID = "\0" + VIRTUAL_CSS_ID;
109
+ const RESOLVED_VIRTUAL_SHARED_CSS_ID = "\0" + VIRTUAL_SHARED_CSS_ID;
110
+ const RESOLVED_VIRTUAL_RUNTIME_ID = "\0" + VIRTUAL_RUNTIME_ID;
111
+
112
+ /**
113
+ * Generate inline dev runtime code
114
+ * Uses import.meta.env.DEV for tree-shaking in production builds
115
+ */
116
+ function generateRuntimeCode(): string {
117
+ return `
118
+ // Luna CSS Dev Runtime - Auto-generated
119
+ // Production: DCE removes dev-only code via import.meta.env.DEV
120
+
121
+ // Hash functions (shared between dev and prod for class name generation)
122
+ function djb2Hash(s) {
123
+ let hash = 5381;
124
+ for (let i = 0; i < s.length; i++) {
125
+ hash = ((hash << 5) + hash + s.charCodeAt(i)) >>> 0;
126
+ }
127
+ return hash;
128
+ }
129
+
130
+ function toBase36(n) {
131
+ const chars = "0123456789abcdefghijklmnopqrstuvwxyz";
132
+ n = n & 0xffffff;
133
+ if (n === 0) return "0";
134
+ let result = "";
135
+ while (n > 0) { result = chars[n % 36] + result; n = Math.floor(n / 36); }
136
+ return result;
137
+ }
138
+
139
+ function hashClassName(decl) { return "_" + toBase36(djb2Hash(decl)); }
140
+
141
+ // Dev-only: runtime CSS injection
142
+ let devState;
143
+ let devInject;
144
+
145
+ if (import.meta.env.DEV) {
146
+ devState = { rules: new Map(), styleEl: null, initialized: false };
147
+
148
+ const init = () => {
149
+ if (devState.initialized || typeof document === "undefined") return;
150
+ devState.styleEl = document.createElement("style");
151
+ devState.styleEl.id = "luna-dev-css";
152
+ document.head.appendChild(devState.styleEl);
153
+ devState.initialized = true;
154
+ };
155
+
156
+ devInject = (className, rule) => {
157
+ init();
158
+ if (devState.rules.has(className)) return;
159
+ devState.rules.set(className, rule);
160
+ if (devState.styleEl) devState.styleEl.textContent += rule;
161
+ console.warn("[luna-css] Generated at runtime:", rule, "\\n → Run 'luna css extract' to pre-generate");
162
+ };
163
+ }
164
+
165
+ export function css(prop, val) {
166
+ const decl = prop + ":" + val;
167
+ const cls = hashClassName(decl);
168
+ if (import.meta.env.DEV && devInject) {
169
+ devInject(cls, "." + cls + "{" + decl + "}");
170
+ }
171
+ return cls;
172
+ }
173
+
174
+ export function styles(pairs) { return pairs.map(([p, v]) => css(p, v)).join(" "); }
175
+
176
+ export function on(pseudo, prop, val) {
177
+ const key = pseudo + ":" + prop + ":" + val;
178
+ const cls = hashClassName(key);
179
+ if (import.meta.env.DEV && devInject) {
180
+ devInject(cls, "." + cls + pseudo + "{" + prop + ":" + val + "}");
181
+ }
182
+ return cls;
183
+ }
184
+
185
+ export function hover(p, v) { return on(":hover", p, v); }
186
+ export function focus(p, v) { return on(":focus", p, v); }
187
+ export function active(p, v) { return on(":active", p, v); }
188
+ export function combine(classes) { return classes.filter(Boolean).join(" "); }
189
+
190
+ // Dev-only utilities
191
+ export function getGeneratedCount() {
192
+ if (import.meta.env.DEV && devState) {
193
+ return devState.rules.size;
194
+ }
195
+ return 0;
196
+ }
197
+
198
+ export function getGeneratedCss() {
199
+ if (import.meta.env.DEV && devState) {
200
+ return Array.from(devState.rules.values()).join("");
201
+ }
202
+ return "";
203
+ }
204
+ `;
205
+ }
206
+
207
+ /**
208
+ * Inject dev runtime script into HTML
209
+ */
210
+ function injectDevRuntime(html: string): string {
211
+ const script = `<script type="module">
212
+ import { css, hover, focus, styles, combine } from "virtual:luna-css-runtime";
213
+ window.__lunaCss = { css, hover, focus, styles, combine };
214
+ </script>`;
215
+
216
+ // Insert before </head>
217
+ return html.replace("</head>", script + "\n</head>");
218
+ }
219
+
220
+ /**
221
+ * Luna CSS Vite Plugin
222
+ *
223
+ * @example
224
+ * ```ts
225
+ * // vite.config.ts
226
+ * import { lunaCss } from "@luna_ui/luna/vite-plugin";
227
+ *
228
+ * export default defineConfig({
229
+ * plugins: [
230
+ * lunaCss({
231
+ * src: ["src/examples/todomvc"],
232
+ * mode: "auto",
233
+ * threshold: 4096,
234
+ * }),
235
+ * ],
236
+ * });
237
+ * ```
238
+ *
239
+ * @example
240
+ * ```ts
241
+ * // main.ts - Import virtual CSS like Tailwind
242
+ * import "virtual:luna.css";
243
+ * ```
244
+ *
245
+ * @example
246
+ * ```ts
247
+ * // Split mode with per-directory chunks
248
+ * lunaCss({
249
+ * src: ["src"],
250
+ * split: true,
251
+ * sharedThreshold: 3,
252
+ * })
253
+ *
254
+ * // In your entry:
255
+ * import "virtual:luna.css"; // All CSS
256
+ * // Or for fine-grained control:
257
+ * import "virtual:luna-shared.css"; // Shared CSS only
258
+ * import "virtual:luna-chunk/todomvc.css"; // Per-directory chunk
259
+ * ```
260
+ */
261
+ export function lunaCss(options: LunaCssPluginOptions = {}): Plugin {
262
+ const {
263
+ src = ["src"],
264
+ mode = "auto",
265
+ threshold = 4096,
266
+ cssFileName = "luna",
267
+ verbose = false,
268
+ split = false,
269
+ sharedThreshold = 3,
270
+ devRuntime,
271
+ injectRuntime = true,
272
+ experimental = {},
273
+ } = options;
274
+
275
+ const {
276
+ optimize: enableOptimize = false,
277
+ optimizeMinFrequency = 2,
278
+ optimizeMaxPatternSize = 5,
279
+ } = experimental;
280
+
281
+ const srcDirs = Array.isArray(src) ? src : [src];
282
+ let config: ResolvedConfig;
283
+ let cachedCss: string | null = null;
284
+ let cachedSplitResult: SplitExtractResult | null = null;
285
+ let cachedMapping: Record<string, string> = {};
286
+ let cachedMergeMap: Map<string, string> | null = null;
287
+ let cachedOptimizedCss: string | null = null;
288
+ let lastExtractTime = 0;
289
+ // Capture cwd at plugin creation time (before Vite changes it)
290
+ const pluginCwd = process.cwd();
291
+
292
+ // Store HTML files for optimization analysis
293
+ let htmlFiles: string[] = [];
294
+
295
+ const log = (msg: string) => {
296
+ if (verbose || enableOptimize) {
297
+ console.log(`[luna-css] ${msg}`);
298
+ }
299
+ };
300
+
301
+ // Resolve source directory path
302
+ const resolveSrcDir = (dir: string): string => {
303
+ // If absolute path, use as-is
304
+ if (path.isAbsolute(dir)) {
305
+ return dir;
306
+ }
307
+ // Resolve relative to the vite config file directory if available,
308
+ // otherwise use the cwd captured at plugin creation time
309
+ const configDir = config.configFile
310
+ ? path.dirname(config.configFile)
311
+ : pluginCwd;
312
+ return path.resolve(configDir, dir);
313
+ };
314
+
315
+ // Extract CSS (combined mode)
316
+ const extractCss = (): { css: string; mapping: Record<string, string> } => {
317
+ const now = Date.now();
318
+ // Cache for 1 second in dev mode
319
+ if (cachedCss && now - lastExtractTime < 1000) {
320
+ return { css: cachedCss, mapping: cachedMapping };
321
+ }
322
+
323
+ let allCss = "";
324
+ const seenRules = new Set<string>();
325
+ const combinedMapping: Record<string, string> = {};
326
+
327
+ for (const dir of srcDirs) {
328
+ const fullPath = resolveSrcDir(dir);
329
+ if (fs.existsSync(fullPath)) {
330
+ const { css, mapping } = extract(fullPath, { warn: false });
331
+ // Merge mapping
332
+ Object.assign(combinedMapping, mapping);
333
+ // Deduplicate rules
334
+ for (const rule of css.split("}")) {
335
+ const trimmed = rule.trim();
336
+ if (trimmed && !seenRules.has(trimmed)) {
337
+ seenRules.add(trimmed);
338
+ allCss += trimmed + "}";
339
+ }
340
+ }
341
+ } else {
342
+ log(`Warning: Source directory not found: ${fullPath}`);
343
+ }
344
+ }
345
+
346
+ cachedCss = allCss;
347
+ cachedMapping = combinedMapping;
348
+ lastExtractTime = now;
349
+ log(`Extracted ${allCss.length} bytes of CSS from ${srcDirs.join(", ")}`);
350
+ return { css: allCss, mapping: combinedMapping };
351
+ };
352
+
353
+ // Find all HTML files in a directory
354
+ const findHtmlFiles = (dir: string): string[] => {
355
+ const files: string[] = [];
356
+ const walk = (currentDir: string) => {
357
+ if (!fs.existsSync(currentDir)) return;
358
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
359
+ for (const entry of entries) {
360
+ const fullPath = path.join(currentDir, entry.name);
361
+ if (entry.isDirectory() && !["node_modules", ".git", "dist", "target"].includes(entry.name)) {
362
+ walk(fullPath);
363
+ } else if (entry.isFile() && entry.name.endsWith(".html")) {
364
+ files.push(fullPath);
365
+ }
366
+ }
367
+ };
368
+ walk(dir);
369
+ return files;
370
+ };
371
+
372
+ // Run CSS optimization based on HTML class usage
373
+ const runOptimization = (): { css: string; mergeMap: Map<string, string> } | null => {
374
+ if (!enableOptimize) {
375
+ return null;
376
+ }
377
+
378
+ // Get CSS and mapping
379
+ const { css, mapping } = extractCss();
380
+ if (!css) {
381
+ return null;
382
+ }
383
+
384
+ // Collect all HTML content from project
385
+ const configDir = config.configFile ? path.dirname(config.configFile) : pluginCwd;
386
+ const htmlFilePaths = findHtmlFiles(configDir);
387
+
388
+ if (htmlFilePaths.length === 0) {
389
+ log("[experimental] No HTML files found for optimization");
390
+ return null;
391
+ }
392
+
393
+ // Read all HTML content
394
+ let combinedHtml = "";
395
+ for (const htmlPath of htmlFilePaths) {
396
+ try {
397
+ combinedHtml += fs.readFileSync(htmlPath, "utf-8");
398
+ } catch (e) {
399
+ // Ignore read errors
400
+ }
401
+ }
402
+
403
+ if (!combinedHtml) {
404
+ return null;
405
+ }
406
+
407
+ log(`[experimental] Running CSS co-occurrence optimization on ${htmlFilePaths.length} HTML file(s)...`);
408
+
409
+ const optimizeResult = optimizeCss(css, combinedHtml, mapping, {
410
+ minFrequency: optimizeMinFrequency,
411
+ maxPatternSize: optimizeMaxPatternSize,
412
+ verbose,
413
+ });
414
+
415
+ if (optimizeResult.patterns.length > 0) {
416
+ log(`[experimental] Merged ${optimizeResult.stats.mergedPatterns} patterns`);
417
+ log(`[experimental] Estimated savings: ${optimizeResult.stats.estimatedBytesSaved} bytes`);
418
+
419
+ for (const pattern of optimizeResult.patterns) {
420
+ log(`[experimental] ${pattern.originalClasses.join(" ")} -> ${pattern.mergedClass} (${pattern.frequency}x)`);
421
+ }
422
+
423
+ return {
424
+ css: optimizeResult.css,
425
+ mergeMap: optimizeResult.mergeMap,
426
+ };
427
+ }
428
+
429
+ log("[experimental] No patterns found for optimization");
430
+ return null;
431
+ };
432
+
433
+ // Get optimized CSS (cached)
434
+ const getOptimizedCss = (): string => {
435
+ // If optimization is disabled, return raw CSS
436
+ if (!enableOptimize) {
437
+ return extractCss().css;
438
+ }
439
+
440
+ // If already computed, return cached
441
+ if (cachedOptimizedCss !== null && cachedMergeMap !== null) {
442
+ return cachedOptimizedCss;
443
+ }
444
+
445
+ // Run optimization
446
+ const result = runOptimization();
447
+ if (result) {
448
+ cachedOptimizedCss = result.css;
449
+ cachedMergeMap = result.mergeMap;
450
+ return result.css;
451
+ }
452
+
453
+ // Fallback to raw CSS
454
+ return extractCss().css;
455
+ };
456
+
457
+ // Extract CSS with splitting
458
+ const extractSplitCss = (): SplitExtractResult => {
459
+ const now = Date.now();
460
+ if (cachedSplitResult && now - lastExtractTime < 1000) {
461
+ return cachedSplitResult;
462
+ }
463
+
464
+ // Combine results from all source directories
465
+ const combinedChunks = new Map<string, { css: string; styles: any }>();
466
+ let combinedSharedCss = "";
467
+ let combinedCss = "";
468
+ const combinedMapping: Record<string, string> = {};
469
+ const combinedStats = new Map<string, { base: number; pseudo: number; media: number }>();
470
+
471
+ for (const dir of srcDirs) {
472
+ const fullPath = resolveSrcDir(dir);
473
+ if (fs.existsSync(fullPath)) {
474
+ const result = extractSplit(fullPath, "dir", {
475
+ warn: false,
476
+ sharedThreshold,
477
+ });
478
+
479
+ // Merge chunks
480
+ for (const [key, chunk] of result.chunks) {
481
+ const prefixedKey = srcDirs.length > 1 ? `${dir}/${key}` : key;
482
+ combinedChunks.set(prefixedKey, chunk);
483
+ }
484
+
485
+ // Merge shared (just concatenate for now)
486
+ if (result.shared.css) {
487
+ combinedSharedCss += result.shared.css;
488
+ }
489
+
490
+ // Merge combined
491
+ combinedCss += result.combined;
492
+
493
+ // Merge mapping
494
+ Object.assign(combinedMapping, result.mapping);
495
+
496
+ // Merge stats
497
+ for (const [key, stat] of result.stats) {
498
+ const prefixedKey = srcDirs.length > 1 ? `${dir}/${key}` : key;
499
+ combinedStats.set(prefixedKey, stat);
500
+ }
501
+ }
502
+ }
503
+
504
+ cachedSplitResult = {
505
+ chunks: combinedChunks,
506
+ shared: { css: combinedSharedCss, styles: { base: new Set(), pseudo: [], media: [] } },
507
+ combined: combinedCss,
508
+ mapping: combinedMapping,
509
+ stats: combinedStats,
510
+ };
511
+ lastExtractTime = now;
512
+
513
+ log(`Split extracted: ${combinedChunks.size} chunks, ${combinedSharedCss.length} bytes shared`);
514
+ return cachedSplitResult;
515
+ };
516
+
517
+ const determineMode = (css: string): "inline" | "external" => {
518
+ if (mode === "auto") {
519
+ return css.length > threshold ? "external" : "inline";
520
+ }
521
+ return mode === "external" ? "external" : "inline";
522
+ };
523
+
524
+ const injectInline = (html: string, css: string): string => {
525
+ const startIdx = html.indexOf(CSS_START_MARKER);
526
+ const endIdx = html.indexOf(CSS_END_MARKER);
527
+
528
+ if (startIdx === -1 || endIdx === -1) {
529
+ log("Warning: CSS markers not found in HTML");
530
+ return html;
531
+ }
532
+
533
+ const before = html.substring(0, startIdx + CSS_START_MARKER.length);
534
+ const after = html.substring(endIdx);
535
+ return `${before}\n ${css}\n ${after}`;
536
+ };
537
+
538
+ return {
539
+ name: "luna-css",
540
+
541
+ configResolved(resolvedConfig) {
542
+ config = resolvedConfig;
543
+ },
544
+
545
+ // Resolve virtual modules
546
+ resolveId(id) {
547
+ if (id === VIRTUAL_CSS_ID) {
548
+ return RESOLVED_VIRTUAL_CSS_ID;
549
+ }
550
+ if (id === VIRTUAL_SHARED_CSS_ID) {
551
+ return RESOLVED_VIRTUAL_SHARED_CSS_ID;
552
+ }
553
+ if (id === VIRTUAL_RUNTIME_ID) {
554
+ return RESOLVED_VIRTUAL_RUNTIME_ID;
555
+ }
556
+ // Support split chunk CSS: virtual:luna-chunk/[chunk-name].css
557
+ if (id.startsWith("virtual:luna-chunk/")) {
558
+ return "\0" + id;
559
+ }
560
+ },
561
+
562
+ // Load virtual modules
563
+ load(id) {
564
+ // Main CSS (all combined)
565
+ if (id === RESOLVED_VIRTUAL_CSS_ID) {
566
+ if (split) {
567
+ const result = extractSplitCss();
568
+ return result.combined;
569
+ }
570
+ // Use optimized CSS in build mode
571
+ const isBuild = config.command === "build";
572
+ if (isBuild && enableOptimize) {
573
+ return getOptimizedCss();
574
+ }
575
+ return extractCss().css;
576
+ }
577
+
578
+ // Shared CSS only (for split mode)
579
+ if (id === RESOLVED_VIRTUAL_SHARED_CSS_ID) {
580
+ if (!split) {
581
+ log("Warning: virtual:luna-shared.css used without split mode");
582
+ return "";
583
+ }
584
+ const result = extractSplitCss();
585
+ return result.shared.css;
586
+ }
587
+
588
+ // Dev runtime (JavaScript)
589
+ if (id === RESOLVED_VIRTUAL_RUNTIME_ID) {
590
+ return generateRuntimeCode();
591
+ }
592
+
593
+ // Split chunk CSS
594
+ if (id.startsWith("\0virtual:luna-chunk/")) {
595
+ const chunkName = id.replace("\0virtual:luna-chunk/", "").replace(".css", "");
596
+ if (!split) {
597
+ log(`Warning: virtual:luna-chunk/${chunkName}.css used without split mode`);
598
+ return "";
599
+ }
600
+ const result = extractSplitCss();
601
+ const chunk = result.chunks.get(chunkName);
602
+ if (chunk) {
603
+ return chunk.css;
604
+ }
605
+ log(`Warning: Chunk "${chunkName}" not found`);
606
+ return "";
607
+ }
608
+ },
609
+
610
+ // Handle HTML transformation
611
+ transformIndexHtml: {
612
+ order: "pre",
613
+ handler(html, ctx) {
614
+ const isBuild = config.command === "build";
615
+ const isDev = !isBuild;
616
+ const useDevRuntime = devRuntime ?? isDev;
617
+ const shouldInjectRuntime = injectRuntime && useDevRuntime && isDev;
618
+
619
+ // In split mode during build, we'll handle CSS differently
620
+ if (split && isBuild) {
621
+ const result = extractSplitCss();
622
+ // For split build, inject shared CSS and add data attributes for chunk loading
623
+ const sharedCss = result.shared.css;
624
+ const combinedCss = result.combined;
625
+
626
+ log(`Split mode build: ${result.chunks.size} chunks, ${sharedCss.length} bytes shared`);
627
+
628
+ // For now, inject combined CSS (full split with lazy loading would need more work)
629
+ return injectInline(html, combinedCss);
630
+ }
631
+
632
+ // Normal mode
633
+ const { css } = extractCss();
634
+ if (!css) {
635
+ // If no CSS and dev runtime enabled, inject runtime loader
636
+ if (shouldInjectRuntime) {
637
+ return injectDevRuntime(html);
638
+ }
639
+ return html;
640
+ }
641
+
642
+ let finalCss = css;
643
+ let finalHtml = html;
644
+
645
+ // Apply experimental optimization if enabled
646
+ if (enableOptimize && isBuild) {
647
+ // Get optimized CSS (this will run optimization and cache it)
648
+ finalCss = getOptimizedCss();
649
+
650
+ // Apply HTML transformation using cached merge map
651
+ if (cachedMergeMap && cachedMergeMap.size > 0) {
652
+ finalHtml = optimizeHtml(html, cachedMergeMap);
653
+ }
654
+ }
655
+
656
+ const actualMode = determineMode(finalCss);
657
+ log(`Mode: ${actualMode}, Build: ${isBuild} (${finalCss.length} bytes)`);
658
+
659
+ let result = injectInline(finalHtml, finalCss);
660
+
661
+ // Inject dev runtime script in development mode
662
+ if (shouldInjectRuntime) {
663
+ result = injectDevRuntime(result);
664
+ }
665
+
666
+ return result;
667
+ },
668
+ },
669
+
670
+ // Serve virtual CSS file in dev mode
671
+ configureServer(server) {
672
+ server.middlewares.use((req, res, next) => {
673
+ if (req.url === `/${cssFileName}.css`) {
674
+ const { css } = extractCss();
675
+ res.setHeader("Content-Type", "text/css");
676
+ res.setHeader("Cache-Control", "no-cache");
677
+ res.end(css);
678
+ return;
679
+ }
680
+ next();
681
+ });
682
+
683
+ // Watch .mbt files for changes
684
+ for (const dir of srcDirs) {
685
+ const fullPath = resolveSrcDir(dir);
686
+ if (fs.existsSync(fullPath)) {
687
+ server.watcher.add(`${fullPath}/**/*.mbt`);
688
+ }
689
+ }
690
+
691
+ server.watcher.on("change", (file) => {
692
+ if (file.endsWith(".mbt")) {
693
+ log(`File changed: ${file}`);
694
+ // Invalidate all caches
695
+ cachedCss = null;
696
+ cachedSplitResult = null;
697
+ cachedMergeMap = null;
698
+ cachedOptimizedCss = null;
699
+
700
+ // Invalidate virtual modules
701
+ const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_CSS_ID);
702
+ if (mod) {
703
+ server.moduleGraph.invalidateModule(mod);
704
+ }
705
+ const sharedMod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_SHARED_CSS_ID);
706
+ if (sharedMod) {
707
+ server.moduleGraph.invalidateModule(sharedMod);
708
+ }
709
+
710
+ // Trigger HMR
711
+ server.ws.send({ type: "full-reload" });
712
+ }
713
+ });
714
+ },
715
+ };
716
+ }
717
+
718
+ export default lunaCss;