@luna_ui/luna 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +1264 -27
- package/dist/css/index.d.ts +194 -0
- package/dist/css/index.js +721 -0
- package/dist/css/runtime.d.ts +92 -0
- package/dist/css/runtime.js +179 -0
- package/dist/index.js +1 -1
- package/dist/jsx-dev-runtime.js +1 -1
- package/dist/jsx-runtime.d.ts +5 -0
- package/dist/jsx-runtime.js +1 -1
- package/dist/src-CHiGeWfy.js +1 -0
- package/dist/vite-plugin.d.ts +122 -0
- package/dist/vite-plugin.js +1518 -0
- package/package.json +16 -2
- package/src/css/extract.ts +798 -0
- package/src/css/index.ts +10 -0
- package/src/css/inject.ts +205 -0
- package/src/css/inline.ts +182 -0
- package/src/css/minify.ts +70 -0
- package/src/css/optimizer.ts +6 -0
- package/src/css/runtime.ts +344 -0
- package/src/css-optimizer/README.md +353 -0
- package/src/css-optimizer/cooccurrence.ts +100 -0
- package/src/css-optimizer/core.ts +263 -0
- package/src/css-optimizer/extractors.ts +243 -0
- package/src/css-optimizer/hash.ts +54 -0
- package/src/css-optimizer/index.ts +129 -0
- package/src/css-optimizer/merge.ts +109 -0
- package/src/css-optimizer/moonbit-analyzer.ts +210 -0
- package/src/css-optimizer/parser.ts +120 -0
- package/src/css-optimizer/pattern.ts +171 -0
- package/src/css-optimizer/transformers.ts +301 -0
- package/src/css-optimizer/types.ts +128 -0
- package/src/event-utils.ts +227 -0
- package/src/index.ts +890 -0
- package/src/jsx-dev-runtime.ts +2 -0
- package/src/jsx-runtime.ts +398 -0
- package/src/vite-plugin.ts +718 -0
- package/tests/__screenshots__/context.test.ts/Context-API-context-with-reactive-effects-context-value-accessible-in-effect-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-For-component--SolidJS-style--For-updates-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-accepts-children-as-function-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-toggles-visibility-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-attribute-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-style-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElementNs--SVG-support--createElementNs-with-dynamic-attribute-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-effect-with-DOM-effect-tracks-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-clear-to-empty-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-empty-array-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-removes-items-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-renders-initial-list-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-updates-when-items-change-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-empty-to-non-empty-transition-in-SVG-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-reordering-in-SVG-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-updates-SVG-elements-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-with-nested-SVG-groups-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-ref-callback--JSX-style--ref-callback-with-nested-elements-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-creates-a-node-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-with-false-condition-creates-placeholder-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-text-nodes-textDyn-creates-reactive-text-node-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-forEach-renders-correctly-without-show--initial-items--1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-forEach-with-context-renders-correctly-without-show-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-nested-components-with-context--forEach--and-show-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-show-and-forEach-inherit-context-from-Owner--fixed--1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-show-and-forEach-work-together--context-uses-default--1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Context---ForEach-integration-forEach-items-can-access-context-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-renders-initial-list-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-updates-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-with-object-items-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-hides-when-condition-is-false-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-renders-when-condition-is-true-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-from-false-to-true-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-reactively-1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--event-listener-pattern--Solid-js-docs-example--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--multiple-cleanups-in-component-body--LIFO-order--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-in-component-body-runs-on-unmount-1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-works-with-For-loop-items--component-body-style--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--timer-cleanup-pattern--Solid-js-style--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Effects-effect-cleanup-runs-before-re-run-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-large-list-update-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-nested-batch-operations-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-rapid-sequential-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-Show-component---visible-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-show-hide-element---visible-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-fragment-with-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-nested-fragments-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-simple-fragment-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-conditional-toggle-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-addition-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-removal-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-text-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-className-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-style-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-multiple-dynamic-attributes-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-deeply-nested-conditionals-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-empty-to-populated-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-populated-to-empty-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-cleanup-order-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-with-inner-signal-change-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-is-called-when-effect-re-runs-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-with-resource-simulation-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-multiple-children--no-wrapper--1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-no-children-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-fragment-with-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-nested-Fragments-work-correctly-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-complex-reordering-with-additions-and-removals-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-insert-in-middle-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-remove-from-middle-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-reverse-list-order-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-shuffle-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-Show-component-renders-when-condition-is-true-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-renders-content-when-initially-true-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-toggles-visibility-dynamically-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Memo-Dependency-Chain-conditional-memo-dependencies-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-basic-signal-get-set-produces-same-values-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-batch-updates-produce-same-final-values-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-peek-reads-value-without-tracking-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-selective-tracking-with-untrack-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-untrack-prevents-dependency-tracking-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API--SolidJS-style--reactivity-accessor-is-reactive-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-empty-string-for-non-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-undefined-for-non-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsFailure-and-stateError-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsPending-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsSuccess-and-stateValue-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateValue-returns-undefined-for-non-success-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-reject-transitions-to-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-resolve-transitions-to-success-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-returns-resource--resolve--and-reject-functions-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-starts-in-pending-state-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-async-resolve-works-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-starts-in-pending-state-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-failure-on-reject-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-success-on-resolve-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-can-wrap-fetch-like-async-operations-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-works-with-setTimeout-simulation-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourceGet-tracks-dependencies-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourcePeek-does-not-track-dependencies-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceRefetch-refetch-resets-to-pending-and-re-runs-fetcher-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-children-to-body-by-default-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-to-selector-mount-target-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/SolidJS-API-compatibility-createEffect-tracks-dependencies-automatically-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-accessor-condition-in-Match-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-multiple-Match-components-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-single-Match-and-fallback-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-components-Switch-updates-DOM-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-multiple-dependencies-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-single-dependency-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-with-defer-option-skips-initial-run-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Arrays-array-updates-work-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-only-triggers-when-accessed-property-changes-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-parent-path-change-notifies-child-accessors-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-nested-property-access-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-property-access-in-effects-1.png +0 -0
- package/tests/context.test.ts +118 -0
- package/tests/css-optimizer-extractors.test.ts +264 -0
- package/tests/css-optimizer-integration.test.ts +566 -0
- package/tests/css-optimizer-transformers.test.ts +301 -0
- package/tests/css-optimizer.test.ts +646 -0
- package/tests/css-runtime.bench.ts +442 -0
- package/tests/css-runtime.test.ts +342 -0
- package/tests/dom.test.ts +872 -0
- package/tests/integration.test.ts +405 -0
- package/tests/issue-5-for-infinite-loop.test.ts +516 -0
- package/tests/jsx-runtime.test.tsx +393 -0
- package/tests/lifecycle.test.ts +833 -0
- package/tests/move-before.bench.ts +304 -0
- package/tests/preact-signals-comparison.test.ts +1608 -0
- package/tests/resource.test.ts +160 -0
- package/tests/router.test.ts +117 -0
- package/tests/show-initial-mount-leak.test.tsx +182 -0
- package/tests/solidjs-api.test.ts +659 -0
- package/tests/static-perf.bench.ts +64 -0
- package/tests/store.test.ts +263 -0
- package/tests/tsx-syntax.test.tsx +404 -0
- package/dist/src-DGWY0NYx.js +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,24 +1,906 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as path from "node:path";
|
|
2
|
+
import * as fs$1 from "node:fs";
|
|
3
|
+
import * as path$1 from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
5
7
|
|
|
8
|
+
//#region src/css/extract.ts
|
|
9
|
+
/**
|
|
10
|
+
* Static CSS Extractor for Luna CSS Module
|
|
11
|
+
*
|
|
12
|
+
* Extracts all CSS declarations from .mbt files at build time.
|
|
13
|
+
* This ensures all styles are collected regardless of runtime branches.
|
|
14
|
+
*/
|
|
15
|
+
const CSS_PATTERN = /\bcss\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
16
|
+
const STYLES_PATTERN = /\bstyles\s*\(\s*\[([\s\S]*?)\]\s*\)/g;
|
|
17
|
+
const STYLES_PAIR_PATTERN = /\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
18
|
+
const HOVER_PATTERN = /\bhover\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
19
|
+
const FOCUS_PATTERN = /\bfocus\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
20
|
+
const ACTIVE_PATTERN = /\bactive\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
21
|
+
const ON_PATTERN = /\bon\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
22
|
+
const MEDIA_PATTERN = /\bmedia\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
23
|
+
const AT_SM_PATTERN = /\bat_sm\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
24
|
+
const AT_MD_PATTERN = /\bat_md\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
25
|
+
const AT_LG_PATTERN = /\bat_lg\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
26
|
+
const AT_XL_PATTERN = /\bat_xl\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
27
|
+
const DARK_PATTERN = /\bdark\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
28
|
+
const UCSS_PATTERN = /\bucss\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
29
|
+
const USTYLES_PATTERN = /\bustyles\s*\(\s*\[([\s\S]*?)\]\s*\)/g;
|
|
30
|
+
const UHOVER_PATTERN = /\buhover\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
31
|
+
const UFOCUS_PATTERN = /\bufocus\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
32
|
+
const UACTIVE_PATTERN = /\buactive\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
33
|
+
const UON_PATTERN = /\buon\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
34
|
+
const UAT_MD_PATTERN = /\buat_md\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
35
|
+
const UAT_LG_PATTERN = /\buat_lg\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
36
|
+
const UDARK_PATTERN = /\budark\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
37
|
+
const ARG_PATTERN = `(?:"(?:[^"\\\\]|\\\\.)*"|[^,)]+)`;
|
|
38
|
+
const WARN_PATTERNS = [
|
|
39
|
+
{
|
|
40
|
+
name: "css",
|
|
41
|
+
pattern: new RegExp(`\\b(u?css)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
42
|
+
args: 2
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "hover",
|
|
46
|
+
pattern: new RegExp(`\\b(u?hover)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
47
|
+
args: 2
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "focus",
|
|
51
|
+
pattern: new RegExp(`\\b(u?focus)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
52
|
+
args: 2
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "active",
|
|
56
|
+
pattern: new RegExp(`\\b(u?active)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
57
|
+
args: 2
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "at_sm",
|
|
61
|
+
pattern: new RegExp(`\\bat_sm\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
62
|
+
args: 2,
|
|
63
|
+
noPrefix: true
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "at_md",
|
|
67
|
+
pattern: new RegExp(`\\b(u?at_md)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
68
|
+
args: 2
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "at_lg",
|
|
72
|
+
pattern: new RegExp(`\\b(u?at_lg)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
73
|
+
args: 2
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "at_xl",
|
|
77
|
+
pattern: new RegExp(`\\bat_xl\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
78
|
+
args: 2,
|
|
79
|
+
noPrefix: true
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "dark",
|
|
83
|
+
pattern: new RegExp(`\\b(u?dark)\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
84
|
+
args: 2
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "on",
|
|
88
|
+
pattern: new RegExp(`\\b(u?on)\\s*\\(\\s*"(::?[^"]+)"\\s*,\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
89
|
+
args: 3,
|
|
90
|
+
pseudoInMatch: true
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "media",
|
|
94
|
+
pattern: new RegExp(`\\bmedia\\s*\\(\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*,\\s*(${ARG_PATTERN})\\s*\\)`, "g"),
|
|
95
|
+
args: 3,
|
|
96
|
+
noPrefix: true
|
|
97
|
+
}
|
|
98
|
+
];
|
|
99
|
+
function isStringLiteral(str) {
|
|
100
|
+
const trimmed = str.trim();
|
|
101
|
+
return trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'");
|
|
102
|
+
}
|
|
103
|
+
function isFunctionDefinition(code) {
|
|
104
|
+
return /:\s*String/.test(code);
|
|
105
|
+
}
|
|
106
|
+
function getLineNumber(content, position) {
|
|
107
|
+
return content.substring(0, position).split("\n").length;
|
|
108
|
+
}
|
|
109
|
+
function detectWarnings(content, filePath) {
|
|
110
|
+
const warnings = [];
|
|
111
|
+
for (const { name, pattern, args, noPrefix, pseudoInMatch } of WARN_PATTERNS) {
|
|
112
|
+
pattern.lastIndex = 0;
|
|
113
|
+
for (const match of content.matchAll(pattern)) {
|
|
114
|
+
const fullMatch = match[0];
|
|
115
|
+
const position = match.index;
|
|
116
|
+
const line = getLineNumber(content, position);
|
|
117
|
+
if (isFunctionDefinition(fullMatch)) continue;
|
|
118
|
+
const extractedArgs = [];
|
|
119
|
+
if (args === 2) {
|
|
120
|
+
const startIdx = noPrefix ? 1 : 2;
|
|
121
|
+
extractedArgs.push(match[startIdx], match[startIdx + 1]);
|
|
122
|
+
} else if (args === 3) if (pseudoInMatch) extractedArgs.push(match[3], match[4]);
|
|
123
|
+
else {
|
|
124
|
+
const startIdx = noPrefix ? 1 : 2;
|
|
125
|
+
extractedArgs.push(match[startIdx], match[startIdx + 1], match[startIdx + 2]);
|
|
126
|
+
}
|
|
127
|
+
for (let i = 0; i < extractedArgs.length; i++) {
|
|
128
|
+
const arg = extractedArgs[i];
|
|
129
|
+
if (arg && !isStringLiteral(arg)) {
|
|
130
|
+
const argNum = pseudoInMatch ? i + 2 : i + 1;
|
|
131
|
+
warnings.push({
|
|
132
|
+
file: filePath,
|
|
133
|
+
line,
|
|
134
|
+
func: name,
|
|
135
|
+
code: fullMatch.trim(),
|
|
136
|
+
reason: `Argument ${argNum} is not a string literal: ${arg.trim()}`
|
|
137
|
+
});
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return warnings;
|
|
144
|
+
}
|
|
145
|
+
function extractFromContent(content) {
|
|
146
|
+
const base = /* @__PURE__ */ new Set();
|
|
147
|
+
const pseudo = [];
|
|
148
|
+
const media = [];
|
|
149
|
+
function extractBase(pattern) {
|
|
150
|
+
for (const match of content.matchAll(pattern)) base.add(`${match[1]}:${match[2]}`);
|
|
151
|
+
}
|
|
152
|
+
function extractStyles(pattern) {
|
|
153
|
+
for (const match of content.matchAll(pattern)) {
|
|
154
|
+
const pairs = match[1];
|
|
155
|
+
for (const pair of pairs.matchAll(STYLES_PAIR_PATTERN)) base.add(`${pair[1]}:${pair[2]}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function extractPseudo(pattern, pseudoSelector) {
|
|
159
|
+
for (const match of content.matchAll(pattern)) pseudo.push({
|
|
160
|
+
pseudo: pseudoSelector,
|
|
161
|
+
property: match[1],
|
|
162
|
+
value: match[2]
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function extractOn(pattern) {
|
|
166
|
+
for (const match of content.matchAll(pattern)) pseudo.push({
|
|
167
|
+
pseudo: match[1],
|
|
168
|
+
property: match[2],
|
|
169
|
+
value: match[3]
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function extractMedia(pattern, condition = null) {
|
|
173
|
+
for (const match of content.matchAll(pattern)) if (condition) media.push({
|
|
174
|
+
condition,
|
|
175
|
+
property: match[1],
|
|
176
|
+
value: match[2]
|
|
177
|
+
});
|
|
178
|
+
else media.push({
|
|
179
|
+
condition: match[1],
|
|
180
|
+
property: match[2],
|
|
181
|
+
value: match[3]
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
extractBase(CSS_PATTERN);
|
|
185
|
+
extractBase(UCSS_PATTERN);
|
|
186
|
+
extractStyles(STYLES_PATTERN);
|
|
187
|
+
extractStyles(USTYLES_PATTERN);
|
|
188
|
+
extractPseudo(HOVER_PATTERN, ":hover");
|
|
189
|
+
extractPseudo(UHOVER_PATTERN, ":hover");
|
|
190
|
+
extractPseudo(FOCUS_PATTERN, ":focus");
|
|
191
|
+
extractPseudo(UFOCUS_PATTERN, ":focus");
|
|
192
|
+
extractPseudo(ACTIVE_PATTERN, ":active");
|
|
193
|
+
extractPseudo(UACTIVE_PATTERN, ":active");
|
|
194
|
+
extractOn(ON_PATTERN);
|
|
195
|
+
extractOn(UON_PATTERN);
|
|
196
|
+
extractMedia(MEDIA_PATTERN);
|
|
197
|
+
extractMedia(AT_SM_PATTERN, "min-width:640px");
|
|
198
|
+
extractMedia(AT_MD_PATTERN, "min-width:768px");
|
|
199
|
+
extractMedia(UAT_MD_PATTERN, "min-width:768px");
|
|
200
|
+
extractMedia(AT_LG_PATTERN, "min-width:1024px");
|
|
201
|
+
extractMedia(UAT_LG_PATTERN, "min-width:1024px");
|
|
202
|
+
extractMedia(AT_XL_PATTERN, "min-width:1280px");
|
|
203
|
+
extractMedia(DARK_PATTERN, "prefers-color-scheme:dark");
|
|
204
|
+
extractMedia(UDARK_PATTERN, "prefers-color-scheme:dark");
|
|
205
|
+
return {
|
|
206
|
+
base,
|
|
207
|
+
pseudo,
|
|
208
|
+
media
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function findMbtFiles$1(dir) {
|
|
212
|
+
const files = [];
|
|
213
|
+
function walk(currentDir) {
|
|
214
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
215
|
+
for (const entry of entries) {
|
|
216
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
217
|
+
if (entry.isDirectory()) {
|
|
218
|
+
if (![
|
|
219
|
+
"node_modules",
|
|
220
|
+
".git",
|
|
221
|
+
"target",
|
|
222
|
+
".mooncakes"
|
|
223
|
+
].includes(entry.name)) walk(fullPath);
|
|
224
|
+
} else if (entry.isFile() && entry.name.endsWith(".mbt")) files.push(fullPath);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
walk(dir);
|
|
228
|
+
return files;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* DJB2 hash function - must match MoonBit registry.mbt implementation
|
|
232
|
+
*/
|
|
233
|
+
function djb2Hash(s) {
|
|
234
|
+
let hash = 5381;
|
|
235
|
+
for (let i = 0; i < s.length; i++) {
|
|
236
|
+
const c = s.charCodeAt(i);
|
|
237
|
+
hash = (hash << 5) + hash + c >>> 0;
|
|
238
|
+
}
|
|
239
|
+
return hash;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Convert hash to base36 string (0-9a-z)
|
|
243
|
+
* Takes lower 24 bits to keep class names short (4-5 chars)
|
|
244
|
+
*/
|
|
245
|
+
function toBase36(n) {
|
|
246
|
+
const chars = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
247
|
+
n = n & 16777215;
|
|
248
|
+
if (n === 0) return "0";
|
|
249
|
+
let result = "";
|
|
250
|
+
while (n > 0) {
|
|
251
|
+
result = chars[n % 36] + result;
|
|
252
|
+
n = Math.floor(n / 36);
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Generate class name from declaration hash
|
|
258
|
+
*/
|
|
259
|
+
function hashClassName(decl) {
|
|
260
|
+
return "_" + toBase36(djb2Hash(decl));
|
|
261
|
+
}
|
|
262
|
+
function generateCSS(styles, options = {}) {
|
|
263
|
+
const { pretty = false } = options;
|
|
264
|
+
const mapping = {};
|
|
265
|
+
const parts = [];
|
|
266
|
+
for (const decl of styles.base) {
|
|
267
|
+
const cls = hashClassName(decl);
|
|
268
|
+
mapping[decl] = cls;
|
|
269
|
+
if (pretty) parts.push(`.${cls} { ${decl} }`);
|
|
270
|
+
else parts.push(`.${cls}{${decl}}`);
|
|
271
|
+
}
|
|
272
|
+
for (const { pseudo, property, value } of styles.pseudo) {
|
|
273
|
+
const hashKey = `${pseudo}:${property}:${value}`;
|
|
274
|
+
const cls = hashClassName(hashKey);
|
|
275
|
+
mapping[hashKey] = cls;
|
|
276
|
+
if (pretty) parts.push(`.${cls}${pseudo} { ${property}: ${value} }`);
|
|
277
|
+
else parts.push(`.${cls}${pseudo}{${property}:${value}}`);
|
|
278
|
+
}
|
|
279
|
+
const mediaGroups = /* @__PURE__ */ new Map();
|
|
280
|
+
for (const { condition, property, value } of styles.media) {
|
|
281
|
+
if (!mediaGroups.has(condition)) mediaGroups.set(condition, []);
|
|
282
|
+
mediaGroups.get(condition).push({
|
|
283
|
+
property,
|
|
284
|
+
value
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
for (const [condition, declarations] of mediaGroups) {
|
|
288
|
+
const rules = [];
|
|
289
|
+
for (const { property, value } of declarations) {
|
|
290
|
+
const hashKey = `@media(${condition}):${property}:${value}`;
|
|
291
|
+
const cls = hashClassName(hashKey);
|
|
292
|
+
mapping[hashKey] = cls;
|
|
293
|
+
if (pretty) rules.push(` .${cls} { ${property}: ${value} }`);
|
|
294
|
+
else rules.push(`.${cls}{${property}:${value}}`);
|
|
295
|
+
}
|
|
296
|
+
if (pretty) parts.push(`@media (${condition}) {\n${rules.join("\n")}\n}`);
|
|
297
|
+
else parts.push(`@media(${condition}){${rules.join("")}}`);
|
|
298
|
+
}
|
|
299
|
+
const separator = pretty ? "\n" : "";
|
|
300
|
+
return {
|
|
301
|
+
css: parts.join(separator),
|
|
302
|
+
mapping
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function extract(dir, options = {}) {
|
|
306
|
+
const { pretty = false, warn = true, strict = false, verbose = false } = options;
|
|
307
|
+
const files = findMbtFiles$1(dir);
|
|
308
|
+
if (verbose) console.error(`Found ${files.length} .mbt files`);
|
|
309
|
+
const combined = {
|
|
310
|
+
base: /* @__PURE__ */ new Set(),
|
|
311
|
+
pseudo: [],
|
|
312
|
+
media: []
|
|
313
|
+
};
|
|
314
|
+
const allWarnings = [];
|
|
315
|
+
for (const file of files) {
|
|
316
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
317
|
+
const extracted = extractFromContent(content);
|
|
318
|
+
for (const decl of extracted.base) combined.base.add(decl);
|
|
319
|
+
combined.pseudo.push(...extracted.pseudo);
|
|
320
|
+
combined.media.push(...extracted.media);
|
|
321
|
+
if (warn) {
|
|
322
|
+
const warnings = detectWarnings(content, file);
|
|
323
|
+
allWarnings.push(...warnings);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (warn && allWarnings.length > 0 && verbose) {
|
|
327
|
+
console.error(`\n⚠️ ${allWarnings.length} warning(s): non-literal CSS arguments detected\n`);
|
|
328
|
+
for (const w of allWarnings) {
|
|
329
|
+
console.error(` ${w.file}:${w.line}`);
|
|
330
|
+
console.error(` ${w.func}(...) - ${w.reason}`);
|
|
331
|
+
console.error(` Code: ${w.code}`);
|
|
332
|
+
console.error("");
|
|
333
|
+
}
|
|
334
|
+
if (strict) throw new Error("Strict mode: non-literal CSS arguments detected");
|
|
335
|
+
}
|
|
336
|
+
if (verbose) {
|
|
337
|
+
console.error(`Extracted:`);
|
|
338
|
+
console.error(` - ${combined.base.size} base declarations`);
|
|
339
|
+
console.error(` - ${combined.pseudo.length} pseudo-class declarations`);
|
|
340
|
+
console.error(` - ${combined.media.length} media query declarations`);
|
|
341
|
+
}
|
|
342
|
+
const { css, mapping } = generateCSS(combined, { pretty });
|
|
343
|
+
return {
|
|
344
|
+
css,
|
|
345
|
+
mapping,
|
|
346
|
+
stats: {
|
|
347
|
+
base: combined.base.size,
|
|
348
|
+
pseudo: combined.pseudo.length,
|
|
349
|
+
media: combined.media.length
|
|
350
|
+
},
|
|
351
|
+
warnings: warn ? allWarnings : void 0
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Extract CSS with split output by file or directory.
|
|
356
|
+
* Shared CSS (used in 3+ files/dirs) is separated into a shared chunk.
|
|
357
|
+
*/
|
|
358
|
+
function extractSplit(dir, mode, options = {}) {
|
|
359
|
+
const { pretty = false, warn = true, strict = false, verbose = false, sharedThreshold = 3 } = options;
|
|
360
|
+
const files = findMbtFiles$1(dir);
|
|
361
|
+
if (verbose) console.error(`Found ${files.length} .mbt files`);
|
|
362
|
+
const declUsage = /* @__PURE__ */ new Map();
|
|
363
|
+
const pseudoUsage = /* @__PURE__ */ new Map();
|
|
364
|
+
const mediaUsage = /* @__PURE__ */ new Map();
|
|
365
|
+
const perChunk = /* @__PURE__ */ new Map();
|
|
366
|
+
const allWarnings = [];
|
|
367
|
+
for (const file of files) {
|
|
368
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
369
|
+
const extracted = extractFromContent(content);
|
|
370
|
+
const relPath = path.relative(dir, file);
|
|
371
|
+
const chunkKey = mode === "file" ? relPath : path.dirname(relPath) || ".";
|
|
372
|
+
if (!perChunk.has(chunkKey)) perChunk.set(chunkKey, {
|
|
373
|
+
base: /* @__PURE__ */ new Set(),
|
|
374
|
+
pseudo: [],
|
|
375
|
+
media: []
|
|
376
|
+
});
|
|
377
|
+
const chunk = perChunk.get(chunkKey);
|
|
378
|
+
for (const decl of extracted.base) {
|
|
379
|
+
chunk.base.add(decl);
|
|
380
|
+
if (!declUsage.has(decl)) declUsage.set(decl, /* @__PURE__ */ new Set());
|
|
381
|
+
declUsage.get(decl).add(chunkKey);
|
|
382
|
+
}
|
|
383
|
+
for (const p of extracted.pseudo) {
|
|
384
|
+
const key = `${p.pseudo}:${p.property}:${p.value}`;
|
|
385
|
+
chunk.pseudo.push(p);
|
|
386
|
+
if (!pseudoUsage.has(key)) pseudoUsage.set(key, /* @__PURE__ */ new Set());
|
|
387
|
+
pseudoUsage.get(key).add(chunkKey);
|
|
388
|
+
}
|
|
389
|
+
for (const m of extracted.media) {
|
|
390
|
+
const key = `@media(${m.condition}):${m.property}:${m.value}`;
|
|
391
|
+
chunk.media.push(m);
|
|
392
|
+
if (!mediaUsage.has(key)) mediaUsage.set(key, /* @__PURE__ */ new Set());
|
|
393
|
+
mediaUsage.get(key).add(chunkKey);
|
|
394
|
+
}
|
|
395
|
+
if (warn) {
|
|
396
|
+
const warnings = detectWarnings(content, file);
|
|
397
|
+
allWarnings.push(...warnings);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const sharedBase = /* @__PURE__ */ new Set();
|
|
401
|
+
const sharedPseudo = [];
|
|
402
|
+
const sharedMedia = [];
|
|
403
|
+
for (const [decl, chunks] of declUsage) if (chunks.size >= sharedThreshold) sharedBase.add(decl);
|
|
404
|
+
const seenPseudo = /* @__PURE__ */ new Set();
|
|
405
|
+
for (const [key, chunks] of pseudoUsage) if (chunks.size >= sharedThreshold && !seenPseudo.has(key)) {
|
|
406
|
+
seenPseudo.add(key);
|
|
407
|
+
const [pseudo, property, value] = key.split(":");
|
|
408
|
+
sharedPseudo.push({
|
|
409
|
+
pseudo,
|
|
410
|
+
property,
|
|
411
|
+
value
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
const seenMedia = /* @__PURE__ */ new Set();
|
|
415
|
+
for (const [key, chunks] of mediaUsage) if (chunks.size >= sharedThreshold && !seenMedia.has(key)) {
|
|
416
|
+
seenMedia.add(key);
|
|
417
|
+
const match = key.match(/^@media\(([^)]+)\):([^:]+):(.+)$/);
|
|
418
|
+
if (match) sharedMedia.push({
|
|
419
|
+
condition: match[1],
|
|
420
|
+
property: match[2],
|
|
421
|
+
value: match[3]
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
const finalChunks = /* @__PURE__ */ new Map();
|
|
425
|
+
const chunkStats = /* @__PURE__ */ new Map();
|
|
426
|
+
for (const [chunkKey, chunk] of perChunk) {
|
|
427
|
+
const chunkOnlyBase = /* @__PURE__ */ new Set();
|
|
428
|
+
for (const decl of chunk.base) if (!sharedBase.has(decl)) chunkOnlyBase.add(decl);
|
|
429
|
+
const chunkOnlyPseudo = chunk.pseudo.filter((p) => {
|
|
430
|
+
const key = `${p.pseudo}:${p.property}:${p.value}`;
|
|
431
|
+
return !seenPseudo.has(key);
|
|
432
|
+
});
|
|
433
|
+
const chunkOnlyMedia = chunk.media.filter((m) => {
|
|
434
|
+
const key = `@media(${m.condition}):${m.property}:${m.value}`;
|
|
435
|
+
return !seenMedia.has(key);
|
|
436
|
+
});
|
|
437
|
+
const chunkStyles = {
|
|
438
|
+
base: chunkOnlyBase,
|
|
439
|
+
pseudo: chunkOnlyPseudo,
|
|
440
|
+
media: chunkOnlyMedia
|
|
441
|
+
};
|
|
442
|
+
const { css } = generateCSS(chunkStyles, { pretty });
|
|
443
|
+
finalChunks.set(chunkKey, {
|
|
444
|
+
css,
|
|
445
|
+
styles: chunkStyles
|
|
446
|
+
});
|
|
447
|
+
chunkStats.set(chunkKey, {
|
|
448
|
+
base: chunkOnlyBase.size,
|
|
449
|
+
pseudo: chunkOnlyPseudo.length,
|
|
450
|
+
media: chunkOnlyMedia.length
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
const sharedStyles = {
|
|
454
|
+
base: sharedBase,
|
|
455
|
+
pseudo: sharedPseudo,
|
|
456
|
+
media: sharedMedia
|
|
457
|
+
};
|
|
458
|
+
const { css: sharedCss, mapping } = generateCSS(sharedStyles, { pretty });
|
|
459
|
+
const combinedStyles = {
|
|
460
|
+
base: /* @__PURE__ */ new Set(),
|
|
461
|
+
pseudo: [],
|
|
462
|
+
media: []
|
|
463
|
+
};
|
|
464
|
+
for (const decl of sharedBase) combinedStyles.base.add(decl);
|
|
465
|
+
for (const [, chunk] of finalChunks) {
|
|
466
|
+
for (const decl of chunk.styles.base) combinedStyles.base.add(decl);
|
|
467
|
+
combinedStyles.pseudo.push(...chunk.styles.pseudo);
|
|
468
|
+
combinedStyles.media.push(...chunk.styles.media);
|
|
469
|
+
}
|
|
470
|
+
combinedStyles.pseudo.push(...sharedPseudo);
|
|
471
|
+
combinedStyles.media.push(...sharedMedia);
|
|
472
|
+
const { css: combinedCss, mapping: fullMapping } = generateCSS(combinedStyles, { pretty });
|
|
473
|
+
if (verbose) {
|
|
474
|
+
console.error(`Split mode: ${mode}`);
|
|
475
|
+
console.error(`Chunks: ${finalChunks.size}`);
|
|
476
|
+
console.error(`Shared declarations (≥${sharedThreshold} usages):`);
|
|
477
|
+
console.error(` - ${sharedBase.size} base`);
|
|
478
|
+
console.error(` - ${sharedPseudo.length} pseudo`);
|
|
479
|
+
console.error(` - ${sharedMedia.length} media`);
|
|
480
|
+
}
|
|
481
|
+
if (warn && allWarnings.length > 0 && verbose) console.error(`\n⚠️ ${allWarnings.length} warning(s)\n`);
|
|
482
|
+
if (strict && allWarnings.length > 0) throw new Error("Strict mode: non-literal CSS arguments detected");
|
|
483
|
+
return {
|
|
484
|
+
chunks: finalChunks,
|
|
485
|
+
shared: {
|
|
486
|
+
css: sharedCss,
|
|
487
|
+
styles: sharedStyles
|
|
488
|
+
},
|
|
489
|
+
combined: combinedCss,
|
|
490
|
+
mapping: fullMapping,
|
|
491
|
+
stats: chunkStats,
|
|
492
|
+
warnings: warn ? allWarnings : void 0
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
//#endregion
|
|
497
|
+
//#region src/css/minify.ts
|
|
498
|
+
/**
|
|
499
|
+
* Minify CSS content
|
|
500
|
+
*/
|
|
501
|
+
function minifyCSS(css) {
|
|
502
|
+
return css.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").replace(/\s*([{};:,])\s*/g, "$1").replace(/;}/g, "}").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").trim();
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Format file size
|
|
506
|
+
*/
|
|
507
|
+
function formatSize(bytes) {
|
|
508
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
509
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Minify CSS and return result with stats
|
|
513
|
+
*/
|
|
514
|
+
function minify(css, options = {}) {
|
|
515
|
+
const originalSize = Buffer.byteLength(css, "utf-8");
|
|
516
|
+
const minified = minifyCSS(css);
|
|
517
|
+
const minifiedSize = Buffer.byteLength(minified, "utf-8");
|
|
518
|
+
const reduction = (1 - minifiedSize / originalSize) * 100;
|
|
519
|
+
if (options.verbose) console.error(`Minified: ${formatSize(originalSize)} → ${formatSize(minifiedSize)} (${reduction.toFixed(1)}% reduction)`);
|
|
520
|
+
return {
|
|
521
|
+
minified,
|
|
522
|
+
originalSize,
|
|
523
|
+
minifiedSize,
|
|
524
|
+
reduction
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
//#endregion
|
|
529
|
+
//#region src/css/inline.ts
|
|
530
|
+
/**
|
|
531
|
+
* CSS Class Name Inliner
|
|
532
|
+
*
|
|
533
|
+
* Replaces CSS utility function calls with pre-computed class names.
|
|
534
|
+
* This enables true zero-runtime CSS by eliminating runtime style registration.
|
|
535
|
+
*/
|
|
536
|
+
const REGISTER_DECL_PATTERN = /mizchi\$luna\$luna\$css\$\$register_decl\s*\(\s*"([^"]+)"\s*\)/g;
|
|
537
|
+
const REGISTER_PSEUDO_PATTERN = /mizchi\$luna\$luna\$css\$\$register_pseudo\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
538
|
+
const REGISTER_MEDIA_PATTERN = /mizchi\$luna\$luna\$css\$\$register_media\s*\(\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\)/g;
|
|
539
|
+
/**
|
|
540
|
+
* Replace CSS function calls with pre-computed class names
|
|
541
|
+
*/
|
|
542
|
+
function inlineCSS(code, mapping, options = {}) {
|
|
543
|
+
const { verbose = false } = options;
|
|
544
|
+
const replacements = [];
|
|
545
|
+
const originalSize = Buffer.byteLength(code, "utf-8");
|
|
546
|
+
let result = code;
|
|
547
|
+
result = result.replace(REGISTER_DECL_PATTERN, (match, decl) => {
|
|
548
|
+
const className = mapping[decl];
|
|
549
|
+
if (className) {
|
|
550
|
+
replacements.push({
|
|
551
|
+
type: "base",
|
|
552
|
+
from: match,
|
|
553
|
+
to: `"${className}"`,
|
|
554
|
+
decl
|
|
555
|
+
});
|
|
556
|
+
return `"${className}"`;
|
|
557
|
+
}
|
|
558
|
+
if (verbose) console.error(`Warning: No mapping for base declaration: ${decl}`);
|
|
559
|
+
return match;
|
|
560
|
+
});
|
|
561
|
+
result = result.replace(REGISTER_PSEUDO_PATTERN, (match, pseudo, property, value) => {
|
|
562
|
+
const key = `${pseudo}:${property}:${value}`;
|
|
563
|
+
const className = mapping[key];
|
|
564
|
+
if (className) {
|
|
565
|
+
replacements.push({
|
|
566
|
+
type: "pseudo",
|
|
567
|
+
from: match,
|
|
568
|
+
to: `"${className}"`,
|
|
569
|
+
key
|
|
570
|
+
});
|
|
571
|
+
return `"${className}"`;
|
|
572
|
+
}
|
|
573
|
+
if (verbose) console.error(`Warning: No mapping for pseudo: ${key}`);
|
|
574
|
+
return match;
|
|
575
|
+
});
|
|
576
|
+
result = result.replace(REGISTER_MEDIA_PATTERN, (match, condition, property, value) => {
|
|
577
|
+
const key = `@media(${condition}):${property}:${value}`;
|
|
578
|
+
const className = mapping[key];
|
|
579
|
+
if (className) {
|
|
580
|
+
replacements.push({
|
|
581
|
+
type: "media",
|
|
582
|
+
from: match,
|
|
583
|
+
to: `"${className}"`,
|
|
584
|
+
key
|
|
585
|
+
});
|
|
586
|
+
return `"${className}"`;
|
|
587
|
+
}
|
|
588
|
+
if (verbose) console.error(`Warning: No mapping for media: ${key}`);
|
|
589
|
+
return match;
|
|
590
|
+
});
|
|
591
|
+
const finalSize = Buffer.byteLength(result, "utf-8");
|
|
592
|
+
return {
|
|
593
|
+
code: result,
|
|
594
|
+
replacements,
|
|
595
|
+
originalSize,
|
|
596
|
+
finalSize
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Remove CSS registry code (dead code after inlining)
|
|
601
|
+
*/
|
|
602
|
+
function removeRegistryCode(code) {
|
|
603
|
+
const patterns = [
|
|
604
|
+
/const mizchi\$luna\$luna\$css\$\$registry\s*=\s*\{[^}]+\};?\n?/g,
|
|
605
|
+
/const mizchi\$luna\$luna\$css\$\$pseudo_registry\s*=\s*\{[^}]+\};?\n?/g,
|
|
606
|
+
/const mizchi\$luna\$luna\$css\$\$media_registry\s*=\s*\{[^}]+\};?\n?/g,
|
|
607
|
+
/const mizchi\$luna\$luna\$css\$\$class_chars\s*=\s*"[^"]+";?\n?/g,
|
|
608
|
+
/const mizchi\$luna\$luna\$css\$\$[a-z_]+\$46\$42\$bind[^;]+;?\n?/g
|
|
609
|
+
];
|
|
610
|
+
let result = code;
|
|
611
|
+
for (const pattern of patterns) result = result.replace(pattern, "");
|
|
612
|
+
return result;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Inline CSS with mapping extracted from source directory
|
|
616
|
+
*/
|
|
617
|
+
function inlineFromSource(code, srcDir, options = {}) {
|
|
618
|
+
const { mapping } = extract(srcDir, { warn: false });
|
|
619
|
+
let result = inlineCSS(code, mapping, options);
|
|
620
|
+
if (options.removeRegistry) result = {
|
|
621
|
+
...result,
|
|
622
|
+
code: removeRegistryCode(result.code),
|
|
623
|
+
finalSize: Buffer.byteLength(removeRegistryCode(result.code), "utf-8")
|
|
624
|
+
};
|
|
625
|
+
return result;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
//#endregion
|
|
629
|
+
//#region src/css/inject.ts
|
|
630
|
+
/**
|
|
631
|
+
* CSS Injection into HTML files
|
|
632
|
+
*
|
|
633
|
+
* Replaces CSS between markers in HTML files with extracted CSS.
|
|
634
|
+
* Supports inline embedding or external file generation.
|
|
635
|
+
*/
|
|
636
|
+
const CSS_START_MARKER = "/* LUNA_CSS_START */";
|
|
637
|
+
const CSS_END_MARKER = "/* LUNA_CSS_END */";
|
|
638
|
+
const LINK_MARKER = "<!-- LUNA_CSS_LINK -->";
|
|
639
|
+
/**
|
|
640
|
+
* Inject extracted CSS into HTML file between markers
|
|
641
|
+
*/
|
|
642
|
+
function injectCssToHtml(options) {
|
|
643
|
+
const { srcDir, htmlFile, mode = "inline", threshold = 4096, cssFileName = "luna.css", verbose = false } = options;
|
|
644
|
+
const { css } = extract(srcDir, { warn: false });
|
|
645
|
+
if (verbose) console.error(`Extracted CSS: ${css.length} bytes`);
|
|
646
|
+
const html = fs.readFileSync(htmlFile, "utf-8");
|
|
647
|
+
let actualMode = mode;
|
|
648
|
+
if (mode === "auto") {
|
|
649
|
+
actualMode = css.length > threshold ? "external" : "inline";
|
|
650
|
+
if (verbose) console.error(`Auto mode: ${css.length} bytes ${css.length > threshold ? ">" : "<="} ${threshold} threshold → ${actualMode}`);
|
|
651
|
+
}
|
|
652
|
+
if (actualMode === "external") return injectExternal(html, css, htmlFile, cssFileName, verbose);
|
|
653
|
+
else return injectInline(html, css, htmlFile, verbose);
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Inject CSS inline between markers
|
|
657
|
+
*/
|
|
658
|
+
function injectInline(html, css, htmlFile, verbose) {
|
|
659
|
+
const startIdx = html.indexOf(CSS_START_MARKER);
|
|
660
|
+
const endIdx = html.indexOf(CSS_END_MARKER);
|
|
661
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
662
|
+
if (verbose) console.error(`Warning: Markers not found in ${htmlFile}. Add /* LUNA_CSS_START */ and /* LUNA_CSS_END */ to your HTML.`);
|
|
663
|
+
return {
|
|
664
|
+
html,
|
|
665
|
+
css,
|
|
666
|
+
replaced: false,
|
|
667
|
+
mode: "inline"
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
const newHtml = `${html.substring(0, startIdx + 20)}\n ${css}\n ${html.substring(endIdx)}`;
|
|
671
|
+
if (verbose) console.error(`Injected CSS inline into ${htmlFile}`);
|
|
672
|
+
return {
|
|
673
|
+
html: newHtml,
|
|
674
|
+
css,
|
|
675
|
+
replaced: true,
|
|
676
|
+
mode: "inline"
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Create external CSS file and add link tag
|
|
681
|
+
*/
|
|
682
|
+
function injectExternal(html, css, htmlFile, cssFileName, verbose) {
|
|
683
|
+
const htmlDir = path.dirname(htmlFile);
|
|
684
|
+
const cssFilePath = path.join(htmlDir, cssFileName);
|
|
685
|
+
const hasLinkMarker = html.includes(LINK_MARKER);
|
|
686
|
+
const startIdx = html.indexOf(CSS_START_MARKER);
|
|
687
|
+
const endIdx = html.indexOf(CSS_END_MARKER);
|
|
688
|
+
let newHtml = html;
|
|
689
|
+
let replaced = false;
|
|
690
|
+
if (hasLinkMarker) {
|
|
691
|
+
newHtml = html.replace(LINK_MARKER, `<link rel="stylesheet" href="${cssFileName}">`);
|
|
692
|
+
replaced = true;
|
|
693
|
+
} else if (startIdx !== -1 && endIdx !== -1) {
|
|
694
|
+
newHtml = `${html.substring(0, startIdx + 20)}\n /* External: ${cssFileName} */\n ${html.substring(endIdx)}`;
|
|
695
|
+
if (!newHtml.includes(`href="${cssFileName}"`)) newHtml = newHtml.replace("</head>", ` <link rel="stylesheet" href="${cssFileName}">\n</head>`);
|
|
696
|
+
replaced = true;
|
|
697
|
+
} else if (verbose) console.error(`Warning: No markers found in ${htmlFile}. Add <!-- LUNA_CSS_LINK --> or /* LUNA_CSS_START/END */ markers.`);
|
|
698
|
+
if (verbose && replaced) console.error(`Created external CSS: ${cssFilePath}`);
|
|
699
|
+
return {
|
|
700
|
+
html: newHtml,
|
|
701
|
+
css,
|
|
702
|
+
replaced,
|
|
703
|
+
cssFile: cssFilePath,
|
|
704
|
+
mode: "external"
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Inject CSS and write to file(s)
|
|
709
|
+
*/
|
|
710
|
+
function injectAndWrite(options) {
|
|
711
|
+
const result = injectCssToHtml(options);
|
|
712
|
+
const outputFile = options.outputFile || options.htmlFile;
|
|
713
|
+
if (result.replaced) {
|
|
714
|
+
fs.writeFileSync(outputFile, result.html);
|
|
715
|
+
if (options.verbose) console.error(`Written HTML to: ${outputFile}`);
|
|
716
|
+
if (result.mode === "external" && result.cssFile) {
|
|
717
|
+
fs.writeFileSync(result.cssFile, result.css);
|
|
718
|
+
if (options.verbose) console.error(`Written CSS to: ${result.cssFile}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return result;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
//#endregion
|
|
725
|
+
//#region src/css-optimizer/moonbit-analyzer.ts
|
|
726
|
+
/**
|
|
727
|
+
* MoonBit Static Analyzer Integration
|
|
728
|
+
*
|
|
729
|
+
* Calls the MoonBit-based CSS static analyzer to extract class co-occurrences
|
|
730
|
+
* from MoonBit source files.
|
|
731
|
+
*/
|
|
732
|
+
let analyzerModule = null;
|
|
733
|
+
/**
|
|
734
|
+
* Get the path to the MoonBit analyzer JS module
|
|
735
|
+
*/
|
|
736
|
+
function getAnalyzerPath() {
|
|
737
|
+
const __dirname = path$1.dirname(fileURLToPath(import.meta.url));
|
|
738
|
+
const candidates = [
|
|
739
|
+
path$1.resolve(__dirname, "../../../target/js/release/build/luna/css/analyzer/analyzer.js"),
|
|
740
|
+
path$1.resolve(__dirname, "../../../../target/js/release/build/luna/css/analyzer/analyzer.js"),
|
|
741
|
+
path$1.resolve(__dirname, "../moonbit/analyzer.js")
|
|
742
|
+
];
|
|
743
|
+
for (const candidate of candidates) if (fs$1.existsSync(candidate)) return candidate;
|
|
744
|
+
throw new Error(`MoonBit analyzer not found. Tried:\n${candidates.join("\n")}\nRun 'moon build --target js src/luna/css/analyzer' first.`);
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Load the MoonBit analyzer module
|
|
748
|
+
*/
|
|
749
|
+
async function loadAnalyzer() {
|
|
750
|
+
if (analyzerModule) return analyzerModule;
|
|
751
|
+
analyzerModule = await import(getAnalyzerPath());
|
|
752
|
+
return analyzerModule;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Analyze a single MoonBit source file
|
|
756
|
+
*/
|
|
757
|
+
async function analyzeFile(source, filePath) {
|
|
758
|
+
const analyzer = await loadAnalyzer();
|
|
759
|
+
if (!analyzer) throw new Error("Failed to load MoonBit analyzer");
|
|
760
|
+
const jsonResult = analyzer.analyze_file_json(source, filePath);
|
|
761
|
+
return JSON.parse(jsonResult);
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Analyze all .mbt files in a directory
|
|
765
|
+
*/
|
|
766
|
+
async function analyzeDirectory(dir, options = {}) {
|
|
767
|
+
const { recursive = true } = options;
|
|
768
|
+
const allCooccurrences = [];
|
|
769
|
+
const allWarnings = [];
|
|
770
|
+
const files = findMbtFiles(dir, recursive);
|
|
771
|
+
for (const file of files) {
|
|
772
|
+
const result = await analyzeFile(fs$1.readFileSync(file, "utf-8"), file);
|
|
773
|
+
allCooccurrences.push(...result.cooccurrences);
|
|
774
|
+
allWarnings.push(...result.warnings);
|
|
775
|
+
}
|
|
776
|
+
return {
|
|
777
|
+
cooccurrences: allCooccurrences,
|
|
778
|
+
warnings: allWarnings
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Find all .mbt files in a directory
|
|
783
|
+
*/
|
|
784
|
+
function findMbtFiles(dir, recursive) {
|
|
785
|
+
const files = [];
|
|
786
|
+
function walk(currentDir) {
|
|
787
|
+
const entries = fs$1.readdirSync(currentDir, { withFileTypes: true });
|
|
788
|
+
for (const entry of entries) {
|
|
789
|
+
const fullPath = path$1.join(currentDir, entry.name);
|
|
790
|
+
if (entry.isDirectory()) {
|
|
791
|
+
if (entry.name === "node_modules" || entry.name === "target" || entry.name === ".git" || entry.name === ".mooncakes") continue;
|
|
792
|
+
if (recursive) walk(fullPath);
|
|
793
|
+
} else if (entry.isFile() && entry.name.endsWith(".mbt")) {
|
|
794
|
+
if (!entry.name.endsWith("_test.mbt")) files.push(fullPath);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
walk(dir);
|
|
799
|
+
return files;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
//#endregion
|
|
6
803
|
//#region bin/cli.ts
|
|
7
|
-
path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
function
|
|
804
|
+
path$1.dirname(fileURLToPath(import.meta.url));
|
|
805
|
+
function printMainHelp() {
|
|
9
806
|
console.log(`
|
|
10
807
|
@luna_ui/luna CLI
|
|
11
808
|
|
|
12
809
|
Usage:
|
|
13
|
-
|
|
810
|
+
luna <command> [options]
|
|
811
|
+
|
|
812
|
+
Commands:
|
|
813
|
+
new <name> Create a new Luna project
|
|
814
|
+
css CSS utilities (extract, minify, inline)
|
|
815
|
+
|
|
816
|
+
Options:
|
|
817
|
+
--help, -h Show this help message
|
|
818
|
+
|
|
819
|
+
Examples:
|
|
820
|
+
luna new myapp # Create TSX project
|
|
821
|
+
luna new myapp --mbt # Create MoonBit project
|
|
822
|
+
luna css extract src # Extract CSS from source
|
|
823
|
+
luna css minify style.css # Minify CSS file
|
|
824
|
+
`);
|
|
825
|
+
}
|
|
826
|
+
function printNewHelp() {
|
|
827
|
+
console.log(`
|
|
828
|
+
luna new - Create a new Luna project
|
|
829
|
+
|
|
830
|
+
Usage:
|
|
831
|
+
luna new <project-name> [options]
|
|
14
832
|
|
|
15
833
|
Options:
|
|
16
834
|
--mbt Generate MoonBit template (default: TSX)
|
|
17
835
|
--help, -h Show this help message
|
|
18
836
|
|
|
19
837
|
Examples:
|
|
20
|
-
|
|
21
|
-
|
|
838
|
+
luna new myapp # TSX template
|
|
839
|
+
luna new myapp --mbt # MoonBit template
|
|
840
|
+
`);
|
|
841
|
+
}
|
|
842
|
+
function printCssHelp() {
|
|
843
|
+
console.log(`
|
|
844
|
+
luna css - CSS utilities
|
|
845
|
+
|
|
846
|
+
Usage:
|
|
847
|
+
luna css <subcommand> [options]
|
|
848
|
+
|
|
849
|
+
Subcommands:
|
|
850
|
+
extract <dir> Extract CSS from .mbt files
|
|
851
|
+
minify <file> Minify CSS file
|
|
852
|
+
inline <file> Inline CSS class names into compiled JS
|
|
853
|
+
inject <html> Inject extracted CSS into HTML file
|
|
854
|
+
analyze-mbt <dir> Analyze MoonBit source for CSS co-occurrences
|
|
855
|
+
|
|
856
|
+
Extract Options:
|
|
857
|
+
-o, --output <file> Output file (default: stdout)
|
|
858
|
+
--pretty Pretty print output
|
|
859
|
+
--json Output as JSON with mapping
|
|
860
|
+
-v, --verbose Show extraction details
|
|
861
|
+
--no-warn Disable non-literal argument warnings
|
|
862
|
+
--strict Exit with error if warnings found
|
|
863
|
+
--split Split CSS by file (outputs to --output-dir)
|
|
864
|
+
--split-dir Split CSS by directory (outputs to --output-dir)
|
|
865
|
+
--output-dir <dir> Output directory for split mode
|
|
866
|
+
--shared-threshold <n> Min usages to be "shared" (default: 3)
|
|
867
|
+
|
|
868
|
+
Minify Options:
|
|
869
|
+
-o, --output <file> Output file (default: stdout)
|
|
870
|
+
-v, --verbose Show minification stats
|
|
871
|
+
|
|
872
|
+
Inline Options:
|
|
873
|
+
-m, --mapping <file> JSON file with css -> classname mapping
|
|
874
|
+
--extract-from <dir> Extract mapping from source directory
|
|
875
|
+
-o, --output <file> Output file (default: stdout)
|
|
876
|
+
--remove-registry Remove CSS registry code after inlining
|
|
877
|
+
-v, --verbose Show replacement details
|
|
878
|
+
--dry-run Show what would be replaced
|
|
879
|
+
|
|
880
|
+
Inject Options:
|
|
881
|
+
--src <dir> Source directory for CSS extraction (required)
|
|
882
|
+
-o, --output <file> Output file (default: modify in-place)
|
|
883
|
+
-m, --mode <mode> Output mode: inline, external, or auto (default: inline)
|
|
884
|
+
-t, --threshold <n> Size threshold for auto mode in bytes (default: 4096)
|
|
885
|
+
--css-file <name> CSS filename for external mode (default: luna.css)
|
|
886
|
+
-v, --verbose Show injection details
|
|
887
|
+
|
|
888
|
+
Analyze-mbt Options:
|
|
889
|
+
-o, --output <file> Output file (default: stdout)
|
|
890
|
+
--no-recursive Don't search recursively
|
|
891
|
+
-v, --verbose Show analysis details
|
|
892
|
+
|
|
893
|
+
Examples:
|
|
894
|
+
luna css extract src -o dist/styles.css
|
|
895
|
+
luna css extract src --json -o mapping.json
|
|
896
|
+
luna css extract src --split --output-dir dist/css
|
|
897
|
+
luna css extract src --split-dir --output-dir dist/css --shared-threshold 2
|
|
898
|
+
luna css minify input.css -o output.min.css
|
|
899
|
+
luna css inline bundle.js --extract-from src -o bundle.inlined.js
|
|
900
|
+
luna css inject index.html --src src
|
|
901
|
+
luna css inject index.html --src src --mode external --css-file styles.css
|
|
902
|
+
luna css inject index.html --src src --mode auto --threshold 2048
|
|
903
|
+
luna css analyze-mbt src/luna -o cooccurrences.json
|
|
22
904
|
`);
|
|
23
905
|
}
|
|
24
906
|
function getTsxTemplates(projectName) {
|
|
@@ -303,35 +1185,28 @@ target
|
|
|
303
1185
|
];
|
|
304
1186
|
}
|
|
305
1187
|
function createProject(projectName, templates, targetDir) {
|
|
306
|
-
if (fs.existsSync(targetDir)) {
|
|
1188
|
+
if (fs$1.existsSync(targetDir)) {
|
|
307
1189
|
console.error(`Error: Directory "${projectName}" already exists.`);
|
|
308
1190
|
process.exit(1);
|
|
309
1191
|
}
|
|
310
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
1192
|
+
fs$1.mkdirSync(targetDir, { recursive: true });
|
|
311
1193
|
for (const template of templates) {
|
|
312
|
-
const filePath = path.join(targetDir, template.path);
|
|
313
|
-
const dir = path.dirname(filePath);
|
|
314
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
315
|
-
fs.writeFileSync(filePath, template.content);
|
|
1194
|
+
const filePath = path$1.join(targetDir, template.path);
|
|
1195
|
+
const dir = path$1.dirname(filePath);
|
|
1196
|
+
if (!fs$1.existsSync(dir)) fs$1.mkdirSync(dir, { recursive: true });
|
|
1197
|
+
fs$1.writeFileSync(filePath, template.content);
|
|
316
1198
|
console.log(` Created: ${template.path}`);
|
|
317
1199
|
}
|
|
318
1200
|
}
|
|
319
|
-
function
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
printHelp();
|
|
1201
|
+
function handleNew(args) {
|
|
1202
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
1203
|
+
printNewHelp();
|
|
323
1204
|
process.exit(0);
|
|
324
1205
|
}
|
|
325
|
-
const
|
|
326
|
-
if (command !== "new") {
|
|
327
|
-
console.error(`Unknown command: ${command}`);
|
|
328
|
-
printHelp();
|
|
329
|
-
process.exit(1);
|
|
330
|
-
}
|
|
331
|
-
const projectName = args[1];
|
|
1206
|
+
const projectName = args.find((a) => !a.startsWith("-"));
|
|
332
1207
|
if (!projectName) {
|
|
333
1208
|
console.error("Error: Project name is required.");
|
|
334
|
-
|
|
1209
|
+
printNewHelp();
|
|
335
1210
|
process.exit(1);
|
|
336
1211
|
}
|
|
337
1212
|
if (!/^[a-zA-Z0-9_-]+$/.test(projectName)) {
|
|
@@ -339,7 +1214,7 @@ function main() {
|
|
|
339
1214
|
process.exit(1);
|
|
340
1215
|
}
|
|
341
1216
|
const useMbt = args.includes("--mbt");
|
|
342
|
-
const targetDir = path.resolve(process.cwd(), projectName);
|
|
1217
|
+
const targetDir = path$1.resolve(process.cwd(), projectName);
|
|
343
1218
|
console.log(`\nCreating ${useMbt ? "MoonBit" : "TSX"} project: ${projectName}\n`);
|
|
344
1219
|
createProject(projectName, useMbt ? getMbtTemplates(projectName) : getTsxTemplates(projectName), targetDir);
|
|
345
1220
|
console.log(`\nDone! To get started:\n`);
|
|
@@ -355,7 +1230,369 @@ function main() {
|
|
|
355
1230
|
}
|
|
356
1231
|
console.log();
|
|
357
1232
|
}
|
|
358
|
-
|
|
1233
|
+
function handleCssExtract(args) {
|
|
1234
|
+
let dir = ".";
|
|
1235
|
+
let outputFile = null;
|
|
1236
|
+
let outputDir = null;
|
|
1237
|
+
let pretty = false;
|
|
1238
|
+
let jsonOutput = false;
|
|
1239
|
+
let verbose = false;
|
|
1240
|
+
let warn = true;
|
|
1241
|
+
let strict = false;
|
|
1242
|
+
let splitMode = null;
|
|
1243
|
+
let sharedThreshold = 3;
|
|
1244
|
+
for (let i = 0; i < args.length; i++) {
|
|
1245
|
+
const arg = args[i];
|
|
1246
|
+
if (arg === "--output" || arg === "-o") outputFile = args[++i];
|
|
1247
|
+
else if (arg === "--output-dir") outputDir = args[++i];
|
|
1248
|
+
else if (arg === "--pretty") pretty = true;
|
|
1249
|
+
else if (arg === "--json") jsonOutput = true;
|
|
1250
|
+
else if (arg === "--verbose" || arg === "-v") verbose = true;
|
|
1251
|
+
else if (arg === "--no-warn") warn = false;
|
|
1252
|
+
else if (arg === "--strict") {
|
|
1253
|
+
strict = true;
|
|
1254
|
+
warn = true;
|
|
1255
|
+
} else if (arg === "--split") splitMode = "file";
|
|
1256
|
+
else if (arg === "--split-dir") splitMode = "dir";
|
|
1257
|
+
else if (arg === "--shared-threshold") {
|
|
1258
|
+
sharedThreshold = parseInt(args[++i], 10);
|
|
1259
|
+
if (isNaN(sharedThreshold) || sharedThreshold < 1) {
|
|
1260
|
+
console.error("Error: --shared-threshold must be a positive number");
|
|
1261
|
+
process.exit(1);
|
|
1262
|
+
}
|
|
1263
|
+
} else if (!arg.startsWith("-")) dir = arg;
|
|
1264
|
+
}
|
|
1265
|
+
try {
|
|
1266
|
+
if (splitMode) {
|
|
1267
|
+
if (!outputDir) {
|
|
1268
|
+
console.error("Error: --output-dir is required for split mode");
|
|
1269
|
+
process.exit(1);
|
|
1270
|
+
}
|
|
1271
|
+
const result$1 = extractSplit(dir, splitMode, {
|
|
1272
|
+
pretty,
|
|
1273
|
+
warn,
|
|
1274
|
+
strict,
|
|
1275
|
+
verbose,
|
|
1276
|
+
sharedThreshold
|
|
1277
|
+
});
|
|
1278
|
+
fs$1.mkdirSync(outputDir, { recursive: true });
|
|
1279
|
+
const sharedPath = path$1.join(outputDir, "_shared.css");
|
|
1280
|
+
fs$1.writeFileSync(sharedPath, result$1.shared.css);
|
|
1281
|
+
if (verbose) console.error(`Written shared CSS to ${sharedPath}`);
|
|
1282
|
+
for (const [chunkKey, chunk] of result$1.chunks) {
|
|
1283
|
+
if (!chunk.css) continue;
|
|
1284
|
+
let chunkPath;
|
|
1285
|
+
if (splitMode === "file") chunkPath = path$1.join(outputDir, chunkKey.replace(/\.mbt$/, ".css"));
|
|
1286
|
+
else {
|
|
1287
|
+
const dirName = chunkKey === "." ? "root" : chunkKey.replace(/\//g, "_");
|
|
1288
|
+
chunkPath = path$1.join(outputDir, `${dirName}.css`);
|
|
1289
|
+
}
|
|
1290
|
+
fs$1.mkdirSync(path$1.dirname(chunkPath), { recursive: true });
|
|
1291
|
+
fs$1.writeFileSync(chunkPath, chunk.css);
|
|
1292
|
+
if (verbose) console.error(`Written chunk CSS to ${chunkPath}`);
|
|
1293
|
+
}
|
|
1294
|
+
if (jsonOutput) {
|
|
1295
|
+
const mappingPath = path$1.join(outputDir, "mapping.json");
|
|
1296
|
+
const mappingData = {
|
|
1297
|
+
mapping: result$1.mapping,
|
|
1298
|
+
chunks: Object.fromEntries(Array.from(result$1.chunks.entries()).map(([k, v]) => [k, { size: v.css.length }])),
|
|
1299
|
+
shared: { size: result$1.shared.css.length },
|
|
1300
|
+
warnings: result$1.warnings
|
|
1301
|
+
};
|
|
1302
|
+
fs$1.writeFileSync(mappingPath, JSON.stringify(mappingData, null, pretty ? 2 : 0));
|
|
1303
|
+
if (verbose) console.error(`Written mapping to ${mappingPath}`);
|
|
1304
|
+
}
|
|
1305
|
+
if (verbose) {
|
|
1306
|
+
console.error("\nSplit extraction summary:");
|
|
1307
|
+
console.error(` Shared: ${result$1.shared.css.length} bytes`);
|
|
1308
|
+
for (const [chunkKey, chunk] of result$1.chunks) console.error(` ${chunkKey}: ${chunk.css.length} bytes`);
|
|
1309
|
+
console.error(` Combined: ${result$1.combined.length} bytes`);
|
|
1310
|
+
}
|
|
1311
|
+
if (warn && result$1.warnings && result$1.warnings.length > 0) printWarnings(result$1.warnings, strict);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const result = extract(dir, {
|
|
1315
|
+
pretty,
|
|
1316
|
+
warn,
|
|
1317
|
+
strict,
|
|
1318
|
+
verbose
|
|
1319
|
+
});
|
|
1320
|
+
let output;
|
|
1321
|
+
if (jsonOutput) output = JSON.stringify({
|
|
1322
|
+
css: result.css,
|
|
1323
|
+
mapping: result.mapping,
|
|
1324
|
+
stats: result.stats,
|
|
1325
|
+
warnings: result.warnings
|
|
1326
|
+
}, null, pretty ? 2 : 0);
|
|
1327
|
+
else output = result.css;
|
|
1328
|
+
if (outputFile) {
|
|
1329
|
+
fs$1.writeFileSync(outputFile, output);
|
|
1330
|
+
if (verbose) console.error(`Written to ${outputFile}`);
|
|
1331
|
+
} else console.log(output);
|
|
1332
|
+
if (warn && result.warnings && result.warnings.length > 0) printWarnings(result.warnings, strict);
|
|
1333
|
+
} catch (err) {
|
|
1334
|
+
console.error(`Error: ${err.message}`);
|
|
1335
|
+
process.exit(1);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
function printWarnings(warnings, strict) {
|
|
1339
|
+
console.error(`\n⚠️ ${warnings.length} warning(s): non-literal CSS arguments detected\n`);
|
|
1340
|
+
for (const w of warnings) {
|
|
1341
|
+
console.error(` ${w.file}:${w.line}`);
|
|
1342
|
+
console.error(` ${w.func}(...) - ${w.reason}`);
|
|
1343
|
+
console.error(` Code: ${w.code}`);
|
|
1344
|
+
console.error("");
|
|
1345
|
+
}
|
|
1346
|
+
if (strict) {
|
|
1347
|
+
console.error("❌ Strict mode: exiting with error due to warnings.");
|
|
1348
|
+
process.exit(1);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
function handleCssMinify(args) {
|
|
1352
|
+
let inputFile = null;
|
|
1353
|
+
let outputFile = null;
|
|
1354
|
+
let verbose = false;
|
|
1355
|
+
for (let i = 0; i < args.length; i++) {
|
|
1356
|
+
const arg = args[i];
|
|
1357
|
+
if (arg === "--output" || arg === "-o") outputFile = args[++i];
|
|
1358
|
+
else if (arg === "--verbose" || arg === "-v") verbose = true;
|
|
1359
|
+
else if (!arg.startsWith("-")) inputFile = arg;
|
|
1360
|
+
}
|
|
1361
|
+
if (!inputFile) {
|
|
1362
|
+
console.error("Error: No input file specified");
|
|
1363
|
+
printCssHelp();
|
|
1364
|
+
process.exit(1);
|
|
1365
|
+
}
|
|
1366
|
+
try {
|
|
1367
|
+
const result = minify(fs$1.readFileSync(inputFile, "utf-8"), { verbose });
|
|
1368
|
+
if (outputFile) {
|
|
1369
|
+
fs$1.writeFileSync(outputFile, result.minified);
|
|
1370
|
+
console.error(`Minified: ${formatSize(result.originalSize)} → ${formatSize(result.minifiedSize)} (${result.reduction.toFixed(1)}% reduction)`);
|
|
1371
|
+
console.error(`Written to: ${outputFile}`);
|
|
1372
|
+
} else console.log(result.minified);
|
|
1373
|
+
} catch (err) {
|
|
1374
|
+
console.error(`Error: ${err.message}`);
|
|
1375
|
+
process.exit(1);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
function handleCssInline(args) {
|
|
1379
|
+
let inputFile = null;
|
|
1380
|
+
let outputFile = null;
|
|
1381
|
+
let mappingFile = null;
|
|
1382
|
+
let extractFrom = null;
|
|
1383
|
+
let verbose = false;
|
|
1384
|
+
let dryRun = false;
|
|
1385
|
+
let removeRegistry = false;
|
|
1386
|
+
for (let i = 0; i < args.length; i++) {
|
|
1387
|
+
const arg = args[i];
|
|
1388
|
+
if (arg === "--mapping" || arg === "-m") mappingFile = args[++i];
|
|
1389
|
+
else if (arg === "--extract-from") extractFrom = args[++i];
|
|
1390
|
+
else if (arg === "--output" || arg === "-o") outputFile = args[++i];
|
|
1391
|
+
else if (arg === "--verbose" || arg === "-v") verbose = true;
|
|
1392
|
+
else if (arg === "--dry-run") {
|
|
1393
|
+
dryRun = true;
|
|
1394
|
+
verbose = true;
|
|
1395
|
+
} else if (arg === "--remove-registry") removeRegistry = true;
|
|
1396
|
+
else if (!arg.startsWith("-")) inputFile = arg;
|
|
1397
|
+
}
|
|
1398
|
+
if (!inputFile) {
|
|
1399
|
+
console.error("Error: No input file specified");
|
|
1400
|
+
printCssHelp();
|
|
1401
|
+
process.exit(1);
|
|
1402
|
+
}
|
|
1403
|
+
if (!mappingFile && !extractFrom) {
|
|
1404
|
+
console.error("Error: Must specify --mapping or --extract-from");
|
|
1405
|
+
printCssHelp();
|
|
1406
|
+
process.exit(1);
|
|
1407
|
+
}
|
|
1408
|
+
try {
|
|
1409
|
+
const code = fs$1.readFileSync(inputFile, "utf-8");
|
|
1410
|
+
let result;
|
|
1411
|
+
if (extractFrom) result = inlineFromSource(code, extractFrom, {
|
|
1412
|
+
verbose,
|
|
1413
|
+
dryRun,
|
|
1414
|
+
removeRegistry
|
|
1415
|
+
});
|
|
1416
|
+
else {
|
|
1417
|
+
const mappingContent = fs$1.readFileSync(mappingFile, "utf-8");
|
|
1418
|
+
const mappingData = JSON.parse(mappingContent);
|
|
1419
|
+
result = inlineCSS(code, mappingData.mapping || mappingData, {
|
|
1420
|
+
verbose,
|
|
1421
|
+
dryRun
|
|
1422
|
+
});
|
|
1423
|
+
if (removeRegistry) result = {
|
|
1424
|
+
...result,
|
|
1425
|
+
code: removeRegistryCode(result.code),
|
|
1426
|
+
finalSize: Buffer.byteLength(removeRegistryCode(result.code), "utf-8")
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
if (verbose) {
|
|
1430
|
+
console.error(`Loaded mapping, found ${result.replacements.length} replacements`);
|
|
1431
|
+
for (const r of result.replacements) console.error(` [${r.type}] ${r.key || r.decl} → ${r.to}`);
|
|
1432
|
+
console.error(`\nSize: ${result.originalSize} → ${result.finalSize} (${((1 - result.finalSize / result.originalSize) * 100).toFixed(1)}% reduction)`);
|
|
1433
|
+
}
|
|
1434
|
+
if (dryRun) console.error("\n[Dry run - no files modified]");
|
|
1435
|
+
else if (outputFile) {
|
|
1436
|
+
fs$1.writeFileSync(outputFile, result.code);
|
|
1437
|
+
if (verbose) console.error(`Written to ${outputFile}`);
|
|
1438
|
+
} else console.log(result.code);
|
|
1439
|
+
} catch (err) {
|
|
1440
|
+
console.error(`Error: ${err.message}`);
|
|
1441
|
+
process.exit(1);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
function handleCssInject(args) {
|
|
1445
|
+
let htmlFile = null;
|
|
1446
|
+
let srcDir = null;
|
|
1447
|
+
let outputFile = null;
|
|
1448
|
+
let mode = "inline";
|
|
1449
|
+
let threshold = 4096;
|
|
1450
|
+
let cssFileName = "luna.css";
|
|
1451
|
+
let verbose = false;
|
|
1452
|
+
for (let i = 0; i < args.length; i++) {
|
|
1453
|
+
const arg = args[i];
|
|
1454
|
+
if (arg === "--src") srcDir = args[++i];
|
|
1455
|
+
else if (arg === "--output" || arg === "-o") outputFile = args[++i];
|
|
1456
|
+
else if (arg === "--mode" || arg === "-m") {
|
|
1457
|
+
const m = args[++i];
|
|
1458
|
+
if (m === "inline" || m === "external" || m === "auto") mode = m;
|
|
1459
|
+
else {
|
|
1460
|
+
console.error(`Error: Invalid mode "${m}". Use inline, external, or auto.`);
|
|
1461
|
+
process.exit(1);
|
|
1462
|
+
}
|
|
1463
|
+
} else if (arg === "--threshold" || arg === "-t") {
|
|
1464
|
+
threshold = parseInt(args[++i], 10);
|
|
1465
|
+
if (isNaN(threshold)) {
|
|
1466
|
+
console.error("Error: --threshold must be a number");
|
|
1467
|
+
process.exit(1);
|
|
1468
|
+
}
|
|
1469
|
+
} else if (arg === "--css-file") cssFileName = args[++i];
|
|
1470
|
+
else if (arg === "--verbose" || arg === "-v") verbose = true;
|
|
1471
|
+
else if (!arg.startsWith("-")) htmlFile = arg;
|
|
1472
|
+
}
|
|
1473
|
+
if (!htmlFile) {
|
|
1474
|
+
console.error("Error: No HTML file specified");
|
|
1475
|
+
printCssHelp();
|
|
1476
|
+
process.exit(1);
|
|
1477
|
+
}
|
|
1478
|
+
if (!srcDir) {
|
|
1479
|
+
console.error("Error: --src <dir> is required");
|
|
1480
|
+
printCssHelp();
|
|
1481
|
+
process.exit(1);
|
|
1482
|
+
}
|
|
1483
|
+
try {
|
|
1484
|
+
const result = injectAndWrite({
|
|
1485
|
+
srcDir,
|
|
1486
|
+
htmlFile,
|
|
1487
|
+
outputFile: outputFile || void 0,
|
|
1488
|
+
mode,
|
|
1489
|
+
threshold,
|
|
1490
|
+
cssFileName,
|
|
1491
|
+
verbose
|
|
1492
|
+
});
|
|
1493
|
+
if (!result.replaced) {
|
|
1494
|
+
console.error("Warning: CSS markers not found. Add /* LUNA_CSS_START */ and /* LUNA_CSS_END */ to your HTML.");
|
|
1495
|
+
process.exit(1);
|
|
1496
|
+
}
|
|
1497
|
+
if (verbose) {
|
|
1498
|
+
console.error(`Mode: ${result.mode}, injected ${result.css.length} bytes of CSS`);
|
|
1499
|
+
if (result.cssFile) console.error(`External CSS file: ${result.cssFile}`);
|
|
1500
|
+
}
|
|
1501
|
+
} catch (err) {
|
|
1502
|
+
console.error(`Error: ${err.message}`);
|
|
1503
|
+
process.exit(1);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
async function handleCssAnalyzeMbt(args) {
|
|
1507
|
+
let dir = ".";
|
|
1508
|
+
let outputFile = null;
|
|
1509
|
+
let recursive = true;
|
|
1510
|
+
let verbose = false;
|
|
1511
|
+
for (let i = 0; i < args.length; i++) {
|
|
1512
|
+
const arg = args[i];
|
|
1513
|
+
if (arg === "--output" || arg === "-o") outputFile = args[++i];
|
|
1514
|
+
else if (arg === "--no-recursive") recursive = false;
|
|
1515
|
+
else if (arg === "--verbose" || arg === "-v") verbose = true;
|
|
1516
|
+
else if (!arg.startsWith("-")) dir = arg;
|
|
1517
|
+
}
|
|
1518
|
+
try {
|
|
1519
|
+
if (verbose) console.error(`Analyzing MoonBit files in: ${dir}`);
|
|
1520
|
+
const result = await analyzeDirectory(dir, { recursive });
|
|
1521
|
+
const output = JSON.stringify(result, null, 2);
|
|
1522
|
+
if (outputFile) {
|
|
1523
|
+
fs$1.writeFileSync(outputFile, output);
|
|
1524
|
+
if (verbose) console.error(`Written to ${outputFile}`);
|
|
1525
|
+
} else console.log(output);
|
|
1526
|
+
if (verbose) {
|
|
1527
|
+
console.error(`\nFound ${result.cooccurrences.length} class co-occurrences`);
|
|
1528
|
+
if (result.warnings.length > 0) {
|
|
1529
|
+
console.error(`Warnings: ${result.warnings.length}`);
|
|
1530
|
+
for (const w of result.warnings) console.error(` ${w.file}:${w.line} - ${w.kind}: ${w.message}`);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
} catch (err) {
|
|
1534
|
+
console.error(`Error: ${err.message}`);
|
|
1535
|
+
process.exit(1);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
async function handleCss(args) {
|
|
1539
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
1540
|
+
printCssHelp();
|
|
1541
|
+
process.exit(0);
|
|
1542
|
+
}
|
|
1543
|
+
const subcommand = args[0];
|
|
1544
|
+
const subArgs = args.slice(1);
|
|
1545
|
+
switch (subcommand) {
|
|
1546
|
+
case "extract":
|
|
1547
|
+
handleCssExtract(subArgs);
|
|
1548
|
+
break;
|
|
1549
|
+
case "minify":
|
|
1550
|
+
handleCssMinify(subArgs);
|
|
1551
|
+
break;
|
|
1552
|
+
case "inline":
|
|
1553
|
+
handleCssInline(subArgs);
|
|
1554
|
+
break;
|
|
1555
|
+
case "inject":
|
|
1556
|
+
handleCssInject(subArgs);
|
|
1557
|
+
break;
|
|
1558
|
+
case "analyze-mbt":
|
|
1559
|
+
await handleCssAnalyzeMbt(subArgs);
|
|
1560
|
+
break;
|
|
1561
|
+
default:
|
|
1562
|
+
console.error(`Unknown css subcommand: ${subcommand}`);
|
|
1563
|
+
printCssHelp();
|
|
1564
|
+
process.exit(1);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
async function main() {
|
|
1568
|
+
const args = process.argv.slice(2);
|
|
1569
|
+
if (args.length === 0) {
|
|
1570
|
+
printMainHelp();
|
|
1571
|
+
process.exit(0);
|
|
1572
|
+
}
|
|
1573
|
+
const command = args[0];
|
|
1574
|
+
if (command === "--help" || command === "-h") {
|
|
1575
|
+
printMainHelp();
|
|
1576
|
+
process.exit(0);
|
|
1577
|
+
}
|
|
1578
|
+
const commandArgs = args.slice(1);
|
|
1579
|
+
switch (command) {
|
|
1580
|
+
case "new":
|
|
1581
|
+
handleNew(commandArgs);
|
|
1582
|
+
break;
|
|
1583
|
+
case "css":
|
|
1584
|
+
await handleCss(commandArgs);
|
|
1585
|
+
break;
|
|
1586
|
+
default:
|
|
1587
|
+
console.error(`Unknown command: ${command}`);
|
|
1588
|
+
printMainHelp();
|
|
1589
|
+
process.exit(1);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
main().catch((err) => {
|
|
1593
|
+
console.error(`Error: ${err.message}`);
|
|
1594
|
+
process.exit(1);
|
|
1595
|
+
});
|
|
359
1596
|
|
|
360
1597
|
//#endregion
|
|
361
1598
|
export { };
|