@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.
- package/dist/cli.mjs +1264 -27
- package/dist/css/index.d.ts +194 -0
- package/dist/css/index.js +721 -0
- package/dist/css/runtime.d.ts +92 -0
- package/dist/css/runtime.js +179 -0
- package/dist/index.js +1 -1
- package/dist/jsx-dev-runtime.js +1 -1
- package/dist/jsx-runtime.d.ts +5 -0
- package/dist/jsx-runtime.js +1 -1
- package/dist/src-CHiGeWfy.js +1 -0
- package/dist/vite-plugin.d.ts +122 -0
- package/dist/vite-plugin.js +1518 -0
- package/package.json +16 -2
- package/src/css/extract.ts +798 -0
- package/src/css/index.ts +10 -0
- package/src/css/inject.ts +205 -0
- package/src/css/inline.ts +182 -0
- package/src/css/minify.ts +70 -0
- package/src/css/optimizer.ts +6 -0
- package/src/css/runtime.ts +344 -0
- package/src/css-optimizer/README.md +353 -0
- package/src/css-optimizer/cooccurrence.ts +100 -0
- package/src/css-optimizer/core.ts +263 -0
- package/src/css-optimizer/extractors.ts +243 -0
- package/src/css-optimizer/hash.ts +54 -0
- package/src/css-optimizer/index.ts +129 -0
- package/src/css-optimizer/merge.ts +109 -0
- package/src/css-optimizer/moonbit-analyzer.ts +210 -0
- package/src/css-optimizer/parser.ts +120 -0
- package/src/css-optimizer/pattern.ts +171 -0
- package/src/css-optimizer/transformers.ts +301 -0
- package/src/css-optimizer/types.ts +128 -0
- package/src/event-utils.ts +227 -0
- package/src/index.ts +890 -0
- package/src/jsx-dev-runtime.ts +2 -0
- package/src/jsx-runtime.ts +398 -0
- package/src/vite-plugin.ts +718 -0
- package/tests/__screenshots__/context.test.ts/Context-API-context-with-reactive-effects-context-value-accessible-in-effect-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-For-component--SolidJS-style--For-updates-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-accepts-children-as-function-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-toggles-visibility-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-attribute-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-style-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElementNs--SVG-support--createElementNs-with-dynamic-attribute-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-effect-with-DOM-effect-tracks-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-clear-to-empty-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-empty-array-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-removes-items-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-renders-initial-list-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-updates-when-items-change-1.png +0 -0
- 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
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-reordering-in-SVG-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-updates-SVG-elements-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-with-nested-SVG-groups-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-ref-callback--JSX-style--ref-callback-with-nested-elements-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-creates-a-node-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-with-false-condition-creates-placeholder-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-text-nodes-textDyn-creates-reactive-text-node-1.png +0 -0
- 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
- 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
- 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
- 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
- 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
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Context---ForEach-integration-forEach-items-can-access-context-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-renders-initial-list-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-updates-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-with-object-items-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-hides-when-condition-is-false-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-renders-when-condition-is-true-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-from-false-to-true-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-reactively-1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--event-listener-pattern--Solid-js-docs-example--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--multiple-cleanups-in-component-body--LIFO-order--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-in-component-body-runs-on-unmount-1.png +0 -0
- 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
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--timer-cleanup-pattern--Solid-js-style--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Effects-effect-cleanup-runs-before-re-run-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-large-list-update-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-nested-batch-operations-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-rapid-sequential-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-Show-component---visible-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-show-hide-element---visible-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-fragment-with-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-nested-fragments-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-simple-fragment-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-conditional-toggle-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-addition-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-removal-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-text-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-className-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-style-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-multiple-dynamic-attributes-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-deeply-nested-conditionals-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-empty-to-populated-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-populated-to-empty-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-cleanup-order-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-with-inner-signal-change-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-is-called-when-effect-re-runs-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-with-resource-simulation-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-multiple-children--no-wrapper--1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-no-children-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-fragment-with-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-nested-Fragments-work-correctly-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-complex-reordering-with-additions-and-removals-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-insert-in-middle-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-remove-from-middle-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-reverse-list-order-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-shuffle-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-Show-component-renders-when-condition-is-true-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-renders-content-when-initially-true-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-toggles-visibility-dynamically-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Memo-Dependency-Chain-conditional-memo-dependencies-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-basic-signal-get-set-produces-same-values-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-batch-updates-produce-same-final-values-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-peek-reads-value-without-tracking-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-selective-tracking-with-untrack-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-untrack-prevents-dependency-tracking-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API--SolidJS-style--reactivity-accessor-is-reactive-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-empty-string-for-non-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-undefined-for-non-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsFailure-and-stateError-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsPending-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsSuccess-and-stateValue-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateValue-returns-undefined-for-non-success-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-reject-transitions-to-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-resolve-transitions-to-success-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-returns-resource--resolve--and-reject-functions-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-starts-in-pending-state-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-async-resolve-works-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-starts-in-pending-state-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-failure-on-reject-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-success-on-resolve-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-can-wrap-fetch-like-async-operations-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-works-with-setTimeout-simulation-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourceGet-tracks-dependencies-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourcePeek-does-not-track-dependencies-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceRefetch-refetch-resets-to-pending-and-re-runs-fetcher-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-children-to-body-by-default-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-to-selector-mount-target-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/SolidJS-API-compatibility-createEffect-tracks-dependencies-automatically-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-accessor-condition-in-Match-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-multiple-Match-components-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-single-Match-and-fallback-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-components-Switch-updates-DOM-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-multiple-dependencies-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-single-dependency-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-with-defer-option-skips-initial-run-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Arrays-array-updates-work-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-only-triggers-when-accessed-property-changes-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-parent-path-change-notifies-child-accessors-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-nested-property-access-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-property-access-in-effects-1.png +0 -0
- package/tests/context.test.ts +118 -0
- package/tests/css-optimizer-extractors.test.ts +264 -0
- package/tests/css-optimizer-integration.test.ts +566 -0
- package/tests/css-optimizer-transformers.test.ts +301 -0
- package/tests/css-optimizer.test.ts +646 -0
- package/tests/css-runtime.bench.ts +442 -0
- package/tests/css-runtime.test.ts +342 -0
- package/tests/dom.test.ts +872 -0
- package/tests/integration.test.ts +405 -0
- package/tests/issue-5-for-infinite-loop.test.ts +516 -0
- package/tests/jsx-runtime.test.tsx +393 -0
- package/tests/lifecycle.test.ts +833 -0
- package/tests/move-before.bench.ts +304 -0
- package/tests/preact-signals-comparison.test.ts +1608 -0
- package/tests/resource.test.ts +160 -0
- package/tests/router.test.ts +117 -0
- package/tests/show-initial-mount-leak.test.tsx +182 -0
- package/tests/solidjs-api.test.ts +659 -0
- package/tests/static-perf.bench.ts +64 -0
- package/tests/store.test.ts +263 -0
- package/tests/tsx-syntax.test.tsx +404 -0
- 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
|
+
}
|