@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,798 @@
1
+ /**
2
+ * Static CSS Extractor for Luna CSS Module
3
+ *
4
+ * Extracts all CSS declarations from .mbt files at build time.
5
+ * This ensures all styles are collected regardless of runtime branches.
6
+ */
7
+
8
+ import fs from "fs";
9
+ import path from "path";
10
+
11
+ // =============================================================================
12
+ // Pattern Definitions
13
+ // =============================================================================
14
+
15
+ // css("property", "value")
16
+ const CSS_PATTERN = /\bcss\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
17
+
18
+ // styles([("property", "value"), ...])
19
+ const STYLES_PATTERN = /\bstyles\s*\(\s*\[([\s\S]*?)\]\s*\)/g;
20
+ const STYLES_PAIR_PATTERN = /\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
21
+
22
+ // hover("property", "value"), focus(...), active(...)
23
+ const HOVER_PATTERN = /\bhover\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
24
+ const FOCUS_PATTERN = /\bfocus\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
25
+ const ACTIVE_PATTERN = /\bactive\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
26
+
27
+ // on(":pseudo", "property", "value")
28
+ const ON_PATTERN =
29
+ /\bon\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
30
+
31
+ // media("condition", "property", "value")
32
+ const MEDIA_PATTERN =
33
+ /\bmedia\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
34
+
35
+ // at_sm, at_md, at_lg, at_xl("property", "value")
36
+ const AT_SM_PATTERN = /\bat_sm\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
37
+ const AT_MD_PATTERN = /\bat_md\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
38
+ const AT_LG_PATTERN = /\bat_lg\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
39
+ const AT_XL_PATTERN = /\bat_xl\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
40
+
41
+ // dark("property", "value")
42
+ const DARK_PATTERN = /\bdark\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
43
+
44
+ // ucss, ustyles, uhover, etc. (re-exports from static_dom)
45
+ const UCSS_PATTERN = /\bucss\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
46
+ const USTYLES_PATTERN = /\bustyles\s*\(\s*\[([\s\S]*?)\]\s*\)/g;
47
+ const UHOVER_PATTERN = /\buhover\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
48
+ const UFOCUS_PATTERN = /\bufocus\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
49
+ const UACTIVE_PATTERN = /\buactive\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
50
+ const UON_PATTERN =
51
+ /\buon\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
52
+ const UAT_MD_PATTERN = /\buat_md\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
53
+ const UAT_LG_PATTERN = /\buat_lg\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
54
+ const UDARK_PATTERN = /\budark\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
55
+
56
+ // =============================================================================
57
+ // Warning Detection Patterns
58
+ // =============================================================================
59
+
60
+ const ARG_PATTERN = `(?:"(?:[^"\\\\]|\\\\.)*"|[^,)]+)`;
61
+
62
+ const WARN_PATTERNS = [
63
+ {
64
+ name: "css",
65
+ pattern: new RegExp(
66
+ `\\b(u?css)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`,
67
+ "g"
68
+ ),
69
+ args: 2,
70
+ },
71
+ {
72
+ name: "hover",
73
+ pattern: new RegExp(
74
+ `\\b(u?hover)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`,
75
+ "g"
76
+ ),
77
+ args: 2,
78
+ },
79
+ {
80
+ name: "focus",
81
+ pattern: new RegExp(
82
+ `\\b(u?focus)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`,
83
+ "g"
84
+ ),
85
+ args: 2,
86
+ },
87
+ {
88
+ name: "active",
89
+ pattern: new RegExp(
90
+ `\\b(u?active)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`,
91
+ "g"
92
+ ),
93
+ args: 2,
94
+ },
95
+ {
96
+ name: "at_sm",
97
+ pattern: new RegExp(
98
+ `\\bat_sm\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`,
99
+ "g"
100
+ ),
101
+ args: 2,
102
+ noPrefix: true,
103
+ },
104
+ {
105
+ name: "at_md",
106
+ pattern: new RegExp(
107
+ `\\b(u?at_md)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`,
108
+ "g"
109
+ ),
110
+ args: 2,
111
+ },
112
+ {
113
+ name: "at_lg",
114
+ pattern: new RegExp(
115
+ `\\b(u?at_lg)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`,
116
+ "g"
117
+ ),
118
+ args: 2,
119
+ },
120
+ {
121
+ name: "at_xl",
122
+ pattern: new RegExp(
123
+ `\\bat_xl\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`,
124
+ "g"
125
+ ),
126
+ args: 2,
127
+ noPrefix: true,
128
+ },
129
+ {
130
+ name: "dark",
131
+ pattern: new RegExp(
132
+ `\\b(u?dark)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`,
133
+ "g"
134
+ ),
135
+ args: 2,
136
+ },
137
+ {
138
+ name: "on",
139
+ pattern: new RegExp(
140
+ `\\b(u?on)\\s*\\(\\s*"(::?[^"]+)"\\s*,\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`,
141
+ "g"
142
+ ),
143
+ args: 3,
144
+ pseudoInMatch: true,
145
+ },
146
+ {
147
+ name: "media",
148
+ pattern: new RegExp(
149
+ `\\bmedia\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`,
150
+ "g"
151
+ ),
152
+ args: 3,
153
+ noPrefix: true,
154
+ },
155
+ ];
156
+
157
+ // =============================================================================
158
+ // Types
159
+ // =============================================================================
160
+
161
+ export interface Warning {
162
+ file: string;
163
+ line: number;
164
+ func: string;
165
+ code: string;
166
+ reason: string;
167
+ }
168
+
169
+ export interface ExtractedStyles {
170
+ base: Set<string>;
171
+ pseudo: Array<{ pseudo: string; property: string; value: string }>;
172
+ media: Array<{ condition: string; property: string; value: string }>;
173
+ }
174
+
175
+ export interface ExtractOptions {
176
+ pretty?: boolean;
177
+ json?: boolean;
178
+ verbose?: boolean;
179
+ warn?: boolean;
180
+ strict?: boolean;
181
+ splitMode?: "file" | "dir" | null;
182
+ }
183
+
184
+ export interface ExtractResult {
185
+ css: string;
186
+ mapping: Record<string, string>;
187
+ stats: {
188
+ base: number;
189
+ pseudo: number;
190
+ media: number;
191
+ };
192
+ warnings?: Warning[];
193
+ }
194
+
195
+ export interface SplitExtractResult {
196
+ /** Per-file or per-dir CSS */
197
+ chunks: Map<string, { css: string; styles: ExtractedStyles }>;
198
+ /** Shared CSS (declarations used in 3+ files/dirs) */
199
+ shared: { css: string; styles: ExtractedStyles };
200
+ /** All CSS combined (for comparison) */
201
+ combined: string;
202
+ /** Class name mapping */
203
+ mapping: Record<string, string>;
204
+ /** Statistics per chunk */
205
+ stats: Map<string, { base: number; pseudo: number; media: number }>;
206
+ warnings?: Warning[];
207
+ }
208
+
209
+ // =============================================================================
210
+ // Warning Detection
211
+ // =============================================================================
212
+
213
+ function isStringLiteral(str: string): boolean {
214
+ const trimmed = str.trim();
215
+ return (
216
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
217
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
218
+ );
219
+ }
220
+
221
+ function isFunctionDefinition(code: string): boolean {
222
+ return /:\s*String/.test(code);
223
+ }
224
+
225
+ function getLineNumber(content: string, position: number): number {
226
+ return content.substring(0, position).split("\n").length;
227
+ }
228
+
229
+ export function detectWarnings(content: string, filePath: string): Warning[] {
230
+ const warnings: Warning[] = [];
231
+
232
+ for (const { name, pattern, args, noPrefix, pseudoInMatch } of WARN_PATTERNS) {
233
+ pattern.lastIndex = 0;
234
+
235
+ for (const match of content.matchAll(pattern)) {
236
+ const fullMatch = match[0];
237
+ const position = match.index!;
238
+ const line = getLineNumber(content, position);
239
+
240
+ if (isFunctionDefinition(fullMatch)) {
241
+ continue;
242
+ }
243
+
244
+ const extractedArgs: string[] = [];
245
+ if (args === 2) {
246
+ const startIdx = noPrefix ? 1 : 2;
247
+ extractedArgs.push(match[startIdx], match[startIdx + 1]);
248
+ } else if (args === 3) {
249
+ if (pseudoInMatch) {
250
+ extractedArgs.push(match[3], match[4]);
251
+ } else {
252
+ const startIdx = noPrefix ? 1 : 2;
253
+ extractedArgs.push(
254
+ match[startIdx],
255
+ match[startIdx + 1],
256
+ match[startIdx + 2]
257
+ );
258
+ }
259
+ }
260
+
261
+ for (let i = 0; i < extractedArgs.length; i++) {
262
+ const arg = extractedArgs[i];
263
+ if (arg && !isStringLiteral(arg)) {
264
+ const argNum = pseudoInMatch ? i + 2 : i + 1;
265
+ warnings.push({
266
+ file: filePath,
267
+ line,
268
+ func: name,
269
+ code: fullMatch.trim(),
270
+ reason: `Argument ${argNum} is not a string literal: ${arg.trim()}`,
271
+ });
272
+ break;
273
+ }
274
+ }
275
+ }
276
+ }
277
+
278
+ return warnings;
279
+ }
280
+
281
+ // =============================================================================
282
+ // Extraction Logic
283
+ // =============================================================================
284
+
285
+ export function extractFromContent(content: string): ExtractedStyles {
286
+ const base = new Set<string>();
287
+ const pseudo: ExtractedStyles["pseudo"] = [];
288
+ const media: ExtractedStyles["media"] = [];
289
+
290
+ function extractBase(pattern: RegExp) {
291
+ for (const match of content.matchAll(pattern)) {
292
+ base.add(`${match[1]}:${match[2]}`);
293
+ }
294
+ }
295
+
296
+ function extractStyles(pattern: RegExp) {
297
+ for (const match of content.matchAll(pattern)) {
298
+ const pairs = match[1];
299
+ for (const pair of pairs.matchAll(STYLES_PAIR_PATTERN)) {
300
+ base.add(`${pair[1]}:${pair[2]}`);
301
+ }
302
+ }
303
+ }
304
+
305
+ function extractPseudo(pattern: RegExp, pseudoSelector: string) {
306
+ for (const match of content.matchAll(pattern)) {
307
+ pseudo.push({
308
+ pseudo: pseudoSelector,
309
+ property: match[1],
310
+ value: match[2],
311
+ });
312
+ }
313
+ }
314
+
315
+ function extractOn(pattern: RegExp) {
316
+ for (const match of content.matchAll(pattern)) {
317
+ pseudo.push({
318
+ pseudo: match[1],
319
+ property: match[2],
320
+ value: match[3],
321
+ });
322
+ }
323
+ }
324
+
325
+ function extractMedia(pattern: RegExp, condition: string | null = null) {
326
+ for (const match of content.matchAll(pattern)) {
327
+ if (condition) {
328
+ media.push({
329
+ condition,
330
+ property: match[1],
331
+ value: match[2],
332
+ });
333
+ } else {
334
+ media.push({
335
+ condition: match[1],
336
+ property: match[2],
337
+ value: match[3],
338
+ });
339
+ }
340
+ }
341
+ }
342
+
343
+ // Extract base styles
344
+ extractBase(CSS_PATTERN);
345
+ extractBase(UCSS_PATTERN);
346
+ extractStyles(STYLES_PATTERN);
347
+ extractStyles(USTYLES_PATTERN);
348
+
349
+ // Extract pseudo-classes
350
+ extractPseudo(HOVER_PATTERN, ":hover");
351
+ extractPseudo(UHOVER_PATTERN, ":hover");
352
+ extractPseudo(FOCUS_PATTERN, ":focus");
353
+ extractPseudo(UFOCUS_PATTERN, ":focus");
354
+ extractPseudo(ACTIVE_PATTERN, ":active");
355
+ extractPseudo(UACTIVE_PATTERN, ":active");
356
+ extractOn(ON_PATTERN);
357
+ extractOn(UON_PATTERN);
358
+
359
+ // Extract media queries
360
+ extractMedia(MEDIA_PATTERN);
361
+ extractMedia(AT_SM_PATTERN, "min-width:640px");
362
+ extractMedia(AT_MD_PATTERN, "min-width:768px");
363
+ extractMedia(UAT_MD_PATTERN, "min-width:768px");
364
+ extractMedia(AT_LG_PATTERN, "min-width:1024px");
365
+ extractMedia(UAT_LG_PATTERN, "min-width:1024px");
366
+ extractMedia(AT_XL_PATTERN, "min-width:1280px");
367
+ extractMedia(DARK_PATTERN, "prefers-color-scheme:dark");
368
+ extractMedia(UDARK_PATTERN, "prefers-color-scheme:dark");
369
+
370
+ return { base, pseudo, media };
371
+ }
372
+
373
+ export function findMbtFiles(dir: string): string[] {
374
+ const files: string[] = [];
375
+
376
+ function walk(currentDir: string) {
377
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
378
+ for (const entry of entries) {
379
+ const fullPath = path.join(currentDir, entry.name);
380
+ if (entry.isDirectory()) {
381
+ if (
382
+ !["node_modules", ".git", "target", ".mooncakes"].includes(entry.name)
383
+ ) {
384
+ walk(fullPath);
385
+ }
386
+ } else if (entry.isFile() && entry.name.endsWith(".mbt")) {
387
+ files.push(fullPath);
388
+ }
389
+ }
390
+ }
391
+
392
+ walk(dir);
393
+ return files;
394
+ }
395
+
396
+ // =============================================================================
397
+ // CSS Generation - Hash-based class names
398
+ // =============================================================================
399
+
400
+ /**
401
+ * DJB2 hash function - must match MoonBit registry.mbt implementation
402
+ */
403
+ function djb2Hash(s: string): number {
404
+ let hash = 5381;
405
+ for (let i = 0; i < s.length; i++) {
406
+ const c = s.charCodeAt(i);
407
+ // hash * 33 + c (using unsigned 32-bit arithmetic)
408
+ hash = ((hash << 5) + hash + c) >>> 0;
409
+ }
410
+ return hash;
411
+ }
412
+
413
+ /**
414
+ * Convert hash to base36 string (0-9a-z)
415
+ * Takes lower 24 bits to keep class names short (4-5 chars)
416
+ */
417
+ function toBase36(n: number): string {
418
+ const chars = "0123456789abcdefghijklmnopqrstuvwxyz";
419
+ // Take lower 24 bits
420
+ n = n & 0xffffff;
421
+ if (n === 0) return "0";
422
+ let result = "";
423
+ while (n > 0) {
424
+ result = chars[n % 36] + result;
425
+ n = Math.floor(n / 36);
426
+ }
427
+ return result;
428
+ }
429
+
430
+ /**
431
+ * Generate class name from declaration hash
432
+ */
433
+ function hashClassName(decl: string): string {
434
+ const hash = djb2Hash(decl);
435
+ return "_" + toBase36(hash);
436
+ }
437
+
438
+ export function generateCSS(
439
+ styles: ExtractedStyles,
440
+ options: { pretty?: boolean } = {}
441
+ ): { css: string; mapping: Record<string, string> } {
442
+ const { pretty = false } = options;
443
+ const mapping: Record<string, string> = {};
444
+ const parts: string[] = [];
445
+
446
+ // Base styles - use hash-based class names for deterministic output
447
+ for (const decl of styles.base) {
448
+ const cls = hashClassName(decl);
449
+ mapping[decl] = cls;
450
+ if (pretty) {
451
+ parts.push(`.${cls} { ${decl} }`);
452
+ } else {
453
+ parts.push(`.${cls}{${decl}}`);
454
+ }
455
+ }
456
+
457
+ // Pseudo-class styles with hash-based class names
458
+ for (const { pseudo, property, value } of styles.pseudo) {
459
+ // Include pseudo in the hash key for uniqueness
460
+ const hashKey = `${pseudo}:${property}:${value}`;
461
+ const cls = hashClassName(hashKey);
462
+ mapping[hashKey] = cls;
463
+ if (pretty) {
464
+ parts.push(`.${cls}${pseudo} { ${property}: ${value} }`);
465
+ } else {
466
+ parts.push(`.${cls}${pseudo}{${property}:${value}}`);
467
+ }
468
+ }
469
+
470
+ // Media query styles (grouped by condition) with hash-based class names
471
+ const mediaGroups = new Map<
472
+ string,
473
+ Array<{ property: string; value: string }>
474
+ >();
475
+ for (const { condition, property, value } of styles.media) {
476
+ if (!mediaGroups.has(condition)) {
477
+ mediaGroups.set(condition, []);
478
+ }
479
+ mediaGroups.get(condition)!.push({ property, value });
480
+ }
481
+
482
+ for (const [condition, declarations] of mediaGroups) {
483
+ const rules: string[] = [];
484
+ for (const { property, value } of declarations) {
485
+ const hashKey = `@media(${condition}):${property}:${value}`;
486
+ const cls = hashClassName(hashKey);
487
+ mapping[hashKey] = cls;
488
+ if (pretty) {
489
+ rules.push(` .${cls} { ${property}: ${value} }`);
490
+ } else {
491
+ rules.push(`.${cls}{${property}:${value}}`);
492
+ }
493
+ }
494
+ if (pretty) {
495
+ parts.push(`@media (${condition}) {\n${rules.join("\n")}\n}`);
496
+ } else {
497
+ parts.push(`@media(${condition}){${rules.join("")}}`);
498
+ }
499
+ }
500
+
501
+ const separator = pretty ? "\n" : "";
502
+ return {
503
+ css: parts.join(separator),
504
+ mapping,
505
+ };
506
+ }
507
+
508
+ // =============================================================================
509
+ // Main Extract Function
510
+ // =============================================================================
511
+
512
+ export function extract(dir: string, options: ExtractOptions = {}): ExtractResult {
513
+ const { pretty = false, warn = true, strict = false, verbose = false } = options;
514
+
515
+ const files = findMbtFiles(dir);
516
+ if (verbose) {
517
+ console.error(`Found ${files.length} .mbt files`);
518
+ }
519
+
520
+ const combined: ExtractedStyles = {
521
+ base: new Set(),
522
+ pseudo: [],
523
+ media: [],
524
+ };
525
+ const allWarnings: Warning[] = [];
526
+
527
+ for (const file of files) {
528
+ const content = fs.readFileSync(file, "utf-8");
529
+ const extracted = extractFromContent(content);
530
+
531
+ for (const decl of extracted.base) {
532
+ combined.base.add(decl);
533
+ }
534
+ combined.pseudo.push(...extracted.pseudo);
535
+ combined.media.push(...extracted.media);
536
+
537
+ if (warn) {
538
+ const warnings = detectWarnings(content, file);
539
+ allWarnings.push(...warnings);
540
+ }
541
+ }
542
+
543
+ if (warn && allWarnings.length > 0 && verbose) {
544
+ console.error(
545
+ `\n⚠️ ${allWarnings.length} warning(s): non-literal CSS arguments detected\n`
546
+ );
547
+ for (const w of allWarnings) {
548
+ console.error(` ${w.file}:${w.line}`);
549
+ console.error(` ${w.func}(...) - ${w.reason}`);
550
+ console.error(` Code: ${w.code}`);
551
+ console.error("");
552
+ }
553
+
554
+ if (strict) {
555
+ throw new Error("Strict mode: non-literal CSS arguments detected");
556
+ }
557
+ }
558
+
559
+ if (verbose) {
560
+ console.error(`Extracted:`);
561
+ console.error(` - ${combined.base.size} base declarations`);
562
+ console.error(` - ${combined.pseudo.length} pseudo-class declarations`);
563
+ console.error(` - ${combined.media.length} media query declarations`);
564
+ }
565
+
566
+ const { css, mapping } = generateCSS(combined, { pretty });
567
+
568
+ return {
569
+ css,
570
+ mapping,
571
+ stats: {
572
+ base: combined.base.size,
573
+ pseudo: combined.pseudo.length,
574
+ media: combined.media.length,
575
+ },
576
+ warnings: warn ? allWarnings : undefined,
577
+ };
578
+ }
579
+
580
+ // =============================================================================
581
+ // Split Extraction (per-file or per-directory)
582
+ // =============================================================================
583
+
584
+ interface SplitExtractOptions extends ExtractOptions {
585
+ /** Minimum usage count to be considered "shared" (default: 3) */
586
+ sharedThreshold?: number;
587
+ }
588
+
589
+ /**
590
+ * Extract CSS with split output by file or directory.
591
+ * Shared CSS (used in 3+ files/dirs) is separated into a shared chunk.
592
+ */
593
+ export function extractSplit(
594
+ dir: string,
595
+ mode: "file" | "dir",
596
+ options: SplitExtractOptions = {}
597
+ ): SplitExtractResult {
598
+ const {
599
+ pretty = false,
600
+ warn = true,
601
+ strict = false,
602
+ verbose = false,
603
+ sharedThreshold = 3,
604
+ } = options;
605
+
606
+ const files = findMbtFiles(dir);
607
+ if (verbose) {
608
+ console.error(`Found ${files.length} .mbt files`);
609
+ }
610
+
611
+ // Track declarations per file/dir
612
+ const declUsage = new Map<string, Set<string>>(); // decl -> Set<chunk_key>
613
+ const pseudoUsage = new Map<string, Set<string>>(); // key -> Set<chunk_key>
614
+ const mediaUsage = new Map<string, Set<string>>(); // key -> Set<chunk_key>
615
+
616
+ // Per-chunk extracted styles
617
+ const perChunk = new Map<string, ExtractedStyles>();
618
+ const allWarnings: Warning[] = [];
619
+
620
+ for (const file of files) {
621
+ const content = fs.readFileSync(file, "utf-8");
622
+ const extracted = extractFromContent(content);
623
+
624
+ // Determine chunk key based on mode
625
+ const relPath = path.relative(dir, file);
626
+ const chunkKey = mode === "file" ? relPath : path.dirname(relPath) || ".";
627
+
628
+ // Initialize chunk if needed
629
+ if (!perChunk.has(chunkKey)) {
630
+ perChunk.set(chunkKey, { base: new Set(), pseudo: [], media: [] });
631
+ }
632
+ const chunk = perChunk.get(chunkKey)!;
633
+
634
+ // Track base declarations
635
+ for (const decl of extracted.base) {
636
+ chunk.base.add(decl);
637
+ if (!declUsage.has(decl)) {
638
+ declUsage.set(decl, new Set());
639
+ }
640
+ declUsage.get(decl)!.add(chunkKey);
641
+ }
642
+
643
+ // Track pseudo declarations
644
+ for (const p of extracted.pseudo) {
645
+ const key = `${p.pseudo}:${p.property}:${p.value}`;
646
+ chunk.pseudo.push(p);
647
+ if (!pseudoUsage.has(key)) {
648
+ pseudoUsage.set(key, new Set());
649
+ }
650
+ pseudoUsage.get(key)!.add(chunkKey);
651
+ }
652
+
653
+ // Track media declarations
654
+ for (const m of extracted.media) {
655
+ const key = `@media(${m.condition}):${m.property}:${m.value}`;
656
+ chunk.media.push(m);
657
+ if (!mediaUsage.has(key)) {
658
+ mediaUsage.set(key, new Set());
659
+ }
660
+ mediaUsage.get(key)!.add(chunkKey);
661
+ }
662
+
663
+ if (warn) {
664
+ const warnings = detectWarnings(content, file);
665
+ allWarnings.push(...warnings);
666
+ }
667
+ }
668
+
669
+ // Separate shared vs chunk-specific declarations
670
+ const sharedBase = new Set<string>();
671
+ const sharedPseudo: ExtractedStyles["pseudo"] = [];
672
+ const sharedMedia: ExtractedStyles["media"] = [];
673
+
674
+ // Find shared base declarations
675
+ for (const [decl, chunks] of declUsage) {
676
+ if (chunks.size >= sharedThreshold) {
677
+ sharedBase.add(decl);
678
+ }
679
+ }
680
+
681
+ // Find shared pseudo declarations
682
+ const seenPseudo = new Set<string>();
683
+ for (const [key, chunks] of pseudoUsage) {
684
+ if (chunks.size >= sharedThreshold && !seenPseudo.has(key)) {
685
+ seenPseudo.add(key);
686
+ const [pseudo, property, value] = key.split(":");
687
+ sharedPseudo.push({ pseudo, property, value });
688
+ }
689
+ }
690
+
691
+ // Find shared media declarations
692
+ const seenMedia = new Set<string>();
693
+ for (const [key, chunks] of mediaUsage) {
694
+ if (chunks.size >= sharedThreshold && !seenMedia.has(key)) {
695
+ seenMedia.add(key);
696
+ // Parse @media(condition):property:value
697
+ const match = key.match(/^@media\(([^)]+)\):([^:]+):(.+)$/);
698
+ if (match) {
699
+ sharedMedia.push({
700
+ condition: match[1],
701
+ property: match[2],
702
+ value: match[3],
703
+ });
704
+ }
705
+ }
706
+ }
707
+
708
+ // Remove shared declarations from individual chunks
709
+ const finalChunks = new Map<string, { css: string; styles: ExtractedStyles }>();
710
+ const chunkStats = new Map<string, { base: number; pseudo: number; media: number }>();
711
+
712
+ for (const [chunkKey, chunk] of perChunk) {
713
+ // Filter out shared base declarations
714
+ const chunkOnlyBase = new Set<string>();
715
+ for (const decl of chunk.base) {
716
+ if (!sharedBase.has(decl)) {
717
+ chunkOnlyBase.add(decl);
718
+ }
719
+ }
720
+
721
+ // Filter out shared pseudo declarations
722
+ const chunkOnlyPseudo = chunk.pseudo.filter((p) => {
723
+ const key = `${p.pseudo}:${p.property}:${p.value}`;
724
+ return !seenPseudo.has(key);
725
+ });
726
+
727
+ // Filter out shared media declarations
728
+ const chunkOnlyMedia = chunk.media.filter((m) => {
729
+ const key = `@media(${m.condition}):${m.property}:${m.value}`;
730
+ return !seenMedia.has(key);
731
+ });
732
+
733
+ const chunkStyles: ExtractedStyles = {
734
+ base: chunkOnlyBase,
735
+ pseudo: chunkOnlyPseudo,
736
+ media: chunkOnlyMedia,
737
+ };
738
+
739
+ const { css } = generateCSS(chunkStyles, { pretty });
740
+ finalChunks.set(chunkKey, { css, styles: chunkStyles });
741
+ chunkStats.set(chunkKey, {
742
+ base: chunkOnlyBase.size,
743
+ pseudo: chunkOnlyPseudo.length,
744
+ media: chunkOnlyMedia.length,
745
+ });
746
+ }
747
+
748
+ // Generate shared CSS
749
+ const sharedStyles: ExtractedStyles = {
750
+ base: sharedBase,
751
+ pseudo: sharedPseudo,
752
+ media: sharedMedia,
753
+ };
754
+ const { css: sharedCss, mapping } = generateCSS(sharedStyles, { pretty });
755
+
756
+ // Generate combined CSS for all chunks + shared
757
+ const combinedStyles: ExtractedStyles = {
758
+ base: new Set<string>(),
759
+ pseudo: [],
760
+ media: [],
761
+ };
762
+ for (const decl of sharedBase) combinedStyles.base.add(decl);
763
+ for (const [, chunk] of finalChunks) {
764
+ for (const decl of chunk.styles.base) combinedStyles.base.add(decl);
765
+ combinedStyles.pseudo.push(...chunk.styles.pseudo);
766
+ combinedStyles.media.push(...chunk.styles.media);
767
+ }
768
+ combinedStyles.pseudo.push(...sharedPseudo);
769
+ combinedStyles.media.push(...sharedMedia);
770
+
771
+ const { css: combinedCss, mapping: fullMapping } = generateCSS(combinedStyles, { pretty });
772
+
773
+ if (verbose) {
774
+ console.error(`Split mode: ${mode}`);
775
+ console.error(`Chunks: ${finalChunks.size}`);
776
+ console.error(`Shared declarations (≥${sharedThreshold} usages):`);
777
+ console.error(` - ${sharedBase.size} base`);
778
+ console.error(` - ${sharedPseudo.length} pseudo`);
779
+ console.error(` - ${sharedMedia.length} media`);
780
+ }
781
+
782
+ if (warn && allWarnings.length > 0 && verbose) {
783
+ console.error(`\n⚠️ ${allWarnings.length} warning(s)\n`);
784
+ }
785
+
786
+ if (strict && allWarnings.length > 0) {
787
+ throw new Error("Strict mode: non-literal CSS arguments detected");
788
+ }
789
+
790
+ return {
791
+ chunks: finalChunks,
792
+ shared: { css: sharedCss, styles: sharedStyles },
793
+ combined: combinedCss,
794
+ mapping: fullMapping,
795
+ stats: chunkStats,
796
+ warnings: warn ? allWarnings : undefined,
797
+ };
798
+ }