@luna_ui/luna 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-CyEkcO3_.d.ts → index-CDWzWF-h.d.ts} +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/jsx-dev-runtime.js +1 -1
- package/dist/jsx-runtime.d.ts +1 -1
- package/dist/jsx-runtime.js +1 -1
- package/dist/src-DEjrAhrg.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/hydration/createHydrator.ts +62 -0
- package/src/hydration/delegate.ts +62 -0
- package/src/hydration/drag.ts +214 -0
- package/src/hydration/index.ts +12 -0
- package/src/hydration/keyboard.ts +64 -0
- package/src/hydration/toggle.ts +101 -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-BDdxGwvq.js +0 -1
|
@@ -0,0 +1,1518 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
//#region src/css/extract.ts
|
|
5
|
+
/**
|
|
6
|
+
* Static CSS Extractor for Luna CSS Module
|
|
7
|
+
*
|
|
8
|
+
* Extracts all CSS declarations from .mbt files at build time.
|
|
9
|
+
* This ensures all styles are collected regardless of runtime branches.
|
|
10
|
+
*/
|
|
11
|
+
const CSS_PATTERN = /\bcss\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
12
|
+
const STYLES_PATTERN = /\bstyles\s*\(\s*\[([\s\S]*?)\]\s*\)/g;
|
|
13
|
+
const STYLES_PAIR_PATTERN = /\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
14
|
+
const HOVER_PATTERN = /\bhover\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
15
|
+
const FOCUS_PATTERN = /\bfocus\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
16
|
+
const ACTIVE_PATTERN = /\bactive\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
17
|
+
const ON_PATTERN = /\bon\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
18
|
+
const MEDIA_PATTERN = /\bmedia\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
19
|
+
const AT_SM_PATTERN = /\bat_sm\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
20
|
+
const AT_MD_PATTERN = /\bat_md\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
21
|
+
const AT_LG_PATTERN = /\bat_lg\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
22
|
+
const AT_XL_PATTERN = /\bat_xl\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
23
|
+
const DARK_PATTERN = /\bdark\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
24
|
+
const UCSS_PATTERN = /\bucss\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
25
|
+
const USTYLES_PATTERN = /\bustyles\s*\(\s*\[([\s\S]*?)\]\s*\)/g;
|
|
26
|
+
const UHOVER_PATTERN = /\buhover\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
27
|
+
const UFOCUS_PATTERN = /\bufocus\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
28
|
+
const UACTIVE_PATTERN = /\buactive\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
29
|
+
const UON_PATTERN = /\buon\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
30
|
+
const UAT_MD_PATTERN = /\buat_md\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
31
|
+
const UAT_LG_PATTERN = /\buat_lg\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
32
|
+
const UDARK_PATTERN = /\budark\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
33
|
+
const ARG_PATTERN = `(?:"(?:[^"\\\\]|\\\\.)*"|[^,)]+)`;
|
|
34
|
+
const WARN_PATTERNS = [
|
|
35
|
+
{
|
|
36
|
+
name: "css",
|
|
37
|
+
pattern: new RegExp(`\\b(u?css)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
38
|
+
args: 2
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "hover",
|
|
42
|
+
pattern: new RegExp(`\\b(u?hover)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
43
|
+
args: 2
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "focus",
|
|
47
|
+
pattern: new RegExp(`\\b(u?focus)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
48
|
+
args: 2
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "active",
|
|
52
|
+
pattern: new RegExp(`\\b(u?active)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
53
|
+
args: 2
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "at_sm",
|
|
57
|
+
pattern: new RegExp(`\\bat_sm\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
58
|
+
args: 2,
|
|
59
|
+
noPrefix: true
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "at_md",
|
|
63
|
+
pattern: new RegExp(`\\b(u?at_md)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
64
|
+
args: 2
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "at_lg",
|
|
68
|
+
pattern: new RegExp(`\\b(u?at_lg)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
69
|
+
args: 2
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "at_xl",
|
|
73
|
+
pattern: new RegExp(`\\bat_xl\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
74
|
+
args: 2,
|
|
75
|
+
noPrefix: true
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "dark",
|
|
79
|
+
pattern: new RegExp(`\\b(u?dark)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
80
|
+
args: 2
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "on",
|
|
84
|
+
pattern: new RegExp(`\\b(u?on)\\s*\\(\\s*"(::?[^"]+)"\\s*,\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
85
|
+
args: 3,
|
|
86
|
+
pseudoInMatch: true
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "media",
|
|
90
|
+
pattern: new RegExp(`\\bmedia\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
91
|
+
args: 3,
|
|
92
|
+
noPrefix: true
|
|
93
|
+
}
|
|
94
|
+
];
|
|
95
|
+
function isStringLiteral(str) {
|
|
96
|
+
const trimmed = str.trim();
|
|
97
|
+
return trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'");
|
|
98
|
+
}
|
|
99
|
+
function isFunctionDefinition(code) {
|
|
100
|
+
return /:\s*String/.test(code);
|
|
101
|
+
}
|
|
102
|
+
function getLineNumber(content, position) {
|
|
103
|
+
return content.substring(0, position).split("\n").length;
|
|
104
|
+
}
|
|
105
|
+
function detectWarnings(content, filePath) {
|
|
106
|
+
const warnings = [];
|
|
107
|
+
for (const { name, pattern, args, noPrefix, pseudoInMatch } of WARN_PATTERNS) {
|
|
108
|
+
pattern.lastIndex = 0;
|
|
109
|
+
for (const match of content.matchAll(pattern)) {
|
|
110
|
+
const fullMatch = match[0];
|
|
111
|
+
const position = match.index;
|
|
112
|
+
const line = getLineNumber(content, position);
|
|
113
|
+
if (isFunctionDefinition(fullMatch)) continue;
|
|
114
|
+
const extractedArgs = [];
|
|
115
|
+
if (args === 2) {
|
|
116
|
+
const startIdx = noPrefix ? 1 : 2;
|
|
117
|
+
extractedArgs.push(match[startIdx], match[startIdx + 1]);
|
|
118
|
+
} else if (args === 3) if (pseudoInMatch) extractedArgs.push(match[3], match[4]);
|
|
119
|
+
else {
|
|
120
|
+
const startIdx = noPrefix ? 1 : 2;
|
|
121
|
+
extractedArgs.push(match[startIdx], match[startIdx + 1], match[startIdx + 2]);
|
|
122
|
+
}
|
|
123
|
+
for (let i = 0; i < extractedArgs.length; i++) {
|
|
124
|
+
const arg = extractedArgs[i];
|
|
125
|
+
if (arg && !isStringLiteral(arg)) {
|
|
126
|
+
const argNum = pseudoInMatch ? i + 2 : i + 1;
|
|
127
|
+
warnings.push({
|
|
128
|
+
file: filePath,
|
|
129
|
+
line,
|
|
130
|
+
func: name,
|
|
131
|
+
code: fullMatch.trim(),
|
|
132
|
+
reason: `Argument ${argNum} is not a string literal: ${arg.trim()}`
|
|
133
|
+
});
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return warnings;
|
|
140
|
+
}
|
|
141
|
+
function extractFromContent(content) {
|
|
142
|
+
const base = /* @__PURE__ */ new Set();
|
|
143
|
+
const pseudo = [];
|
|
144
|
+
const media = [];
|
|
145
|
+
function extractBase(pattern) {
|
|
146
|
+
for (const match of content.matchAll(pattern)) base.add(`${match[1]}:${match[2]}`);
|
|
147
|
+
}
|
|
148
|
+
function extractStyles(pattern) {
|
|
149
|
+
for (const match of content.matchAll(pattern)) {
|
|
150
|
+
const pairs = match[1];
|
|
151
|
+
for (const pair of pairs.matchAll(STYLES_PAIR_PATTERN)) base.add(`${pair[1]}:${pair[2]}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function extractPseudo(pattern, pseudoSelector) {
|
|
155
|
+
for (const match of content.matchAll(pattern)) pseudo.push({
|
|
156
|
+
pseudo: pseudoSelector,
|
|
157
|
+
property: match[1],
|
|
158
|
+
value: match[2]
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function extractOn(pattern) {
|
|
162
|
+
for (const match of content.matchAll(pattern)) pseudo.push({
|
|
163
|
+
pseudo: match[1],
|
|
164
|
+
property: match[2],
|
|
165
|
+
value: match[3]
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function extractMedia(pattern, condition = null) {
|
|
169
|
+
for (const match of content.matchAll(pattern)) if (condition) media.push({
|
|
170
|
+
condition,
|
|
171
|
+
property: match[1],
|
|
172
|
+
value: match[2]
|
|
173
|
+
});
|
|
174
|
+
else media.push({
|
|
175
|
+
condition: match[1],
|
|
176
|
+
property: match[2],
|
|
177
|
+
value: match[3]
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
extractBase(CSS_PATTERN);
|
|
181
|
+
extractBase(UCSS_PATTERN);
|
|
182
|
+
extractStyles(STYLES_PATTERN);
|
|
183
|
+
extractStyles(USTYLES_PATTERN);
|
|
184
|
+
extractPseudo(HOVER_PATTERN, ":hover");
|
|
185
|
+
extractPseudo(UHOVER_PATTERN, ":hover");
|
|
186
|
+
extractPseudo(FOCUS_PATTERN, ":focus");
|
|
187
|
+
extractPseudo(UFOCUS_PATTERN, ":focus");
|
|
188
|
+
extractPseudo(ACTIVE_PATTERN, ":active");
|
|
189
|
+
extractPseudo(UACTIVE_PATTERN, ":active");
|
|
190
|
+
extractOn(ON_PATTERN);
|
|
191
|
+
extractOn(UON_PATTERN);
|
|
192
|
+
extractMedia(MEDIA_PATTERN);
|
|
193
|
+
extractMedia(AT_SM_PATTERN, "min-width:640px");
|
|
194
|
+
extractMedia(AT_MD_PATTERN, "min-width:768px");
|
|
195
|
+
extractMedia(UAT_MD_PATTERN, "min-width:768px");
|
|
196
|
+
extractMedia(AT_LG_PATTERN, "min-width:1024px");
|
|
197
|
+
extractMedia(UAT_LG_PATTERN, "min-width:1024px");
|
|
198
|
+
extractMedia(AT_XL_PATTERN, "min-width:1280px");
|
|
199
|
+
extractMedia(DARK_PATTERN, "prefers-color-scheme:dark");
|
|
200
|
+
extractMedia(UDARK_PATTERN, "prefers-color-scheme:dark");
|
|
201
|
+
return {
|
|
202
|
+
base,
|
|
203
|
+
pseudo,
|
|
204
|
+
media
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function findMbtFiles(dir) {
|
|
208
|
+
const files = [];
|
|
209
|
+
function walk(currentDir) {
|
|
210
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
213
|
+
if (entry.isDirectory()) {
|
|
214
|
+
if (![
|
|
215
|
+
"node_modules",
|
|
216
|
+
".git",
|
|
217
|
+
"target",
|
|
218
|
+
".mooncakes"
|
|
219
|
+
].includes(entry.name)) walk(fullPath);
|
|
220
|
+
} else if (entry.isFile() && entry.name.endsWith(".mbt")) files.push(fullPath);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
walk(dir);
|
|
224
|
+
return files;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* DJB2 hash function - must match MoonBit registry.mbt implementation
|
|
228
|
+
*/
|
|
229
|
+
function djb2Hash$1(s) {
|
|
230
|
+
let hash = 5381;
|
|
231
|
+
for (let i = 0; i < s.length; i++) {
|
|
232
|
+
const c = s.charCodeAt(i);
|
|
233
|
+
hash = (hash << 5) + hash + c >>> 0;
|
|
234
|
+
}
|
|
235
|
+
return hash;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Convert hash to base36 string (0-9a-z)
|
|
239
|
+
* Takes lower 24 bits to keep class names short (4-5 chars)
|
|
240
|
+
*/
|
|
241
|
+
function toBase36$1(n) {
|
|
242
|
+
const chars = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
243
|
+
n = n & 16777215;
|
|
244
|
+
if (n === 0) return "0";
|
|
245
|
+
let result = "";
|
|
246
|
+
while (n > 0) {
|
|
247
|
+
result = chars[n % 36] + result;
|
|
248
|
+
n = Math.floor(n / 36);
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Generate class name from declaration hash
|
|
254
|
+
*/
|
|
255
|
+
function hashClassName(decl) {
|
|
256
|
+
return "_" + toBase36$1(djb2Hash$1(decl));
|
|
257
|
+
}
|
|
258
|
+
function generateCSS(styles, options = {}) {
|
|
259
|
+
const { pretty = false } = options;
|
|
260
|
+
const mapping = {};
|
|
261
|
+
const parts = [];
|
|
262
|
+
for (const decl of styles.base) {
|
|
263
|
+
const cls = hashClassName(decl);
|
|
264
|
+
mapping[decl] = cls;
|
|
265
|
+
if (pretty) parts.push(`.${cls} { ${decl} }`);
|
|
266
|
+
else parts.push(`.${cls}{${decl}}`);
|
|
267
|
+
}
|
|
268
|
+
for (const { pseudo, property, value } of styles.pseudo) {
|
|
269
|
+
const hashKey = `${pseudo}:${property}:${value}`;
|
|
270
|
+
const cls = hashClassName(hashKey);
|
|
271
|
+
mapping[hashKey] = cls;
|
|
272
|
+
if (pretty) parts.push(`.${cls}${pseudo} { ${property}: ${value} }`);
|
|
273
|
+
else parts.push(`.${cls}${pseudo}{${property}:${value}}`);
|
|
274
|
+
}
|
|
275
|
+
const mediaGroups = /* @__PURE__ */ new Map();
|
|
276
|
+
for (const { condition, property, value } of styles.media) {
|
|
277
|
+
if (!mediaGroups.has(condition)) mediaGroups.set(condition, []);
|
|
278
|
+
mediaGroups.get(condition).push({
|
|
279
|
+
property,
|
|
280
|
+
value
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
for (const [condition, declarations] of mediaGroups) {
|
|
284
|
+
const rules = [];
|
|
285
|
+
for (const { property, value } of declarations) {
|
|
286
|
+
const hashKey = `@media(${condition}):${property}:${value}`;
|
|
287
|
+
const cls = hashClassName(hashKey);
|
|
288
|
+
mapping[hashKey] = cls;
|
|
289
|
+
if (pretty) rules.push(` .${cls} { ${property}: ${value} }`);
|
|
290
|
+
else rules.push(`.${cls}{${property}:${value}}`);
|
|
291
|
+
}
|
|
292
|
+
if (pretty) parts.push(`@media (${condition}) {\n${rules.join("\n")}\n}`);
|
|
293
|
+
else parts.push(`@media(${condition}){${rules.join("")}}`);
|
|
294
|
+
}
|
|
295
|
+
const separator = pretty ? "\n" : "";
|
|
296
|
+
return {
|
|
297
|
+
css: parts.join(separator),
|
|
298
|
+
mapping
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function extract(dir, options = {}) {
|
|
302
|
+
const { pretty = false, warn = true, strict = false, verbose = false } = options;
|
|
303
|
+
const files = findMbtFiles(dir);
|
|
304
|
+
if (verbose) console.error(`Found ${files.length} .mbt files`);
|
|
305
|
+
const combined = {
|
|
306
|
+
base: /* @__PURE__ */ new Set(),
|
|
307
|
+
pseudo: [],
|
|
308
|
+
media: []
|
|
309
|
+
};
|
|
310
|
+
const allWarnings = [];
|
|
311
|
+
for (const file of files) {
|
|
312
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
313
|
+
const extracted = extractFromContent(content);
|
|
314
|
+
for (const decl of extracted.base) combined.base.add(decl);
|
|
315
|
+
combined.pseudo.push(...extracted.pseudo);
|
|
316
|
+
combined.media.push(...extracted.media);
|
|
317
|
+
if (warn) {
|
|
318
|
+
const warnings = detectWarnings(content, file);
|
|
319
|
+
allWarnings.push(...warnings);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (warn && allWarnings.length > 0 && verbose) {
|
|
323
|
+
console.error(`\n⚠️ ${allWarnings.length} warning(s): non-literal CSS arguments detected\n`);
|
|
324
|
+
for (const w of allWarnings) {
|
|
325
|
+
console.error(` ${w.file}:${w.line}`);
|
|
326
|
+
console.error(` ${w.func}(...) - ${w.reason}`);
|
|
327
|
+
console.error(` Code: ${w.code}`);
|
|
328
|
+
console.error("");
|
|
329
|
+
}
|
|
330
|
+
if (strict) throw new Error("Strict mode: non-literal CSS arguments detected");
|
|
331
|
+
}
|
|
332
|
+
if (verbose) {
|
|
333
|
+
console.error(`Extracted:`);
|
|
334
|
+
console.error(` - ${combined.base.size} base declarations`);
|
|
335
|
+
console.error(` - ${combined.pseudo.length} pseudo-class declarations`);
|
|
336
|
+
console.error(` - ${combined.media.length} media query declarations`);
|
|
337
|
+
}
|
|
338
|
+
const { css, mapping } = generateCSS(combined, { pretty });
|
|
339
|
+
return {
|
|
340
|
+
css,
|
|
341
|
+
mapping,
|
|
342
|
+
stats: {
|
|
343
|
+
base: combined.base.size,
|
|
344
|
+
pseudo: combined.pseudo.length,
|
|
345
|
+
media: combined.media.length
|
|
346
|
+
},
|
|
347
|
+
warnings: warn ? allWarnings : void 0
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Extract CSS with split output by file or directory.
|
|
352
|
+
* Shared CSS (used in 3+ files/dirs) is separated into a shared chunk.
|
|
353
|
+
*/
|
|
354
|
+
function extractSplit(dir, mode, options = {}) {
|
|
355
|
+
const { pretty = false, warn = true, strict = false, verbose = false, sharedThreshold = 3 } = options;
|
|
356
|
+
const files = findMbtFiles(dir);
|
|
357
|
+
if (verbose) console.error(`Found ${files.length} .mbt files`);
|
|
358
|
+
const declUsage = /* @__PURE__ */ new Map();
|
|
359
|
+
const pseudoUsage = /* @__PURE__ */ new Map();
|
|
360
|
+
const mediaUsage = /* @__PURE__ */ new Map();
|
|
361
|
+
const perChunk = /* @__PURE__ */ new Map();
|
|
362
|
+
const allWarnings = [];
|
|
363
|
+
for (const file of files) {
|
|
364
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
365
|
+
const extracted = extractFromContent(content);
|
|
366
|
+
const relPath = path.relative(dir, file);
|
|
367
|
+
const chunkKey = mode === "file" ? relPath : path.dirname(relPath) || ".";
|
|
368
|
+
if (!perChunk.has(chunkKey)) perChunk.set(chunkKey, {
|
|
369
|
+
base: /* @__PURE__ */ new Set(),
|
|
370
|
+
pseudo: [],
|
|
371
|
+
media: []
|
|
372
|
+
});
|
|
373
|
+
const chunk = perChunk.get(chunkKey);
|
|
374
|
+
for (const decl of extracted.base) {
|
|
375
|
+
chunk.base.add(decl);
|
|
376
|
+
if (!declUsage.has(decl)) declUsage.set(decl, /* @__PURE__ */ new Set());
|
|
377
|
+
declUsage.get(decl).add(chunkKey);
|
|
378
|
+
}
|
|
379
|
+
for (const p of extracted.pseudo) {
|
|
380
|
+
const key = `${p.pseudo}:${p.property}:${p.value}`;
|
|
381
|
+
chunk.pseudo.push(p);
|
|
382
|
+
if (!pseudoUsage.has(key)) pseudoUsage.set(key, /* @__PURE__ */ new Set());
|
|
383
|
+
pseudoUsage.get(key).add(chunkKey);
|
|
384
|
+
}
|
|
385
|
+
for (const m of extracted.media) {
|
|
386
|
+
const key = `@media(${m.condition}):${m.property}:${m.value}`;
|
|
387
|
+
chunk.media.push(m);
|
|
388
|
+
if (!mediaUsage.has(key)) mediaUsage.set(key, /* @__PURE__ */ new Set());
|
|
389
|
+
mediaUsage.get(key).add(chunkKey);
|
|
390
|
+
}
|
|
391
|
+
if (warn) {
|
|
392
|
+
const warnings = detectWarnings(content, file);
|
|
393
|
+
allWarnings.push(...warnings);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const sharedBase = /* @__PURE__ */ new Set();
|
|
397
|
+
const sharedPseudo = [];
|
|
398
|
+
const sharedMedia = [];
|
|
399
|
+
for (const [decl, chunks] of declUsage) if (chunks.size >= sharedThreshold) sharedBase.add(decl);
|
|
400
|
+
const seenPseudo = /* @__PURE__ */ new Set();
|
|
401
|
+
for (const [key, chunks] of pseudoUsage) if (chunks.size >= sharedThreshold && !seenPseudo.has(key)) {
|
|
402
|
+
seenPseudo.add(key);
|
|
403
|
+
const [pseudo, property, value] = key.split(":");
|
|
404
|
+
sharedPseudo.push({
|
|
405
|
+
pseudo,
|
|
406
|
+
property,
|
|
407
|
+
value
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
const seenMedia = /* @__PURE__ */ new Set();
|
|
411
|
+
for (const [key, chunks] of mediaUsage) if (chunks.size >= sharedThreshold && !seenMedia.has(key)) {
|
|
412
|
+
seenMedia.add(key);
|
|
413
|
+
const match = key.match(/^@media\(([^)]+)\):([^:]+):(.+)$/);
|
|
414
|
+
if (match) sharedMedia.push({
|
|
415
|
+
condition: match[1],
|
|
416
|
+
property: match[2],
|
|
417
|
+
value: match[3]
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
const finalChunks = /* @__PURE__ */ new Map();
|
|
421
|
+
const chunkStats = /* @__PURE__ */ new Map();
|
|
422
|
+
for (const [chunkKey, chunk] of perChunk) {
|
|
423
|
+
const chunkOnlyBase = /* @__PURE__ */ new Set();
|
|
424
|
+
for (const decl of chunk.base) if (!sharedBase.has(decl)) chunkOnlyBase.add(decl);
|
|
425
|
+
const chunkOnlyPseudo = chunk.pseudo.filter((p) => {
|
|
426
|
+
const key = `${p.pseudo}:${p.property}:${p.value}`;
|
|
427
|
+
return !seenPseudo.has(key);
|
|
428
|
+
});
|
|
429
|
+
const chunkOnlyMedia = chunk.media.filter((m) => {
|
|
430
|
+
const key = `@media(${m.condition}):${m.property}:${m.value}`;
|
|
431
|
+
return !seenMedia.has(key);
|
|
432
|
+
});
|
|
433
|
+
const chunkStyles = {
|
|
434
|
+
base: chunkOnlyBase,
|
|
435
|
+
pseudo: chunkOnlyPseudo,
|
|
436
|
+
media: chunkOnlyMedia
|
|
437
|
+
};
|
|
438
|
+
const { css } = generateCSS(chunkStyles, { pretty });
|
|
439
|
+
finalChunks.set(chunkKey, {
|
|
440
|
+
css,
|
|
441
|
+
styles: chunkStyles
|
|
442
|
+
});
|
|
443
|
+
chunkStats.set(chunkKey, {
|
|
444
|
+
base: chunkOnlyBase.size,
|
|
445
|
+
pseudo: chunkOnlyPseudo.length,
|
|
446
|
+
media: chunkOnlyMedia.length
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
const sharedStyles = {
|
|
450
|
+
base: sharedBase,
|
|
451
|
+
pseudo: sharedPseudo,
|
|
452
|
+
media: sharedMedia
|
|
453
|
+
};
|
|
454
|
+
const { css: sharedCss, mapping } = generateCSS(sharedStyles, { pretty });
|
|
455
|
+
const combinedStyles = {
|
|
456
|
+
base: /* @__PURE__ */ new Set(),
|
|
457
|
+
pseudo: [],
|
|
458
|
+
media: []
|
|
459
|
+
};
|
|
460
|
+
for (const decl of sharedBase) combinedStyles.base.add(decl);
|
|
461
|
+
for (const [, chunk] of finalChunks) {
|
|
462
|
+
for (const decl of chunk.styles.base) combinedStyles.base.add(decl);
|
|
463
|
+
combinedStyles.pseudo.push(...chunk.styles.pseudo);
|
|
464
|
+
combinedStyles.media.push(...chunk.styles.media);
|
|
465
|
+
}
|
|
466
|
+
combinedStyles.pseudo.push(...sharedPseudo);
|
|
467
|
+
combinedStyles.media.push(...sharedMedia);
|
|
468
|
+
const { css: combinedCss, mapping: fullMapping } = generateCSS(combinedStyles, { pretty });
|
|
469
|
+
if (verbose) {
|
|
470
|
+
console.error(`Split mode: ${mode}`);
|
|
471
|
+
console.error(`Chunks: ${finalChunks.size}`);
|
|
472
|
+
console.error(`Shared declarations (≥${sharedThreshold} usages):`);
|
|
473
|
+
console.error(` - ${sharedBase.size} base`);
|
|
474
|
+
console.error(` - ${sharedPseudo.length} pseudo`);
|
|
475
|
+
console.error(` - ${sharedMedia.length} media`);
|
|
476
|
+
}
|
|
477
|
+
if (warn && allWarnings.length > 0 && verbose) console.error(`\n⚠️ ${allWarnings.length} warning(s)\n`);
|
|
478
|
+
if (strict && allWarnings.length > 0) throw new Error("Strict mode: non-literal CSS arguments detected");
|
|
479
|
+
return {
|
|
480
|
+
chunks: finalChunks,
|
|
481
|
+
shared: {
|
|
482
|
+
css: sharedCss,
|
|
483
|
+
styles: sharedStyles
|
|
484
|
+
},
|
|
485
|
+
combined: combinedCss,
|
|
486
|
+
mapping: fullMapping,
|
|
487
|
+
stats: chunkStats,
|
|
488
|
+
warnings: warn ? allWarnings : void 0
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
//#endregion
|
|
493
|
+
//#region src/css-optimizer/hash.ts
|
|
494
|
+
/**
|
|
495
|
+
* Hash functions for CSS class name generation
|
|
496
|
+
*/
|
|
497
|
+
/**
|
|
498
|
+
* DJB2 hash function
|
|
499
|
+
* A simple but effective string hashing algorithm
|
|
500
|
+
*/
|
|
501
|
+
function djb2Hash(s) {
|
|
502
|
+
let hash = 5381;
|
|
503
|
+
for (let i = 0; i < s.length; i++) {
|
|
504
|
+
const c = s.charCodeAt(i);
|
|
505
|
+
hash = (hash << 5) + hash + c >>> 0;
|
|
506
|
+
}
|
|
507
|
+
return hash;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Convert a number to base36 string (0-9a-z)
|
|
511
|
+
* Takes lower 24 bits to keep class names short (4-5 chars)
|
|
512
|
+
*/
|
|
513
|
+
function toBase36(n) {
|
|
514
|
+
const chars = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
515
|
+
n = n & 16777215;
|
|
516
|
+
if (n === 0) return "0";
|
|
517
|
+
let result = "";
|
|
518
|
+
while (n > 0) {
|
|
519
|
+
result = chars[n % 36] + result;
|
|
520
|
+
n = Math.floor(n / 36);
|
|
521
|
+
}
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Generate a merged class name from combined declarations
|
|
526
|
+
* @param declarations - Array of CSS declarations
|
|
527
|
+
* @param prefix - Class name prefix (default: "_m" for merged)
|
|
528
|
+
*/
|
|
529
|
+
function hashMergedClassName(declarations, prefix = "_m") {
|
|
530
|
+
return prefix + toBase36(djb2Hash(declarations.sort().join(";")));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
//#endregion
|
|
534
|
+
//#region src/css-optimizer/pattern.ts
|
|
535
|
+
/**
|
|
536
|
+
* Find frequent patterns (sets of classes that often appear together)
|
|
537
|
+
* Uses a greedy enumeration approach for small pattern sizes
|
|
538
|
+
*/
|
|
539
|
+
function findFrequentPatterns(usages, minFrequency, maxSize) {
|
|
540
|
+
const patternCounts = /* @__PURE__ */ new Map();
|
|
541
|
+
for (const usage of usages) {
|
|
542
|
+
const classes = [...usage.classes].sort();
|
|
543
|
+
const n = classes.length;
|
|
544
|
+
for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) {
|
|
545
|
+
const key = `${classes[i]}|${classes[j]}`;
|
|
546
|
+
patternCounts.set(key, (patternCounts.get(key) || 0) + 1);
|
|
547
|
+
}
|
|
548
|
+
if (maxSize >= 3) for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) for (let k = j + 1; k < n; k++) {
|
|
549
|
+
const key = `${classes[i]}|${classes[j]}|${classes[k]}`;
|
|
550
|
+
patternCounts.set(key, (patternCounts.get(key) || 0) + 1);
|
|
551
|
+
}
|
|
552
|
+
if (maxSize >= 4) for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) for (let k = j + 1; k < n; k++) for (let l = k + 1; l < n; l++) {
|
|
553
|
+
const key = `${classes[i]}|${classes[j]}|${classes[k]}|${classes[l]}`;
|
|
554
|
+
patternCounts.set(key, (patternCounts.get(key) || 0) + 1);
|
|
555
|
+
}
|
|
556
|
+
if (maxSize >= 5) for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) for (let k = j + 1; k < n; k++) for (let l = k + 1; l < n; l++) for (let m = l + 1; m < n; m++) {
|
|
557
|
+
const key = `${classes[i]}|${classes[j]}|${classes[k]}|${classes[l]}|${classes[m]}`;
|
|
558
|
+
patternCounts.set(key, (patternCounts.get(key) || 0) + 1);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const patterns = [];
|
|
562
|
+
for (const [key, frequency] of patternCounts) if (frequency >= minFrequency) {
|
|
563
|
+
const originalClasses = key.split("|");
|
|
564
|
+
const classCount = originalClasses.length;
|
|
565
|
+
const bytesSaved = (classCount - 1) * 7 * frequency + (classCount - 1) * 25;
|
|
566
|
+
patterns.push({
|
|
567
|
+
originalClasses,
|
|
568
|
+
mergedClass: "",
|
|
569
|
+
declarations: [],
|
|
570
|
+
frequency,
|
|
571
|
+
bytesSaved
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
patterns.sort((a, b) => b.bytesSaved - a.bytesSaved);
|
|
575
|
+
return removeSubsumedPatterns(patterns);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Remove patterns that are subsets of larger patterns with similar frequency
|
|
579
|
+
* This prevents double-counting savings
|
|
580
|
+
*/
|
|
581
|
+
function removeSubsumedPatterns(patterns) {
|
|
582
|
+
const result = [];
|
|
583
|
+
for (let i = 0; i < patterns.length; i++) {
|
|
584
|
+
const pattern = patterns[i];
|
|
585
|
+
let isDominated = false;
|
|
586
|
+
for (let j = 0; j < patterns.length; j++) {
|
|
587
|
+
if (i === j) continue;
|
|
588
|
+
const other = patterns[j];
|
|
589
|
+
if (other.originalClasses.length > pattern.originalClasses.length && other.frequency >= pattern.frequency * .8) {
|
|
590
|
+
const patternSet = new Set(pattern.originalClasses);
|
|
591
|
+
const otherSet = new Set(other.originalClasses);
|
|
592
|
+
let isSubset = true;
|
|
593
|
+
for (const cls of patternSet) if (!otherSet.has(cls)) {
|
|
594
|
+
isSubset = false;
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
if (isSubset) {
|
|
598
|
+
isDominated = true;
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (!isDominated) result.push(pattern);
|
|
604
|
+
}
|
|
605
|
+
return result;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
//#endregion
|
|
609
|
+
//#region src/css-optimizer/core.ts
|
|
610
|
+
/**
|
|
611
|
+
* Core optimizer - performs pattern analysis and CSS generation
|
|
612
|
+
*
|
|
613
|
+
* This is the framework-agnostic core that:
|
|
614
|
+
* 1. Analyzes class usages to find frequent patterns
|
|
615
|
+
* 2. Generates merged CSS rules
|
|
616
|
+
* 3. Returns a merge map for HTML transformation
|
|
617
|
+
*
|
|
618
|
+
* @param usages - Array of class usages extracted from source
|
|
619
|
+
* @param css - Original CSS content
|
|
620
|
+
* @param classToDeclaration - Map from class name to CSS declaration
|
|
621
|
+
* @param options - Optimization options
|
|
622
|
+
*/
|
|
623
|
+
function optimizeCore(usages, css, classToDeclaration, options = {}) {
|
|
624
|
+
const { minFrequency = 2, maxPatternSize = 5, pretty = false, verbose = false } = options;
|
|
625
|
+
const log = (msg) => {
|
|
626
|
+
if (verbose) console.log(`[css-optimizer] ${msg}`);
|
|
627
|
+
};
|
|
628
|
+
if (usages.length === 0) return {
|
|
629
|
+
css,
|
|
630
|
+
mergeMap: /* @__PURE__ */ new Map(),
|
|
631
|
+
patterns: [],
|
|
632
|
+
stats: {
|
|
633
|
+
originalClasses: classToDeclaration.size,
|
|
634
|
+
mergedPatterns: 0,
|
|
635
|
+
estimatedBytesSaved: 0
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
log(`Analyzing ${usages.length} class usage sites`);
|
|
639
|
+
const patterns = findFrequentPatterns(usages, minFrequency, maxPatternSize);
|
|
640
|
+
log(`Found ${patterns.length} frequent patterns`);
|
|
641
|
+
const mergeMap = /* @__PURE__ */ new Map();
|
|
642
|
+
const cssRules = [];
|
|
643
|
+
const usedClasses = /* @__PURE__ */ new Set();
|
|
644
|
+
let totalBytesSaved = 0;
|
|
645
|
+
for (const pattern of patterns) {
|
|
646
|
+
const declarations = [];
|
|
647
|
+
let allFound = true;
|
|
648
|
+
for (const cls of pattern.originalClasses) {
|
|
649
|
+
const decl = classToDeclaration.get(cls);
|
|
650
|
+
if (decl) declarations.push(decl);
|
|
651
|
+
else {
|
|
652
|
+
allFound = false;
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (!allFound || declarations.length === 0) continue;
|
|
657
|
+
let hasConflict = false;
|
|
658
|
+
for (const cls of pattern.originalClasses) if (usedClasses.has(cls)) {
|
|
659
|
+
hasConflict = true;
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
if (hasConflict) continue;
|
|
663
|
+
const mergedClass = hashMergedClassName(declarations);
|
|
664
|
+
pattern.mergedClass = mergedClass;
|
|
665
|
+
pattern.declarations = declarations;
|
|
666
|
+
const mergeKey = pattern.originalClasses.join(" ");
|
|
667
|
+
mergeMap.set(mergeKey, mergedClass);
|
|
668
|
+
for (const cls of pattern.originalClasses) usedClasses.add(cls);
|
|
669
|
+
const declStr = declarations.join(";");
|
|
670
|
+
if (pretty) cssRules.push(`.${mergedClass} { ${declStr.replace(/;/g, "; ")} }`);
|
|
671
|
+
else cssRules.push(`.${mergedClass}{${declStr}}`);
|
|
672
|
+
totalBytesSaved += pattern.bytesSaved;
|
|
673
|
+
}
|
|
674
|
+
const originalRules = [];
|
|
675
|
+
const rulePattern = /\.([a-zA-Z_][a-zA-Z0-9_-]*)\{([^}]+)\}/g;
|
|
676
|
+
let match;
|
|
677
|
+
rulePattern.lastIndex = 0;
|
|
678
|
+
while ((match = rulePattern.exec(css)) !== null) {
|
|
679
|
+
const cls = match[1];
|
|
680
|
+
if (!usedClasses.has(cls)) originalRules.push(match[0]);
|
|
681
|
+
}
|
|
682
|
+
const mediaPattern = /@media[^{]+\{[^}]+\}/g;
|
|
683
|
+
mediaPattern.lastIndex = 0;
|
|
684
|
+
while ((match = mediaPattern.exec(css)) !== null) originalRules.push(match[0]);
|
|
685
|
+
const pseudoPattern = /\.([a-zA-Z_][a-zA-Z0-9_-]*)(:[a-zA-Z-]+)\{([^}]+)\}/g;
|
|
686
|
+
pseudoPattern.lastIndex = 0;
|
|
687
|
+
while ((match = pseudoPattern.exec(css)) !== null) originalRules.push(match[0]);
|
|
688
|
+
const separator = pretty ? "\n" : "";
|
|
689
|
+
const optimizedCss = [...cssRules, ...originalRules].join(separator);
|
|
690
|
+
const actualPatterns = patterns.filter((p) => p.mergedClass !== "");
|
|
691
|
+
log(`Merged ${actualPatterns.length} patterns`);
|
|
692
|
+
log(`Estimated savings: ${totalBytesSaved} bytes`);
|
|
693
|
+
return {
|
|
694
|
+
css: optimizedCss,
|
|
695
|
+
mergeMap,
|
|
696
|
+
patterns: actualPatterns,
|
|
697
|
+
stats: {
|
|
698
|
+
originalClasses: classToDeclaration.size,
|
|
699
|
+
mergedPatterns: actualPatterns.length,
|
|
700
|
+
estimatedBytesSaved: totalBytesSaved
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
//#endregion
|
|
706
|
+
//#region src/css-optimizer/extractors.ts
|
|
707
|
+
/**
|
|
708
|
+
* HTML Class Extractor
|
|
709
|
+
*
|
|
710
|
+
* Extracts class usages from HTML content by parsing class="" attributes.
|
|
711
|
+
* Works with static HTML, SSR output, template strings, etc.
|
|
712
|
+
*/
|
|
713
|
+
var HtmlExtractor = class {
|
|
714
|
+
name = "html";
|
|
715
|
+
pattern = /class\s*=\s*"([^"]+)"/g;
|
|
716
|
+
extract(content, options = {}) {
|
|
717
|
+
const { classPrefix = "_", minClasses = 2, source = "html" } = options;
|
|
718
|
+
const usages = [];
|
|
719
|
+
let match;
|
|
720
|
+
this.pattern.lastIndex = 0;
|
|
721
|
+
while ((match = this.pattern.exec(content)) !== null) {
|
|
722
|
+
const classValue = match[1].trim();
|
|
723
|
+
if (!classValue) continue;
|
|
724
|
+
const classes = classValue.split(/\s+/).filter((c) => c.startsWith(classPrefix) && c.length > classPrefix.length);
|
|
725
|
+
if (classes.length >= minClasses) {
|
|
726
|
+
classes.sort();
|
|
727
|
+
usages.push({
|
|
728
|
+
classes,
|
|
729
|
+
source: `${source}:${match.index}`
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return usages;
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
/**
|
|
737
|
+
* JSX Class Extractor (simplified)
|
|
738
|
+
*
|
|
739
|
+
* Extracts class usages from JSX/TSX by finding className attributes.
|
|
740
|
+
* Note: This is a simplified regex-based extractor. For full AST support,
|
|
741
|
+
* consider using @babel/parser or TypeScript compiler.
|
|
742
|
+
*/
|
|
743
|
+
var JsxExtractor = class {
|
|
744
|
+
name = "jsx";
|
|
745
|
+
stringPattern = /className\s*=\s*"([^"]+)"/g;
|
|
746
|
+
templatePattern = /className\s*=\s*\{['"`]([^'"`]+)['"`]\}/g;
|
|
747
|
+
extract(content, options = {}) {
|
|
748
|
+
const { classPrefix = "_", minClasses = 2, source = "jsx" } = options;
|
|
749
|
+
const usages = [];
|
|
750
|
+
this.extractFromPattern(content, this.stringPattern, classPrefix, minClasses, source, usages);
|
|
751
|
+
this.extractFromPattern(content, this.templatePattern, classPrefix, minClasses, source, usages);
|
|
752
|
+
return usages;
|
|
753
|
+
}
|
|
754
|
+
extractFromPattern(content, pattern, classPrefix, minClasses, source, usages) {
|
|
755
|
+
let match;
|
|
756
|
+
pattern.lastIndex = 0;
|
|
757
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
758
|
+
const classValue = match[1].trim();
|
|
759
|
+
if (!classValue) continue;
|
|
760
|
+
const classes = classValue.split(/\s+/).filter((c) => c.startsWith(classPrefix) && c.length > classPrefix.length);
|
|
761
|
+
if (classes.length >= minClasses) {
|
|
762
|
+
classes.sort();
|
|
763
|
+
usages.push({
|
|
764
|
+
classes,
|
|
765
|
+
source: `${source}:${match.index}`
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
/**
|
|
772
|
+
* Svelte Class Extractor (simplified)
|
|
773
|
+
*
|
|
774
|
+
* Extracts class usages from Svelte templates.
|
|
775
|
+
* Handles both class="..." and class:directive syntax.
|
|
776
|
+
*/
|
|
777
|
+
var SvelteExtractor = class {
|
|
778
|
+
name = "svelte";
|
|
779
|
+
classPattern = /class\s*=\s*"([^"]+)"/g;
|
|
780
|
+
extract(content, options = {}) {
|
|
781
|
+
const { classPrefix = "_", minClasses = 2, source = "svelte" } = options;
|
|
782
|
+
const usages = [];
|
|
783
|
+
let match;
|
|
784
|
+
this.classPattern.lastIndex = 0;
|
|
785
|
+
while ((match = this.classPattern.exec(content)) !== null) {
|
|
786
|
+
const classValue = match[1].trim();
|
|
787
|
+
if (!classValue) continue;
|
|
788
|
+
const classes = classValue.replace(/\{[^}]+\}/g, " ").split(/\s+/).filter((c) => c.startsWith(classPrefix) && c.length > classPrefix.length);
|
|
789
|
+
if (classes.length >= minClasses) {
|
|
790
|
+
classes.sort();
|
|
791
|
+
usages.push({
|
|
792
|
+
classes,
|
|
793
|
+
source: `${source}:${match.index}`
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return usages;
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
/**
|
|
801
|
+
* Multi-source extractor
|
|
802
|
+
*
|
|
803
|
+
* Combines usages from multiple sources/extractors.
|
|
804
|
+
* Useful for analyzing entire projects with mixed file types.
|
|
805
|
+
*/
|
|
806
|
+
var MultiExtractor = class {
|
|
807
|
+
name = "multi";
|
|
808
|
+
extractors;
|
|
809
|
+
constructor() {
|
|
810
|
+
this.extractors = new Map([
|
|
811
|
+
["html", new HtmlExtractor()],
|
|
812
|
+
["jsx", new JsxExtractor()],
|
|
813
|
+
["tsx", new JsxExtractor()],
|
|
814
|
+
["svelte", new SvelteExtractor()]
|
|
815
|
+
]);
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Register a custom extractor for a file extension
|
|
819
|
+
*/
|
|
820
|
+
register(extension, extractor) {
|
|
821
|
+
this.extractors.set(extension, extractor);
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Extract from content using the appropriate extractor
|
|
825
|
+
*/
|
|
826
|
+
extract(content, options = {}) {
|
|
827
|
+
return this.extractors.get("html").extract(content, options);
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Extract from content with explicit file type
|
|
831
|
+
*/
|
|
832
|
+
extractWithType(content, fileType, options = {}) {
|
|
833
|
+
return (this.extractors.get(fileType) || this.extractors.get("html")).extract(content, options);
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Extract from multiple files
|
|
837
|
+
*/
|
|
838
|
+
extractFromFiles(files, options = {}) {
|
|
839
|
+
const allUsages = [];
|
|
840
|
+
for (const file of files) {
|
|
841
|
+
const ext = file.path.split(".").pop() || "html";
|
|
842
|
+
const usages = (this.extractors.get(ext) || this.extractors.get("html")).extract(file.content, {
|
|
843
|
+
...options,
|
|
844
|
+
source: file.path
|
|
845
|
+
});
|
|
846
|
+
allUsages.push(...usages);
|
|
847
|
+
}
|
|
848
|
+
return allUsages;
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
const htmlExtractor = new HtmlExtractor();
|
|
852
|
+
const jsxExtractor = new JsxExtractor();
|
|
853
|
+
const svelteExtractor = new SvelteExtractor();
|
|
854
|
+
const multiExtractor = new MultiExtractor();
|
|
855
|
+
|
|
856
|
+
//#endregion
|
|
857
|
+
//#region src/css-optimizer/transformers.ts
|
|
858
|
+
/**
|
|
859
|
+
* HTML Transformer
|
|
860
|
+
*
|
|
861
|
+
* Transforms HTML by replacing class="" attribute values with merged classes.
|
|
862
|
+
*/
|
|
863
|
+
var HtmlTransformer = class {
|
|
864
|
+
name = "html";
|
|
865
|
+
pattern = /class\s*=\s*"([^"]+)"/g;
|
|
866
|
+
transform(content, mergeMap, options = {}) {
|
|
867
|
+
if (mergeMap.size === 0) return content;
|
|
868
|
+
const { classPrefix = "_" } = options;
|
|
869
|
+
const sortedKeys = [...mergeMap.keys()].sort((a, b) => b.length - a.length);
|
|
870
|
+
return content.replace(this.pattern, (match, classValue) => {
|
|
871
|
+
const classes = classValue.trim().split(/\s+/);
|
|
872
|
+
const prefixedClasses = classes.filter((c) => c.startsWith(classPrefix));
|
|
873
|
+
const otherClasses = classes.filter((c) => !c.startsWith(classPrefix));
|
|
874
|
+
prefixedClasses.sort();
|
|
875
|
+
let result = [...prefixedClasses];
|
|
876
|
+
const merged = [];
|
|
877
|
+
for (const key of sortedKeys) {
|
|
878
|
+
const patternClasses = key.split(" ");
|
|
879
|
+
const mergedClass = mergeMap.get(key);
|
|
880
|
+
let allPresent = true;
|
|
881
|
+
for (const cls of patternClasses) if (!result.includes(cls)) {
|
|
882
|
+
allPresent = false;
|
|
883
|
+
break;
|
|
884
|
+
}
|
|
885
|
+
if (allPresent) {
|
|
886
|
+
result = result.filter((c) => !patternClasses.includes(c));
|
|
887
|
+
merged.push(mergedClass);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return `class="${[
|
|
891
|
+
...merged,
|
|
892
|
+
...result,
|
|
893
|
+
...otherClasses
|
|
894
|
+
].join(" ")}"`;
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
/**
|
|
899
|
+
* JSX Transformer
|
|
900
|
+
*
|
|
901
|
+
* Transforms JSX/TSX by replacing className attribute values.
|
|
902
|
+
*/
|
|
903
|
+
var JsxTransformer = class {
|
|
904
|
+
name = "jsx";
|
|
905
|
+
stringPattern = /className\s*=\s*"([^"]+)"/g;
|
|
906
|
+
transform(content, mergeMap, options = {}) {
|
|
907
|
+
if (mergeMap.size === 0) return content;
|
|
908
|
+
const { classPrefix = "_" } = options;
|
|
909
|
+
const sortedKeys = [...mergeMap.keys()].sort((a, b) => b.length - a.length);
|
|
910
|
+
return content.replace(this.stringPattern, (match, classValue) => {
|
|
911
|
+
const classes = classValue.trim().split(/\s+/);
|
|
912
|
+
const prefixedClasses = classes.filter((c) => c.startsWith(classPrefix));
|
|
913
|
+
const otherClasses = classes.filter((c) => !c.startsWith(classPrefix));
|
|
914
|
+
prefixedClasses.sort();
|
|
915
|
+
let result = [...prefixedClasses];
|
|
916
|
+
const merged = [];
|
|
917
|
+
for (const key of sortedKeys) {
|
|
918
|
+
const patternClasses = key.split(" ");
|
|
919
|
+
const mergedClass = mergeMap.get(key);
|
|
920
|
+
let allPresent = true;
|
|
921
|
+
for (const cls of patternClasses) if (!result.includes(cls)) {
|
|
922
|
+
allPresent = false;
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
if (allPresent) {
|
|
926
|
+
result = result.filter((c) => !patternClasses.includes(c));
|
|
927
|
+
merged.push(mergedClass);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return `className="${[
|
|
931
|
+
...merged,
|
|
932
|
+
...result,
|
|
933
|
+
...otherClasses
|
|
934
|
+
].join(" ")}"`;
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
/**
|
|
939
|
+
* Svelte Transformer
|
|
940
|
+
*
|
|
941
|
+
* Transforms Svelte templates by replacing class attribute values.
|
|
942
|
+
* Preserves dynamic {expression} syntax.
|
|
943
|
+
*/
|
|
944
|
+
var SvelteTransformer = class {
|
|
945
|
+
name = "svelte";
|
|
946
|
+
pattern = /class\s*=\s*"([^"]+)"/g;
|
|
947
|
+
transform(content, mergeMap, options = {}) {
|
|
948
|
+
if (mergeMap.size === 0) return content;
|
|
949
|
+
const { classPrefix = "_" } = options;
|
|
950
|
+
const sortedKeys = [...mergeMap.keys()].sort((a, b) => b.length - a.length);
|
|
951
|
+
return content.replace(this.pattern, (match, classValue) => {
|
|
952
|
+
const expressions = [];
|
|
953
|
+
const classes = classValue.replace(/\{[^}]+\}/g, (expr) => {
|
|
954
|
+
expressions.push(expr);
|
|
955
|
+
return `__EXPR_${expressions.length - 1}__`;
|
|
956
|
+
}).trim().split(/\s+/);
|
|
957
|
+
const prefixedClasses = classes.filter((c) => c.startsWith(classPrefix) && !c.startsWith("__EXPR_"));
|
|
958
|
+
const otherClasses = classes.filter((c) => !c.startsWith(classPrefix) || c.startsWith("__EXPR_"));
|
|
959
|
+
prefixedClasses.sort();
|
|
960
|
+
let result = [...prefixedClasses];
|
|
961
|
+
const merged = [];
|
|
962
|
+
for (const key of sortedKeys) {
|
|
963
|
+
const patternClasses = key.split(" ");
|
|
964
|
+
const mergedClass = mergeMap.get(key);
|
|
965
|
+
let allPresent = true;
|
|
966
|
+
for (const cls of patternClasses) if (!result.includes(cls)) {
|
|
967
|
+
allPresent = false;
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
if (allPresent) {
|
|
971
|
+
result = result.filter((c) => !patternClasses.includes(c));
|
|
972
|
+
merged.push(mergedClass);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
let finalClasses = [
|
|
976
|
+
...merged,
|
|
977
|
+
...result,
|
|
978
|
+
...otherClasses
|
|
979
|
+
].join(" ");
|
|
980
|
+
expressions.forEach((expr, i) => {
|
|
981
|
+
finalClasses = finalClasses.replace(`__EXPR_${i}__`, expr);
|
|
982
|
+
});
|
|
983
|
+
return `class="${finalClasses}"`;
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
/**
|
|
988
|
+
* Multi-format transformer
|
|
989
|
+
*
|
|
990
|
+
* Automatically selects the appropriate transformer based on content or file type.
|
|
991
|
+
*/
|
|
992
|
+
var MultiTransformer = class {
|
|
993
|
+
name = "multi";
|
|
994
|
+
transformers;
|
|
995
|
+
constructor() {
|
|
996
|
+
this.transformers = new Map([
|
|
997
|
+
["html", new HtmlTransformer()],
|
|
998
|
+
["jsx", new JsxTransformer()],
|
|
999
|
+
["tsx", new JsxTransformer()],
|
|
1000
|
+
["svelte", new SvelteTransformer()]
|
|
1001
|
+
]);
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Register a custom transformer for a file extension
|
|
1005
|
+
*/
|
|
1006
|
+
register(extension, transformer) {
|
|
1007
|
+
this.transformers.set(extension, transformer);
|
|
1008
|
+
}
|
|
1009
|
+
transform(content, mergeMap, options) {
|
|
1010
|
+
return this.transformers.get("html").transform(content, mergeMap, options);
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* Transform with explicit file type
|
|
1014
|
+
*/
|
|
1015
|
+
transformWithType(content, mergeMap, fileType, options) {
|
|
1016
|
+
return (this.transformers.get(fileType) || this.transformers.get("html")).transform(content, mergeMap, options);
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Transform multiple files
|
|
1020
|
+
*/
|
|
1021
|
+
transformFiles(files, mergeMap, options) {
|
|
1022
|
+
return files.map((file) => {
|
|
1023
|
+
const ext = file.path.split(".").pop() || "html";
|
|
1024
|
+
const transformer = this.transformers.get(ext) || this.transformers.get("html");
|
|
1025
|
+
return {
|
|
1026
|
+
path: file.path,
|
|
1027
|
+
content: transformer.transform(file.content, mergeMap, options)
|
|
1028
|
+
};
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
const htmlTransformer = new HtmlTransformer();
|
|
1033
|
+
const jsxTransformer = new JsxTransformer();
|
|
1034
|
+
const svelteTransformer = new SvelteTransformer();
|
|
1035
|
+
const multiTransformer = new MultiTransformer();
|
|
1036
|
+
|
|
1037
|
+
//#endregion
|
|
1038
|
+
//#region src/css-optimizer/merge.ts
|
|
1039
|
+
/**
|
|
1040
|
+
* Optimize CSS by merging co-occurring classes (Luna convenience wrapper)
|
|
1041
|
+
*
|
|
1042
|
+
* This is a convenience function that:
|
|
1043
|
+
* 1. Extracts class usages from HTML using HtmlExtractor
|
|
1044
|
+
* 2. Converts Luna's declaration mapping format
|
|
1045
|
+
* 3. Calls the core optimizer
|
|
1046
|
+
*
|
|
1047
|
+
* For non-Luna projects, use the `optimize` function instead.
|
|
1048
|
+
*
|
|
1049
|
+
* @param css - Original CSS content
|
|
1050
|
+
* @param html - HTML content to analyze
|
|
1051
|
+
* @param declarationMapping - Luna's declaration -> class name mapping
|
|
1052
|
+
* @param options - Optimization options
|
|
1053
|
+
*/
|
|
1054
|
+
function optimizeCss(css, html, declarationMapping, options = {}) {
|
|
1055
|
+
const { classPrefix = "_", verbose = false } = options;
|
|
1056
|
+
const log = (msg) => {
|
|
1057
|
+
if (verbose) console.log(`[optimizer] ${msg}`);
|
|
1058
|
+
};
|
|
1059
|
+
const usages = htmlExtractor.extract(html, {
|
|
1060
|
+
classPrefix,
|
|
1061
|
+
source: "html"
|
|
1062
|
+
});
|
|
1063
|
+
log(`Found ${usages.length} class usage sites`);
|
|
1064
|
+
if (usages.length === 0) return {
|
|
1065
|
+
css,
|
|
1066
|
+
mergeMap: /* @__PURE__ */ new Map(),
|
|
1067
|
+
patterns: [],
|
|
1068
|
+
stats: {
|
|
1069
|
+
originalClasses: Object.keys(declarationMapping).length,
|
|
1070
|
+
mergedPatterns: 0,
|
|
1071
|
+
estimatedBytesSaved: 0
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
const classToDecl = /* @__PURE__ */ new Map();
|
|
1075
|
+
for (const [decl, cls] of Object.entries(declarationMapping)) classToDecl.set(cls, decl);
|
|
1076
|
+
return optimizeCore(usages, css, classToDecl, options);
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Apply optimization to HTML by replacing class combinations (Luna convenience wrapper)
|
|
1080
|
+
*
|
|
1081
|
+
* This is a convenience function that uses HtmlTransformer.
|
|
1082
|
+
* For non-Luna projects, use the transformer directly.
|
|
1083
|
+
*
|
|
1084
|
+
* @param html - HTML content to transform
|
|
1085
|
+
* @param mergeMap - Merge map from optimizeCss result
|
|
1086
|
+
* @param classPrefix - Class prefix (default: "_")
|
|
1087
|
+
*/
|
|
1088
|
+
function optimizeHtml(html, mergeMap, classPrefix = "_") {
|
|
1089
|
+
return htmlTransformer.transform(html, mergeMap, { classPrefix });
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
//#endregion
|
|
1093
|
+
//#region src/vite-plugin.ts
|
|
1094
|
+
const CSS_START_MARKER = "/* LUNA_CSS_START */";
|
|
1095
|
+
const CSS_END_MARKER = "/* LUNA_CSS_END */";
|
|
1096
|
+
const VIRTUAL_CSS_ID = "virtual:luna.css";
|
|
1097
|
+
const VIRTUAL_SHARED_CSS_ID = "virtual:luna-shared.css";
|
|
1098
|
+
const VIRTUAL_RUNTIME_ID = "virtual:luna-css-runtime";
|
|
1099
|
+
const RESOLVED_VIRTUAL_CSS_ID = "\0" + VIRTUAL_CSS_ID;
|
|
1100
|
+
const RESOLVED_VIRTUAL_SHARED_CSS_ID = "\0" + VIRTUAL_SHARED_CSS_ID;
|
|
1101
|
+
const RESOLVED_VIRTUAL_RUNTIME_ID = "\0" + VIRTUAL_RUNTIME_ID;
|
|
1102
|
+
/**
|
|
1103
|
+
* Generate inline dev runtime code
|
|
1104
|
+
* Uses import.meta.env.DEV for tree-shaking in production builds
|
|
1105
|
+
*/
|
|
1106
|
+
function generateRuntimeCode() {
|
|
1107
|
+
return `
|
|
1108
|
+
// Luna CSS Dev Runtime - Auto-generated
|
|
1109
|
+
// Production: DCE removes dev-only code via import.meta.env.DEV
|
|
1110
|
+
|
|
1111
|
+
// Hash functions (shared between dev and prod for class name generation)
|
|
1112
|
+
function djb2Hash(s) {
|
|
1113
|
+
let hash = 5381;
|
|
1114
|
+
for (let i = 0; i < s.length; i++) {
|
|
1115
|
+
hash = ((hash << 5) + hash + s.charCodeAt(i)) >>> 0;
|
|
1116
|
+
}
|
|
1117
|
+
return hash;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function toBase36(n) {
|
|
1121
|
+
const chars = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
1122
|
+
n = n & 0xffffff;
|
|
1123
|
+
if (n === 0) return "0";
|
|
1124
|
+
let result = "";
|
|
1125
|
+
while (n > 0) { result = chars[n % 36] + result; n = Math.floor(n / 36); }
|
|
1126
|
+
return result;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function hashClassName(decl) { return "_" + toBase36(djb2Hash(decl)); }
|
|
1130
|
+
|
|
1131
|
+
// Dev-only: runtime CSS injection
|
|
1132
|
+
let devState;
|
|
1133
|
+
let devInject;
|
|
1134
|
+
|
|
1135
|
+
if (import.meta.env.DEV) {
|
|
1136
|
+
devState = { rules: new Map(), styleEl: null, initialized: false };
|
|
1137
|
+
|
|
1138
|
+
const init = () => {
|
|
1139
|
+
if (devState.initialized || typeof document === "undefined") return;
|
|
1140
|
+
devState.styleEl = document.createElement("style");
|
|
1141
|
+
devState.styleEl.id = "luna-dev-css";
|
|
1142
|
+
document.head.appendChild(devState.styleEl);
|
|
1143
|
+
devState.initialized = true;
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
devInject = (className, rule) => {
|
|
1147
|
+
init();
|
|
1148
|
+
if (devState.rules.has(className)) return;
|
|
1149
|
+
devState.rules.set(className, rule);
|
|
1150
|
+
if (devState.styleEl) devState.styleEl.textContent += rule;
|
|
1151
|
+
console.warn("[luna-css] Generated at runtime:", rule, "\\n → Run 'luna css extract' to pre-generate");
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
export function css(prop, val) {
|
|
1156
|
+
const decl = prop + ":" + val;
|
|
1157
|
+
const cls = hashClassName(decl);
|
|
1158
|
+
if (import.meta.env.DEV && devInject) {
|
|
1159
|
+
devInject(cls, "." + cls + "{" + decl + "}");
|
|
1160
|
+
}
|
|
1161
|
+
return cls;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
export function styles(pairs) { return pairs.map(([p, v]) => css(p, v)).join(" "); }
|
|
1165
|
+
|
|
1166
|
+
export function on(pseudo, prop, val) {
|
|
1167
|
+
const key = pseudo + ":" + prop + ":" + val;
|
|
1168
|
+
const cls = hashClassName(key);
|
|
1169
|
+
if (import.meta.env.DEV && devInject) {
|
|
1170
|
+
devInject(cls, "." + cls + pseudo + "{" + prop + ":" + val + "}");
|
|
1171
|
+
}
|
|
1172
|
+
return cls;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
export function hover(p, v) { return on(":hover", p, v); }
|
|
1176
|
+
export function focus(p, v) { return on(":focus", p, v); }
|
|
1177
|
+
export function active(p, v) { return on(":active", p, v); }
|
|
1178
|
+
export function combine(classes) { return classes.filter(Boolean).join(" "); }
|
|
1179
|
+
|
|
1180
|
+
// Dev-only utilities
|
|
1181
|
+
export function getGeneratedCount() {
|
|
1182
|
+
if (import.meta.env.DEV && devState) {
|
|
1183
|
+
return devState.rules.size;
|
|
1184
|
+
}
|
|
1185
|
+
return 0;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
export function getGeneratedCss() {
|
|
1189
|
+
if (import.meta.env.DEV && devState) {
|
|
1190
|
+
return Array.from(devState.rules.values()).join("");
|
|
1191
|
+
}
|
|
1192
|
+
return "";
|
|
1193
|
+
}
|
|
1194
|
+
`;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Inject dev runtime script into HTML
|
|
1198
|
+
*/
|
|
1199
|
+
function injectDevRuntime(html) {
|
|
1200
|
+
return html.replace("</head>", "<script type=\"module\">\nimport { css, hover, focus, styles, combine } from \"virtual:luna-css-runtime\";\nwindow.__lunaCss = { css, hover, focus, styles, combine };\n<\/script>\n</head>");
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Luna CSS Vite Plugin
|
|
1204
|
+
*
|
|
1205
|
+
* @example
|
|
1206
|
+
* ```ts
|
|
1207
|
+
* // vite.config.ts
|
|
1208
|
+
* import { lunaCss } from "@luna_ui/luna/vite-plugin";
|
|
1209
|
+
*
|
|
1210
|
+
* export default defineConfig({
|
|
1211
|
+
* plugins: [
|
|
1212
|
+
* lunaCss({
|
|
1213
|
+
* src: ["src/examples/todomvc"],
|
|
1214
|
+
* mode: "auto",
|
|
1215
|
+
* threshold: 4096,
|
|
1216
|
+
* }),
|
|
1217
|
+
* ],
|
|
1218
|
+
* });
|
|
1219
|
+
* ```
|
|
1220
|
+
*
|
|
1221
|
+
* @example
|
|
1222
|
+
* ```ts
|
|
1223
|
+
* // main.ts - Import virtual CSS like Tailwind
|
|
1224
|
+
* import "virtual:luna.css";
|
|
1225
|
+
* ```
|
|
1226
|
+
*
|
|
1227
|
+
* @example
|
|
1228
|
+
* ```ts
|
|
1229
|
+
* // Split mode with per-directory chunks
|
|
1230
|
+
* lunaCss({
|
|
1231
|
+
* src: ["src"],
|
|
1232
|
+
* split: true,
|
|
1233
|
+
* sharedThreshold: 3,
|
|
1234
|
+
* })
|
|
1235
|
+
*
|
|
1236
|
+
* // In your entry:
|
|
1237
|
+
* import "virtual:luna.css"; // All CSS
|
|
1238
|
+
* // Or for fine-grained control:
|
|
1239
|
+
* import "virtual:luna-shared.css"; // Shared CSS only
|
|
1240
|
+
* import "virtual:luna-chunk/todomvc.css"; // Per-directory chunk
|
|
1241
|
+
* ```
|
|
1242
|
+
*/
|
|
1243
|
+
function lunaCss(options = {}) {
|
|
1244
|
+
const { src = ["src"], mode = "auto", threshold = 4096, cssFileName = "luna", verbose = false, split = false, sharedThreshold = 3, devRuntime, injectRuntime = true, experimental = {} } = options;
|
|
1245
|
+
const { optimize: enableOptimize = false, optimizeMinFrequency = 2, optimizeMaxPatternSize = 5 } = experimental;
|
|
1246
|
+
const srcDirs = Array.isArray(src) ? src : [src];
|
|
1247
|
+
let config;
|
|
1248
|
+
let cachedCss = null;
|
|
1249
|
+
let cachedSplitResult = null;
|
|
1250
|
+
let cachedMapping = {};
|
|
1251
|
+
let cachedMergeMap = null;
|
|
1252
|
+
let cachedOptimizedCss = null;
|
|
1253
|
+
let lastExtractTime = 0;
|
|
1254
|
+
const pluginCwd = process.cwd();
|
|
1255
|
+
const log = (msg) => {
|
|
1256
|
+
if (verbose || enableOptimize) console.log(`[luna-css] ${msg}`);
|
|
1257
|
+
};
|
|
1258
|
+
const resolveSrcDir = (dir) => {
|
|
1259
|
+
if (path.isAbsolute(dir)) return dir;
|
|
1260
|
+
const configDir = config.configFile ? path.dirname(config.configFile) : pluginCwd;
|
|
1261
|
+
return path.resolve(configDir, dir);
|
|
1262
|
+
};
|
|
1263
|
+
const extractCss = () => {
|
|
1264
|
+
const now = Date.now();
|
|
1265
|
+
if (cachedCss && now - lastExtractTime < 1e3) return {
|
|
1266
|
+
css: cachedCss,
|
|
1267
|
+
mapping: cachedMapping
|
|
1268
|
+
};
|
|
1269
|
+
let allCss = "";
|
|
1270
|
+
const seenRules = /* @__PURE__ */ new Set();
|
|
1271
|
+
const combinedMapping = {};
|
|
1272
|
+
for (const dir of srcDirs) {
|
|
1273
|
+
const fullPath = resolveSrcDir(dir);
|
|
1274
|
+
if (fs.existsSync(fullPath)) {
|
|
1275
|
+
const { css, mapping } = extract(fullPath, { warn: false });
|
|
1276
|
+
Object.assign(combinedMapping, mapping);
|
|
1277
|
+
for (const rule of css.split("}")) {
|
|
1278
|
+
const trimmed = rule.trim();
|
|
1279
|
+
if (trimmed && !seenRules.has(trimmed)) {
|
|
1280
|
+
seenRules.add(trimmed);
|
|
1281
|
+
allCss += trimmed + "}";
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
} else log(`Warning: Source directory not found: ${fullPath}`);
|
|
1285
|
+
}
|
|
1286
|
+
cachedCss = allCss;
|
|
1287
|
+
cachedMapping = combinedMapping;
|
|
1288
|
+
lastExtractTime = now;
|
|
1289
|
+
log(`Extracted ${allCss.length} bytes of CSS from ${srcDirs.join(", ")}`);
|
|
1290
|
+
return {
|
|
1291
|
+
css: allCss,
|
|
1292
|
+
mapping: combinedMapping
|
|
1293
|
+
};
|
|
1294
|
+
};
|
|
1295
|
+
const findHtmlFiles = (dir) => {
|
|
1296
|
+
const files = [];
|
|
1297
|
+
const walk = (currentDir) => {
|
|
1298
|
+
if (!fs.existsSync(currentDir)) return;
|
|
1299
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
1300
|
+
for (const entry of entries) {
|
|
1301
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
1302
|
+
if (entry.isDirectory() && ![
|
|
1303
|
+
"node_modules",
|
|
1304
|
+
".git",
|
|
1305
|
+
"dist",
|
|
1306
|
+
"target"
|
|
1307
|
+
].includes(entry.name)) walk(fullPath);
|
|
1308
|
+
else if (entry.isFile() && entry.name.endsWith(".html")) files.push(fullPath);
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
walk(dir);
|
|
1312
|
+
return files;
|
|
1313
|
+
};
|
|
1314
|
+
const runOptimization = () => {
|
|
1315
|
+
if (!enableOptimize) return null;
|
|
1316
|
+
const { css, mapping } = extractCss();
|
|
1317
|
+
if (!css) return null;
|
|
1318
|
+
const htmlFilePaths = findHtmlFiles(config.configFile ? path.dirname(config.configFile) : pluginCwd);
|
|
1319
|
+
if (htmlFilePaths.length === 0) {
|
|
1320
|
+
log("[experimental] No HTML files found for optimization");
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
let combinedHtml = "";
|
|
1324
|
+
for (const htmlPath of htmlFilePaths) try {
|
|
1325
|
+
combinedHtml += fs.readFileSync(htmlPath, "utf-8");
|
|
1326
|
+
} catch (e) {}
|
|
1327
|
+
if (!combinedHtml) return null;
|
|
1328
|
+
log(`[experimental] Running CSS co-occurrence optimization on ${htmlFilePaths.length} HTML file(s)...`);
|
|
1329
|
+
const optimizeResult = optimizeCss(css, combinedHtml, mapping, {
|
|
1330
|
+
minFrequency: optimizeMinFrequency,
|
|
1331
|
+
maxPatternSize: optimizeMaxPatternSize,
|
|
1332
|
+
verbose
|
|
1333
|
+
});
|
|
1334
|
+
if (optimizeResult.patterns.length > 0) {
|
|
1335
|
+
log(`[experimental] Merged ${optimizeResult.stats.mergedPatterns} patterns`);
|
|
1336
|
+
log(`[experimental] Estimated savings: ${optimizeResult.stats.estimatedBytesSaved} bytes`);
|
|
1337
|
+
for (const pattern of optimizeResult.patterns) log(`[experimental] ${pattern.originalClasses.join(" ")} -> ${pattern.mergedClass} (${pattern.frequency}x)`);
|
|
1338
|
+
return {
|
|
1339
|
+
css: optimizeResult.css,
|
|
1340
|
+
mergeMap: optimizeResult.mergeMap
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
log("[experimental] No patterns found for optimization");
|
|
1344
|
+
return null;
|
|
1345
|
+
};
|
|
1346
|
+
const getOptimizedCss = () => {
|
|
1347
|
+
if (!enableOptimize) return extractCss().css;
|
|
1348
|
+
if (cachedOptimizedCss !== null && cachedMergeMap !== null) return cachedOptimizedCss;
|
|
1349
|
+
const result = runOptimization();
|
|
1350
|
+
if (result) {
|
|
1351
|
+
cachedOptimizedCss = result.css;
|
|
1352
|
+
cachedMergeMap = result.mergeMap;
|
|
1353
|
+
return result.css;
|
|
1354
|
+
}
|
|
1355
|
+
return extractCss().css;
|
|
1356
|
+
};
|
|
1357
|
+
const extractSplitCss = () => {
|
|
1358
|
+
const now = Date.now();
|
|
1359
|
+
if (cachedSplitResult && now - lastExtractTime < 1e3) return cachedSplitResult;
|
|
1360
|
+
const combinedChunks = /* @__PURE__ */ new Map();
|
|
1361
|
+
let combinedSharedCss = "";
|
|
1362
|
+
let combinedCss = "";
|
|
1363
|
+
const combinedMapping = {};
|
|
1364
|
+
const combinedStats = /* @__PURE__ */ new Map();
|
|
1365
|
+
for (const dir of srcDirs) {
|
|
1366
|
+
const fullPath = resolveSrcDir(dir);
|
|
1367
|
+
if (fs.existsSync(fullPath)) {
|
|
1368
|
+
const result = extractSplit(fullPath, "dir", {
|
|
1369
|
+
warn: false,
|
|
1370
|
+
sharedThreshold
|
|
1371
|
+
});
|
|
1372
|
+
for (const [key, chunk] of result.chunks) {
|
|
1373
|
+
const prefixedKey = srcDirs.length > 1 ? `${dir}/${key}` : key;
|
|
1374
|
+
combinedChunks.set(prefixedKey, chunk);
|
|
1375
|
+
}
|
|
1376
|
+
if (result.shared.css) combinedSharedCss += result.shared.css;
|
|
1377
|
+
combinedCss += result.combined;
|
|
1378
|
+
Object.assign(combinedMapping, result.mapping);
|
|
1379
|
+
for (const [key, stat] of result.stats) {
|
|
1380
|
+
const prefixedKey = srcDirs.length > 1 ? `${dir}/${key}` : key;
|
|
1381
|
+
combinedStats.set(prefixedKey, stat);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
cachedSplitResult = {
|
|
1386
|
+
chunks: combinedChunks,
|
|
1387
|
+
shared: {
|
|
1388
|
+
css: combinedSharedCss,
|
|
1389
|
+
styles: {
|
|
1390
|
+
base: /* @__PURE__ */ new Set(),
|
|
1391
|
+
pseudo: [],
|
|
1392
|
+
media: []
|
|
1393
|
+
}
|
|
1394
|
+
},
|
|
1395
|
+
combined: combinedCss,
|
|
1396
|
+
mapping: combinedMapping,
|
|
1397
|
+
stats: combinedStats
|
|
1398
|
+
};
|
|
1399
|
+
lastExtractTime = now;
|
|
1400
|
+
log(`Split extracted: ${combinedChunks.size} chunks, ${combinedSharedCss.length} bytes shared`);
|
|
1401
|
+
return cachedSplitResult;
|
|
1402
|
+
};
|
|
1403
|
+
const determineMode = (css) => {
|
|
1404
|
+
if (mode === "auto") return css.length > threshold ? "external" : "inline";
|
|
1405
|
+
return mode === "external" ? "external" : "inline";
|
|
1406
|
+
};
|
|
1407
|
+
const injectInline = (html, css) => {
|
|
1408
|
+
const startIdx = html.indexOf(CSS_START_MARKER);
|
|
1409
|
+
const endIdx = html.indexOf(CSS_END_MARKER);
|
|
1410
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
1411
|
+
log("Warning: CSS markers not found in HTML");
|
|
1412
|
+
return html;
|
|
1413
|
+
}
|
|
1414
|
+
return `${html.substring(0, startIdx + 20)}\n ${css}\n ${html.substring(endIdx)}`;
|
|
1415
|
+
};
|
|
1416
|
+
return {
|
|
1417
|
+
name: "luna-css",
|
|
1418
|
+
configResolved(resolvedConfig) {
|
|
1419
|
+
config = resolvedConfig;
|
|
1420
|
+
},
|
|
1421
|
+
resolveId(id) {
|
|
1422
|
+
if (id === VIRTUAL_CSS_ID) return RESOLVED_VIRTUAL_CSS_ID;
|
|
1423
|
+
if (id === VIRTUAL_SHARED_CSS_ID) return RESOLVED_VIRTUAL_SHARED_CSS_ID;
|
|
1424
|
+
if (id === VIRTUAL_RUNTIME_ID) return RESOLVED_VIRTUAL_RUNTIME_ID;
|
|
1425
|
+
if (id.startsWith("virtual:luna-chunk/")) return "\0" + id;
|
|
1426
|
+
},
|
|
1427
|
+
load(id) {
|
|
1428
|
+
if (id === RESOLVED_VIRTUAL_CSS_ID) {
|
|
1429
|
+
if (split) return extractSplitCss().combined;
|
|
1430
|
+
if (config.command === "build" && enableOptimize) return getOptimizedCss();
|
|
1431
|
+
return extractCss().css;
|
|
1432
|
+
}
|
|
1433
|
+
if (id === RESOLVED_VIRTUAL_SHARED_CSS_ID) {
|
|
1434
|
+
if (!split) {
|
|
1435
|
+
log("Warning: virtual:luna-shared.css used without split mode");
|
|
1436
|
+
return "";
|
|
1437
|
+
}
|
|
1438
|
+
return extractSplitCss().shared.css;
|
|
1439
|
+
}
|
|
1440
|
+
if (id === RESOLVED_VIRTUAL_RUNTIME_ID) return generateRuntimeCode();
|
|
1441
|
+
if (id.startsWith("\0virtual:luna-chunk/")) {
|
|
1442
|
+
const chunkName = id.replace("\0virtual:luna-chunk/", "").replace(".css", "");
|
|
1443
|
+
if (!split) {
|
|
1444
|
+
log(`Warning: virtual:luna-chunk/${chunkName}.css used without split mode`);
|
|
1445
|
+
return "";
|
|
1446
|
+
}
|
|
1447
|
+
const chunk = extractSplitCss().chunks.get(chunkName);
|
|
1448
|
+
if (chunk) return chunk.css;
|
|
1449
|
+
log(`Warning: Chunk "${chunkName}" not found`);
|
|
1450
|
+
return "";
|
|
1451
|
+
}
|
|
1452
|
+
},
|
|
1453
|
+
transformIndexHtml: {
|
|
1454
|
+
order: "pre",
|
|
1455
|
+
handler(html, ctx) {
|
|
1456
|
+
const isBuild = config.command === "build";
|
|
1457
|
+
const isDev = !isBuild;
|
|
1458
|
+
const shouldInjectRuntime = injectRuntime && (devRuntime ?? isDev) && isDev;
|
|
1459
|
+
if (split && isBuild) {
|
|
1460
|
+
const result$1 = extractSplitCss();
|
|
1461
|
+
const sharedCss = result$1.shared.css;
|
|
1462
|
+
const combinedCss = result$1.combined;
|
|
1463
|
+
log(`Split mode build: ${result$1.chunks.size} chunks, ${sharedCss.length} bytes shared`);
|
|
1464
|
+
return injectInline(html, combinedCss);
|
|
1465
|
+
}
|
|
1466
|
+
const { css } = extractCss();
|
|
1467
|
+
if (!css) {
|
|
1468
|
+
if (shouldInjectRuntime) return injectDevRuntime(html);
|
|
1469
|
+
return html;
|
|
1470
|
+
}
|
|
1471
|
+
let finalCss = css;
|
|
1472
|
+
let finalHtml = html;
|
|
1473
|
+
if (enableOptimize && isBuild) {
|
|
1474
|
+
finalCss = getOptimizedCss();
|
|
1475
|
+
if (cachedMergeMap && cachedMergeMap.size > 0) finalHtml = optimizeHtml(html, cachedMergeMap);
|
|
1476
|
+
}
|
|
1477
|
+
log(`Mode: ${determineMode(finalCss)}, Build: ${isBuild} (${finalCss.length} bytes)`);
|
|
1478
|
+
let result = injectInline(finalHtml, finalCss);
|
|
1479
|
+
if (shouldInjectRuntime) result = injectDevRuntime(result);
|
|
1480
|
+
return result;
|
|
1481
|
+
}
|
|
1482
|
+
},
|
|
1483
|
+
configureServer(server) {
|
|
1484
|
+
server.middlewares.use((req, res, next) => {
|
|
1485
|
+
if (req.url === `/${cssFileName}.css`) {
|
|
1486
|
+
const { css } = extractCss();
|
|
1487
|
+
res.setHeader("Content-Type", "text/css");
|
|
1488
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
1489
|
+
res.end(css);
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
next();
|
|
1493
|
+
});
|
|
1494
|
+
for (const dir of srcDirs) {
|
|
1495
|
+
const fullPath = resolveSrcDir(dir);
|
|
1496
|
+
if (fs.existsSync(fullPath)) server.watcher.add(`${fullPath}/**/*.mbt`);
|
|
1497
|
+
}
|
|
1498
|
+
server.watcher.on("change", (file) => {
|
|
1499
|
+
if (file.endsWith(".mbt")) {
|
|
1500
|
+
log(`File changed: ${file}`);
|
|
1501
|
+
cachedCss = null;
|
|
1502
|
+
cachedSplitResult = null;
|
|
1503
|
+
cachedMergeMap = null;
|
|
1504
|
+
cachedOptimizedCss = null;
|
|
1505
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_CSS_ID);
|
|
1506
|
+
if (mod) server.moduleGraph.invalidateModule(mod);
|
|
1507
|
+
const sharedMod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_SHARED_CSS_ID);
|
|
1508
|
+
if (sharedMod) server.moduleGraph.invalidateModule(sharedMod);
|
|
1509
|
+
server.ws.send({ type: "full-reload" });
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
var vite_plugin_default = lunaCss;
|
|
1516
|
+
|
|
1517
|
+
//#endregion
|
|
1518
|
+
export { vite_plugin_default as default, lunaCss };
|