@luna_ui/luna 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +1264 -27
- package/dist/css/index.d.ts +194 -0
- package/dist/css/index.js +721 -0
- package/dist/css/runtime.d.ts +92 -0
- package/dist/css/runtime.js +179 -0
- package/dist/{index-CyEkcO3_.d.ts → index-CDWzWF-h.d.ts} +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/jsx-dev-runtime.js +1 -1
- package/dist/jsx-runtime.d.ts +1 -1
- package/dist/jsx-runtime.js +1 -1
- package/dist/src-DEjrAhrg.js +1 -0
- package/dist/vite-plugin.d.ts +122 -0
- package/dist/vite-plugin.js +1518 -0
- package/package.json +16 -2
- package/src/css/extract.ts +798 -0
- package/src/css/index.ts +10 -0
- package/src/css/inject.ts +205 -0
- package/src/css/inline.ts +182 -0
- package/src/css/minify.ts +70 -0
- package/src/css/optimizer.ts +6 -0
- package/src/css/runtime.ts +344 -0
- package/src/css-optimizer/README.md +353 -0
- package/src/css-optimizer/cooccurrence.ts +100 -0
- package/src/css-optimizer/core.ts +263 -0
- package/src/css-optimizer/extractors.ts +243 -0
- package/src/css-optimizer/hash.ts +54 -0
- package/src/css-optimizer/index.ts +129 -0
- package/src/css-optimizer/merge.ts +109 -0
- package/src/css-optimizer/moonbit-analyzer.ts +210 -0
- package/src/css-optimizer/parser.ts +120 -0
- package/src/css-optimizer/pattern.ts +171 -0
- package/src/css-optimizer/transformers.ts +301 -0
- package/src/css-optimizer/types.ts +128 -0
- package/src/event-utils.ts +227 -0
- package/src/hydration/createHydrator.ts +62 -0
- package/src/hydration/delegate.ts +62 -0
- package/src/hydration/drag.ts +214 -0
- package/src/hydration/index.ts +12 -0
- package/src/hydration/keyboard.ts +64 -0
- package/src/hydration/toggle.ts +101 -0
- package/src/index.ts +890 -0
- package/src/jsx-dev-runtime.ts +2 -0
- package/src/jsx-runtime.ts +398 -0
- package/src/vite-plugin.ts +718 -0
- package/tests/__screenshots__/context.test.ts/Context-API-context-with-reactive-effects-context-value-accessible-in-effect-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-For-component--SolidJS-style--For-updates-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-accepts-children-as-function-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-Show-component--SolidJS-style--Show-toggles-visibility-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-attribute-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElement-createElement-with-dynamic-style-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-createElementNs--SVG-support--createElementNs-with-dynamic-attribute-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-effect-with-DOM-effect-tracks-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-clear-to-empty-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-handles-empty-array-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-removes-items-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-renders-initial-list-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach--list-rendering--forEach-updates-when-items-change-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-empty-to-non-empty-transition-in-SVG-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-handles-reordering-in-SVG-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-updates-SVG-elements-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-forEach-with-SVG-elements-forEach-with-nested-SVG-groups-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-ref-callback--JSX-style--ref-callback-with-nested-elements-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-creates-a-node-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-show--conditional-rendering--show-with-false-condition-creates-placeholder-1.png +0 -0
- package/tests/__screenshots__/dom.test.ts/DOM-API-text-nodes-textDyn-creates-reactive-text-node-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-forEach-renders-correctly-without-show--initial-items--1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-forEach-with-context-renders-correctly-without-show-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-nested-components-with-context--forEach--and-show-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-show-and-forEach-inherit-context-from-Owner--fixed--1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Complex-nested-scenario-show-and-forEach-work-together--context-uses-default--1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Context---ForEach-integration-forEach-items-can-access-context-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-renders-initial-list-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-updates-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-ForEach-with-reactive-updates-forEach-with-object-items-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-hides-when-condition-is-false-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-renders-when-condition-is-true-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-from-false-to-true-1.png +0 -0
- package/tests/__screenshots__/integration.test.ts/Integration--Nested-Components-with-Context-Show--conditional-rendering--show-toggles-reactively-1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--event-listener-pattern--Solid-js-docs-example--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--multiple-cleanups-in-component-body--LIFO-order--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-in-component-body-runs-on-unmount-1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--onCleanup-works-with-For-loop-items--component-body-style--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Component-Body--Solid-js-style--timer-cleanup-pattern--Solid-js-style--1.png +0 -0
- package/tests/__screenshots__/lifecycle.test.ts/onCleanup-in-Effects-effect-cleanup-runs-before-re-run-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-large-list-update-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-nested-batch-operations-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Bulk-Updates-rapid-sequential-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-Show-component---visible-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Conditional-show-hide-element---visible-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-fragment-with-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-nested-fragments-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Fragments-simple-fragment-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-conditional-toggle-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-addition-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-list-removal-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/DOM-Rendering-Comparison---Reactive-Updates-text-updates-match-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-className-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-dynamic-style-updates-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Dynamic-Attributes-Comparison-multiple-dynamic-attributes-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-deeply-nested-conditionals-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-empty-to-populated-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Edge-Cases-list-transitions-from-populated-to-empty-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-cleanup-order-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-nested-effects-with-inner-signal-change-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-is-called-when-effect-re-runs-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Effect-Cleanup-Comparison-onCleanup-with-resource-simulation-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-multiple-children--no-wrapper--1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-Fragment-with-no-children-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-fragment-with-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Fragment-Comparison-with-Preact-nested-Fragments-work-correctly-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-complex-reordering-with-additions-and-removals-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-insert-in-middle-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-remove-from-middle-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-reverse-list-order-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/List-Reordering-shuffle-list-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-Show-component-renders-when-condition-is-true-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-renders-content-when-initially-true-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Luna-Conditional-Rendering-show-toggles-visibility-dynamically-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Memo-Dependency-Chain-conditional-memo-dependencies-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-basic-signal-get-set-produces-same-values-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Signal-Behavior-Comparison-batch-updates-produce-same-final-values-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-peek-reads-value-without-tracking-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-selective-tracking-with-untrack-1.png +0 -0
- package/tests/__screenshots__/preact-signals-comparison.test.ts/Untrack-and-Peek-Behavior-untrack-prevents-dependency-tracking-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API--SolidJS-style--reactivity-accessor-is-reactive-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-empty-string-for-non-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateError-returns-undefined-for-non-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsFailure-and-stateError-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsPending-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateIsSuccess-and-stateValue-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-AsyncState-helpers-stateValue-returns-undefined-for-non-success-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-reject-transitions-to-failure-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-resolve-transitions-to-success-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-returns-resource--resolve--and-reject-functions-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createDeferred-starts-in-pending-state-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-async-resolve-works-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-starts-in-pending-state-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-failure-on-reject-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-createResource-transitions-to-success-on-resolve-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-can-wrap-fetch-like-async-operations-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-integration-with-Promise-works-with-setTimeout-simulation-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourceGet-tracks-dependencies-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceGet-vs-resourcePeek-resourcePeek-does-not-track-dependencies-1.png +0 -0
- package/tests/__screenshots__/resource.test.ts/Resource-API-resourceRefetch-refetch-resets-to-pending-and-re-runs-fetcher-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-children-to-body-by-default-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Portal-component-Portal-renders-to-selector-mount-target-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/SolidJS-API-compatibility-createEffect-tracks-dependencies-automatically-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-accessor-condition-in-Match-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-multiple-Match-components-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-component-Switch-with-single-Match-and-fallback-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/Switch-Match-components-Switch-updates-DOM-when-signal-changes-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-multiple-dependencies-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-tracks-single-dependency-1.png +0 -0
- package/tests/__screenshots__/solidjs-api.test.ts/on---utility-on-with-defer-option-skips-initial-run-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Arrays-array-updates-work-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-only-triggers-when-accessed-property-changes-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-parent-path-change-notifies-child-accessors-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-nested-property-access-1.png +0 -0
- package/tests/__screenshots__/store.test.ts/createStore-Reactivity-tracks-property-access-in-effects-1.png +0 -0
- package/tests/context.test.ts +118 -0
- package/tests/css-optimizer-extractors.test.ts +264 -0
- package/tests/css-optimizer-integration.test.ts +566 -0
- package/tests/css-optimizer-transformers.test.ts +301 -0
- package/tests/css-optimizer.test.ts +646 -0
- package/tests/css-runtime.bench.ts +442 -0
- package/tests/css-runtime.test.ts +342 -0
- package/tests/dom.test.ts +872 -0
- package/tests/integration.test.ts +405 -0
- package/tests/issue-5-for-infinite-loop.test.ts +516 -0
- package/tests/jsx-runtime.test.tsx +393 -0
- package/tests/lifecycle.test.ts +833 -0
- package/tests/move-before.bench.ts +304 -0
- package/tests/preact-signals-comparison.test.ts +1608 -0
- package/tests/resource.test.ts +160 -0
- package/tests/router.test.ts +117 -0
- package/tests/show-initial-mount-leak.test.tsx +182 -0
- package/tests/solidjs-api.test.ts +659 -0
- package/tests/static-perf.bench.ts +64 -0
- package/tests/store.test.ts +263 -0
- package/tests/tsx-syntax.test.tsx +404 -0
- package/dist/src-BDdxGwvq.js +0 -1
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS Optimizer Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the CSS optimizer integration with the Vite plugin pipeline.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, test, expect, beforeEach, vi } from "vitest";
|
|
8
|
+
import { optimizeCss, optimizeHtml, extractClassUsages } from "../src/css-optimizer/index.js";
|
|
9
|
+
|
|
10
|
+
describe("CSS Optimizer Integration Tests", () => {
|
|
11
|
+
/**
|
|
12
|
+
* These tests simulate the full pipeline from HTML extraction
|
|
13
|
+
* through optimization to final HTML/CSS output.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
describe("Full pipeline simulation", () => {
|
|
17
|
+
test("should optimize a realistic component page", () => {
|
|
18
|
+
// Simulating a card component repeated multiple times
|
|
19
|
+
const html = `
|
|
20
|
+
<!DOCTYPE html>
|
|
21
|
+
<html>
|
|
22
|
+
<head><title>Card Grid</title></head>
|
|
23
|
+
<body>
|
|
24
|
+
<div class="container">
|
|
25
|
+
<div class="_flex _gap _p4 _bg _rounded card">Card 1</div>
|
|
26
|
+
<div class="_flex _gap _p4 _bg _rounded card">Card 2</div>
|
|
27
|
+
<div class="_flex _gap _p4 _bg _rounded card">Card 3</div>
|
|
28
|
+
<div class="_flex _gap _p4 _bg _rounded card">Card 4</div>
|
|
29
|
+
</div>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const css = `
|
|
35
|
+
._flex{display:flex}
|
|
36
|
+
._gap{gap:1rem}
|
|
37
|
+
._p4{padding:1rem}
|
|
38
|
+
._bg{background:#f0f0f0}
|
|
39
|
+
._rounded{border-radius:8px}
|
|
40
|
+
`.replace(/\s+/g, "");
|
|
41
|
+
|
|
42
|
+
const mapping: Record<string, string> = {
|
|
43
|
+
"display:flex": "_flex",
|
|
44
|
+
"gap:1rem": "_gap",
|
|
45
|
+
"padding:1rem": "_p4",
|
|
46
|
+
"background:#f0f0f0": "_bg",
|
|
47
|
+
"border-radius:8px": "_rounded",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const result = optimizeCss(css, html, mapping, {
|
|
51
|
+
minFrequency: 2,
|
|
52
|
+
maxPatternSize: 5,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Should find a pattern for the 5 classes that appear together 4 times
|
|
56
|
+
expect(result.patterns.length).toBeGreaterThan(0);
|
|
57
|
+
expect(result.mergeMap.size).toBeGreaterThan(0);
|
|
58
|
+
expect(result.stats.mergedPatterns).toBeGreaterThan(0);
|
|
59
|
+
|
|
60
|
+
// Verify optimized CSS contains merged class
|
|
61
|
+
const mergedClassNames = Array.from(result.mergeMap.values());
|
|
62
|
+
for (const className of mergedClassNames) {
|
|
63
|
+
expect(result.css).toContain(className);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Verify HTML optimization
|
|
67
|
+
const optimizedHtml = optimizeHtml(html, result.mergeMap);
|
|
68
|
+
for (const className of mergedClassNames) {
|
|
69
|
+
expect(optimizedHtml).toContain(className);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("should handle TodoMVC-like patterns", () => {
|
|
74
|
+
// Simulating a TodoMVC item list
|
|
75
|
+
const html = `
|
|
76
|
+
<!DOCTYPE html>
|
|
77
|
+
<html>
|
|
78
|
+
<body>
|
|
79
|
+
<ul class="_list">
|
|
80
|
+
<li class="_flex _items_center _p2 _border_b todo-item"><span class="_text">Buy groceries</span></li>
|
|
81
|
+
<li class="_flex _items_center _p2 _border_b todo-item"><span class="_text">Walk dog</span></li>
|
|
82
|
+
<li class="_flex _items_center _p2 _border_b todo-item"><span class="_text">Read book</span></li>
|
|
83
|
+
<li class="_flex _items_center _p2 _border_b todo-item"><span class="_text">Clean room</span></li>
|
|
84
|
+
<li class="_flex _items_center _p2 _border_b todo-item"><span class="_text">Call mom</span></li>
|
|
85
|
+
</ul>
|
|
86
|
+
</body>
|
|
87
|
+
</html>
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
const css = `._flex{display:flex}._items_center{align-items:center}._p2{padding:0.5rem}._border_b{border-bottom:1px solid #ddd}._list{list-style:none}._text{color:#333}`;
|
|
91
|
+
|
|
92
|
+
const mapping: Record<string, string> = {
|
|
93
|
+
"display:flex": "_flex",
|
|
94
|
+
"align-items:center": "_items_center",
|
|
95
|
+
"padding:0.5rem": "_p2",
|
|
96
|
+
"border-bottom:1px solid #ddd": "_border_b",
|
|
97
|
+
"list-style:none": "_list",
|
|
98
|
+
"color:#333": "_text",
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const result = optimizeCss(css, html, mapping, {
|
|
102
|
+
minFrequency: 2,
|
|
103
|
+
maxPatternSize: 5,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// The 4 classes on <li> appear 5 times each
|
|
107
|
+
expect(result.patterns.length).toBeGreaterThan(0);
|
|
108
|
+
expect(result.stats.estimatedBytesSaved).toBeGreaterThan(0);
|
|
109
|
+
|
|
110
|
+
// Verify HTML transformation preserves non-luna classes
|
|
111
|
+
const optimizedHtml = optimizeHtml(html, result.mergeMap);
|
|
112
|
+
expect(optimizedHtml).toContain("todo-item");
|
|
113
|
+
expect(optimizedHtml).toContain("_list");
|
|
114
|
+
expect(optimizedHtml).toContain("_text");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("should preserve unmerged classes correctly", () => {
|
|
118
|
+
// Mix of frequent and rare patterns
|
|
119
|
+
const html = `
|
|
120
|
+
<div class="_a _b common">1</div>
|
|
121
|
+
<div class="_a _b common">2</div>
|
|
122
|
+
<div class="_a _b common">3</div>
|
|
123
|
+
<div class="_c _d rare">4</div>
|
|
124
|
+
`;
|
|
125
|
+
|
|
126
|
+
const css = `._a{color:red}._b{color:blue}._c{color:green}._d{color:yellow}`;
|
|
127
|
+
const mapping: Record<string, string> = {
|
|
128
|
+
"color:red": "_a",
|
|
129
|
+
"color:blue": "_b",
|
|
130
|
+
"color:green": "_c",
|
|
131
|
+
"color:yellow": "_d",
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
|
|
135
|
+
|
|
136
|
+
// Should merge _a _b (3 occurrences) but not _c _d (1 occurrence)
|
|
137
|
+
expect(result.patterns.length).toBeGreaterThanOrEqual(1);
|
|
138
|
+
|
|
139
|
+
// Verify CSS still has rules for _c and _d
|
|
140
|
+
expect(result.css).toContain("._c");
|
|
141
|
+
expect(result.css).toContain("._d");
|
|
142
|
+
|
|
143
|
+
// Verify HTML keeps rare classes unchanged
|
|
144
|
+
const optimizedHtml = optimizeHtml(html, result.mergeMap);
|
|
145
|
+
expect(optimizedHtml).toContain("_c _d");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("Incremental build scenarios", () => {
|
|
150
|
+
test("should produce deterministic output for same input", () => {
|
|
151
|
+
const html = `
|
|
152
|
+
<div class="_x _y">1</div>
|
|
153
|
+
<div class="_x _y">2</div>
|
|
154
|
+
<div class="_x _y">3</div>
|
|
155
|
+
`;
|
|
156
|
+
const css = `._x{a:b}._y{c:d}`;
|
|
157
|
+
const mapping: Record<string, string> = { "a:b": "_x", "c:d": "_y" };
|
|
158
|
+
|
|
159
|
+
const result1 = optimizeCss(css, html, mapping, { minFrequency: 2 });
|
|
160
|
+
const result2 = optimizeCss(css, html, mapping, { minFrequency: 2 });
|
|
161
|
+
|
|
162
|
+
expect(result1.css).toBe(result2.css);
|
|
163
|
+
expect(Array.from(result1.mergeMap.entries())).toEqual(
|
|
164
|
+
Array.from(result2.mergeMap.entries())
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("should handle added elements gracefully", () => {
|
|
169
|
+
const html1 = `
|
|
170
|
+
<div class="_m _n">1</div>
|
|
171
|
+
<div class="_m _n">2</div>
|
|
172
|
+
`;
|
|
173
|
+
const html2 = `
|
|
174
|
+
<div class="_m _n">1</div>
|
|
175
|
+
<div class="_m _n">2</div>
|
|
176
|
+
<div class="_m _n">3</div>
|
|
177
|
+
`;
|
|
178
|
+
|
|
179
|
+
const css = `._m{e:f}._n{g:h}`;
|
|
180
|
+
const mapping: Record<string, string> = { "e:f": "_m", "g:h": "_n" };
|
|
181
|
+
|
|
182
|
+
// First run with minFrequency: 2
|
|
183
|
+
const result1 = optimizeCss(css, html1, mapping, { minFrequency: 2 });
|
|
184
|
+
// Second run with same threshold
|
|
185
|
+
const result2 = optimizeCss(css, html2, mapping, { minFrequency: 2 });
|
|
186
|
+
|
|
187
|
+
// Both should merge since both have >= 2 occurrences
|
|
188
|
+
expect(result1.patterns.length).toBeGreaterThan(0);
|
|
189
|
+
expect(result2.patterns.length).toBeGreaterThan(0);
|
|
190
|
+
|
|
191
|
+
// Merged class name should be the same (deterministic)
|
|
192
|
+
const merged1 = Array.from(result1.mergeMap.values())[0];
|
|
193
|
+
const merged2 = Array.from(result2.mergeMap.values())[0];
|
|
194
|
+
expect(merged1).toBe(merged2);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("Multiple HTML files simulation", () => {
|
|
199
|
+
test("should handle combined HTML from multiple pages", () => {
|
|
200
|
+
// Simulating multiple pages being combined for analysis
|
|
201
|
+
const page1 = `<div class="_h1 _mb _text">Page 1 Title</div>`;
|
|
202
|
+
const page2 = `<div class="_h1 _mb _text">Page 2 Title</div>`;
|
|
203
|
+
const page3 = `<div class="_h1 _mb _text">Page 3 Title</div>`;
|
|
204
|
+
const combinedHtml = [page1, page2, page3].join("\n");
|
|
205
|
+
|
|
206
|
+
const css = `._h1{font-size:2rem}._mb{margin-bottom:1rem}._text{color:#111}`;
|
|
207
|
+
const mapping: Record<string, string> = {
|
|
208
|
+
"font-size:2rem": "_h1",
|
|
209
|
+
"margin-bottom:1rem": "_mb",
|
|
210
|
+
"color:#111": "_text",
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const result = optimizeCss(css, combinedHtml, mapping, { minFrequency: 2 });
|
|
214
|
+
|
|
215
|
+
// Should find patterns across pages
|
|
216
|
+
expect(result.patterns.length).toBeGreaterThan(0);
|
|
217
|
+
expect(result.stats.estimatedBytesSaved).toBeGreaterThan(0);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("should detect cross-page patterns correctly", () => {
|
|
221
|
+
// Pattern appears in different pages
|
|
222
|
+
const page1 = `<button class="_btn _primary">Submit</button>`;
|
|
223
|
+
const page2 = `<button class="_btn _primary">Save</button>`;
|
|
224
|
+
const page3 = `<button class="_btn _secondary">Cancel</button>`;
|
|
225
|
+
const combinedHtml = [page1, page2, page3].join("\n");
|
|
226
|
+
|
|
227
|
+
const css = `._btn{padding:0.5rem 1rem}._primary{background:blue}._secondary{background:gray}`;
|
|
228
|
+
const mapping: Record<string, string> = {
|
|
229
|
+
"padding:0.5rem 1rem": "_btn",
|
|
230
|
+
"background:blue": "_primary",
|
|
231
|
+
"background:gray": "_secondary",
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const result = optimizeCss(css, combinedHtml, mapping, { minFrequency: 2 });
|
|
235
|
+
|
|
236
|
+
// _btn _primary appears 2 times, should be merged
|
|
237
|
+
// _btn _secondary appears 1 time, should not be merged
|
|
238
|
+
expect(result.mergeMap.size).toBeGreaterThanOrEqual(1);
|
|
239
|
+
|
|
240
|
+
// Verify _secondary is preserved
|
|
241
|
+
expect(result.css).toContain("._secondary");
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("Edge cases in real-world scenarios", () => {
|
|
246
|
+
test("should handle empty HTML gracefully", () => {
|
|
247
|
+
const html = "";
|
|
248
|
+
const css = `._x{a:b}._y{c:d}`;
|
|
249
|
+
const mapping: Record<string, string> = { "a:b": "_x", "c:d": "_y" };
|
|
250
|
+
|
|
251
|
+
const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
|
|
252
|
+
|
|
253
|
+
expect(result.patterns).toHaveLength(0);
|
|
254
|
+
expect(result.mergeMap.size).toBe(0);
|
|
255
|
+
expect(result.css).toBe(css); // Original CSS preserved
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("should handle HTML with no Luna classes", () => {
|
|
259
|
+
const html = `
|
|
260
|
+
<div class="regular-class">Content</div>
|
|
261
|
+
<span class="another-class">More</span>
|
|
262
|
+
`;
|
|
263
|
+
const css = `._a{x:y}._b{w:z}`;
|
|
264
|
+
const mapping: Record<string, string> = { "x:y": "_a", "w:z": "_b" };
|
|
265
|
+
|
|
266
|
+
const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
|
|
267
|
+
|
|
268
|
+
expect(result.patterns).toHaveLength(0);
|
|
269
|
+
expect(result.css).toBe(css);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("should handle classes not in mapping", () => {
|
|
273
|
+
const html = `
|
|
274
|
+
<div class="_unknown1 _unknown2">1</div>
|
|
275
|
+
<div class="_unknown1 _unknown2">2</div>
|
|
276
|
+
<div class="_unknown1 _unknown2">3</div>
|
|
277
|
+
`;
|
|
278
|
+
const css = `._other{foo:bar}`;
|
|
279
|
+
const mapping: Record<string, string> = { "foo:bar": "_other" };
|
|
280
|
+
|
|
281
|
+
const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
|
|
282
|
+
|
|
283
|
+
// Pattern is found in HTML but declarations not in mapping
|
|
284
|
+
expect(result.mergeMap.size).toBe(0);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("should handle pseudo-class rules preservation", () => {
|
|
288
|
+
const html = `
|
|
289
|
+
<a class="_link _blue">1</a>
|
|
290
|
+
<a class="_link _blue">2</a>
|
|
291
|
+
<a class="_link _blue">3</a>
|
|
292
|
+
`;
|
|
293
|
+
|
|
294
|
+
// Include pseudo-class rules
|
|
295
|
+
const css = `._link{text-decoration:none}._blue{color:blue}._link:hover{text-decoration:underline}._blue:hover{color:darkblue}`;
|
|
296
|
+
const mapping: Record<string, string> = {
|
|
297
|
+
"text-decoration:none": "_link",
|
|
298
|
+
"color:blue": "_blue",
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
|
|
302
|
+
|
|
303
|
+
// Base rules should be merged
|
|
304
|
+
expect(result.patterns.length).toBeGreaterThan(0);
|
|
305
|
+
|
|
306
|
+
// Pseudo-class rules should be preserved
|
|
307
|
+
expect(result.css).toContain(":hover");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("should handle deeply nested HTML", () => {
|
|
311
|
+
const html = `
|
|
312
|
+
<div>
|
|
313
|
+
<div>
|
|
314
|
+
<div>
|
|
315
|
+
<div>
|
|
316
|
+
<span class="_deep _nested">1</span>
|
|
317
|
+
<span class="_deep _nested">2</span>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
`;
|
|
323
|
+
const css = `._deep{position:relative}._nested{display:inline}`;
|
|
324
|
+
const mapping: Record<string, string> = {
|
|
325
|
+
"position:relative": "_deep",
|
|
326
|
+
"display:inline": "_nested",
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
|
|
330
|
+
|
|
331
|
+
expect(result.patterns.length).toBeGreaterThan(0);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("should handle many unique classes with few patterns", () => {
|
|
335
|
+
// Many different class combinations
|
|
336
|
+
const html = `
|
|
337
|
+
<div class="_a _b">1</div>
|
|
338
|
+
<div class="_c _d">2</div>
|
|
339
|
+
<div class="_e _f">3</div>
|
|
340
|
+
<div class="_g _h">4</div>
|
|
341
|
+
<div class="_a _b">5</div>
|
|
342
|
+
`;
|
|
343
|
+
const css = `._a{a:1}._b{b:2}._c{c:3}._d{d:4}._e{e:5}._f{f:6}._g{g:7}._h{h:8}`;
|
|
344
|
+
const mapping: Record<string, string> = {
|
|
345
|
+
"a:1": "_a",
|
|
346
|
+
"b:2": "_b",
|
|
347
|
+
"c:3": "_c",
|
|
348
|
+
"d:4": "_d",
|
|
349
|
+
"e:5": "_e",
|
|
350
|
+
"f:6": "_f",
|
|
351
|
+
"g:7": "_g",
|
|
352
|
+
"h:8": "_h",
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
|
|
356
|
+
|
|
357
|
+
// Only _a _b appears twice
|
|
358
|
+
expect(result.patterns.length).toBe(1);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
describe("Performance regression scenarios", () => {
|
|
363
|
+
test("should handle large number of class usages efficiently", () => {
|
|
364
|
+
// Generate 100 elements with the same pattern
|
|
365
|
+
const items = Array.from({ length: 100 }, (_, i) =>
|
|
366
|
+
`<div class="_rep1 _rep2 _rep3">Item ${i}</div>`
|
|
367
|
+
).join("\n");
|
|
368
|
+
const html = `<div>${items}</div>`;
|
|
369
|
+
|
|
370
|
+
const css = `._rep1{a:b}._rep2{c:d}._rep3{e:f}`;
|
|
371
|
+
const mapping: Record<string, string> = {
|
|
372
|
+
"a:b": "_rep1",
|
|
373
|
+
"c:d": "_rep2",
|
|
374
|
+
"e:f": "_rep3",
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const start = performance.now();
|
|
378
|
+
const result = optimizeCss(css, html, mapping, {
|
|
379
|
+
minFrequency: 2,
|
|
380
|
+
maxPatternSize: 5,
|
|
381
|
+
});
|
|
382
|
+
const elapsed = performance.now() - start;
|
|
383
|
+
|
|
384
|
+
expect(result.patterns.length).toBeGreaterThan(0);
|
|
385
|
+
expect(result.stats.estimatedBytesSaved).toBeGreaterThan(0);
|
|
386
|
+
// Should complete reasonably fast (< 500ms)
|
|
387
|
+
expect(elapsed).toBeLessThan(500);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("should handle many different patterns efficiently", () => {
|
|
391
|
+
// Generate elements with varying patterns
|
|
392
|
+
const items: string[] = [];
|
|
393
|
+
for (let i = 0; i < 50; i++) {
|
|
394
|
+
// Half with pattern A, half with pattern B
|
|
395
|
+
if (i % 2 === 0) {
|
|
396
|
+
items.push(`<div class="_pa1 _pa2">A${i}</div>`);
|
|
397
|
+
} else {
|
|
398
|
+
items.push(`<div class="_pb1 _pb2">B${i}</div>`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const html = items.join("\n");
|
|
402
|
+
|
|
403
|
+
const css = `._pa1{pa1:v}._pa2{pa2:v}._pb1{pb1:v}._pb2{pb2:v}`;
|
|
404
|
+
const mapping: Record<string, string> = {
|
|
405
|
+
"pa1:v": "_pa1",
|
|
406
|
+
"pa2:v": "_pa2",
|
|
407
|
+
"pb1:v": "_pb1",
|
|
408
|
+
"pb2:v": "_pb2",
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const result = optimizeCss(css, html, mapping, { minFrequency: 2 });
|
|
412
|
+
|
|
413
|
+
// Should find both patterns
|
|
414
|
+
expect(result.patterns.length).toBe(2);
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
describe("extractClassUsages edge cases", () => {
|
|
419
|
+
test("should handle malformed HTML gracefully", () => {
|
|
420
|
+
const malformed = `<div class="_a _b"><span class="_c"`;
|
|
421
|
+
const usages = extractClassUsages(malformed);
|
|
422
|
+
|
|
423
|
+
// Should still extract valid class attributes
|
|
424
|
+
expect(usages.length).toBeGreaterThanOrEqual(0);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("should handle single quotes in class attribute", () => {
|
|
428
|
+
const html = `<div class='_sq1 _sq2'>test</div><div class='_sq1 _sq2'>test2</div>`;
|
|
429
|
+
const usages = extractClassUsages(html);
|
|
430
|
+
|
|
431
|
+
// Double quote regex won't match single quotes
|
|
432
|
+
// This is expected behavior - document or fix if needed
|
|
433
|
+
expect(usages.length).toBe(0);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("should handle extra whitespace in classes", () => {
|
|
437
|
+
const html = `
|
|
438
|
+
<div class=" _ws1 _ws2 ">1</div>
|
|
439
|
+
<div class="_ws1 _ws2">2</div>
|
|
440
|
+
`;
|
|
441
|
+
const usages = extractClassUsages(html);
|
|
442
|
+
|
|
443
|
+
expect(usages.length).toBe(2);
|
|
444
|
+
// Classes should be normalized (whitespace trimmed)
|
|
445
|
+
for (const usage of usages) {
|
|
446
|
+
expect(usage.classes.length).toBe(2);
|
|
447
|
+
expect(usage.classes).toContain("_ws1");
|
|
448
|
+
expect(usage.classes).toContain("_ws2");
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
describe("optimizeHtml edge cases", () => {
|
|
454
|
+
test("should handle multiple class attributes per element", () => {
|
|
455
|
+
// Technically invalid HTML but should handle gracefully
|
|
456
|
+
const html = `<div class="_a _b" class="_c _d">test</div>`;
|
|
457
|
+
const mergeMap = new Map([["_a _b", "_merged"]]);
|
|
458
|
+
|
|
459
|
+
const result = optimizeHtml(html, mergeMap);
|
|
460
|
+
// First class attr should be replaced
|
|
461
|
+
expect(result).toContain("_merged");
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("should preserve class order for non-merged classes", () => {
|
|
465
|
+
const html = `<div class="_first other _second another">test</div>`;
|
|
466
|
+
const mergeMap = new Map<string, string>(); // No merges
|
|
467
|
+
|
|
468
|
+
const result = optimizeHtml(html, mergeMap);
|
|
469
|
+
// With empty mergeMap, HTML is returned unchanged
|
|
470
|
+
expect(result).toBe(html);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("should reorder classes when merging", () => {
|
|
474
|
+
const html = `<div class="_first other _second another">test</div><div class="_first other _second another">test2</div>`;
|
|
475
|
+
const mergeMap = new Map([["_first _second", "_merged"]]);
|
|
476
|
+
|
|
477
|
+
const result = optimizeHtml(html, mergeMap);
|
|
478
|
+
// Merged class should appear, with non-luna classes at the end
|
|
479
|
+
expect(result).toContain("_merged");
|
|
480
|
+
expect(result).toContain("other");
|
|
481
|
+
expect(result).toContain("another");
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("should handle empty class attribute", () => {
|
|
485
|
+
const html = `<div class="">empty</div><div class="_a _b">filled</div>`;
|
|
486
|
+
const mergeMap = new Map([["_a _b", "_merged"]]);
|
|
487
|
+
|
|
488
|
+
const result = optimizeHtml(html, mergeMap);
|
|
489
|
+
expect(result).toContain('class=""');
|
|
490
|
+
expect(result).toContain("_merged");
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe("CSS Optimizer Options", () => {
|
|
496
|
+
test("minFrequency controls pattern threshold", () => {
|
|
497
|
+
const html = `
|
|
498
|
+
<div class="_opt1 _opt2">1</div>
|
|
499
|
+
<div class="_opt1 _opt2">2</div>
|
|
500
|
+
<div class="_opt1 _opt2">3</div>
|
|
501
|
+
`;
|
|
502
|
+
const css = `._opt1{o1:v}._opt2{o2:v}`;
|
|
503
|
+
const mapping: Record<string, string> = { "o1:v": "_opt1", "o2:v": "_opt2" };
|
|
504
|
+
|
|
505
|
+
// With minFrequency: 4, should not merge (only 3 occurrences)
|
|
506
|
+
const result1 = optimizeCss(css, html, mapping, { minFrequency: 4 });
|
|
507
|
+
expect(result1.patterns.length).toBe(0);
|
|
508
|
+
|
|
509
|
+
// With minFrequency: 3, should merge
|
|
510
|
+
const result2 = optimizeCss(css, html, mapping, { minFrequency: 3 });
|
|
511
|
+
expect(result2.patterns.length).toBe(1);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("maxPatternSize limits pattern complexity", () => {
|
|
515
|
+
const html = `
|
|
516
|
+
<div class="_s1 _s2 _s3 _s4 _s5">1</div>
|
|
517
|
+
<div class="_s1 _s2 _s3 _s4 _s5">2</div>
|
|
518
|
+
<div class="_s1 _s2 _s3 _s4 _s5">3</div>
|
|
519
|
+
`;
|
|
520
|
+
const css = `._s1{s1:v}._s2{s2:v}._s3{s3:v}._s4{s4:v}._s5{s5:v}`;
|
|
521
|
+
const mapping: Record<string, string> = {
|
|
522
|
+
"s1:v": "_s1",
|
|
523
|
+
"s2:v": "_s2",
|
|
524
|
+
"s3:v": "_s3",
|
|
525
|
+
"s4:v": "_s4",
|
|
526
|
+
"s5:v": "_s5",
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
// With maxPatternSize: 2, should only find pairs
|
|
530
|
+
const result1 = optimizeCss(css, html, mapping, {
|
|
531
|
+
minFrequency: 2,
|
|
532
|
+
maxPatternSize: 2,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// With maxPatternSize: 5, should find quintuples
|
|
536
|
+
const result2 = optimizeCss(css, html, mapping, {
|
|
537
|
+
minFrequency: 2,
|
|
538
|
+
maxPatternSize: 5,
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// The 5-class pattern should be found with maxPatternSize: 5
|
|
542
|
+
const hasQuintuple = result2.patterns.some(
|
|
543
|
+
(p) => p.originalClasses.length === 5
|
|
544
|
+
);
|
|
545
|
+
const hasPair = result1.patterns.some((p) => p.originalClasses.length === 2);
|
|
546
|
+
|
|
547
|
+
expect(hasQuintuple).toBe(true);
|
|
548
|
+
expect(hasPair).toBe(true);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test("classPrefix filters by prefix", () => {
|
|
552
|
+
const html = `
|
|
553
|
+
<div class="_luna1 _luna2 tailwind-class">1</div>
|
|
554
|
+
<div class="_luna1 _luna2 another-class">2</div>
|
|
555
|
+
`;
|
|
556
|
+
|
|
557
|
+
// Default prefix "_" should only extract luna classes
|
|
558
|
+
const usages = extractClassUsages(html, "html", "_");
|
|
559
|
+
|
|
560
|
+
for (const usage of usages) {
|
|
561
|
+
for (const cls of usage.classes) {
|
|
562
|
+
expect(cls.startsWith("_")).toBe(true);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
});
|