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