@lovrozagar/flare 0.2.0 → 0.3.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/README.md +2 -0
- package/package.json +1 -1
- package/src/head-client/index.ts +85 -35
- package/src/loader-pipeline/index.ts +26 -3
- package/src/plugins/sx-ast/rewrite.ts +109 -72
- package/src/server-handler/index.ts +2 -2
- package/src/styles/cn-vendor/NOTICE.md +18 -0
- package/src/styles/cn-vendor/engine.ts +1603 -0
- package/src/styles/cn-vendor/tables.generated.ts +123 -0
- package/src/styles/cn-vendor/types.ts +120 -0
- package/src/styles/cn.ts +15 -0
- package/src/styles/index.ts +1 -36
package/README.md
CHANGED
|
@@ -744,6 +744,8 @@ import { styles, cn } from "@lovrozagar/flare/styles";
|
|
|
744
744
|
<div class={cn("base", on() && "active")} />
|
|
745
745
|
```
|
|
746
746
|
|
|
747
|
+
`cn` merges Tailwind conflicts: the last utility in a group wins. A static `class="px-2 px-4"` compiles to `px-4`. Runtime `cn` is tree-shaken unless a dynamic class list exists.
|
|
748
|
+
|
|
747
749
|
`tw=` on `styles()` or as a JSX attribute is **dropped**. Put utilities in `class=`. `css=` compiles through the same plugin (not a `data-c` hash).
|
|
748
750
|
|
|
749
751
|
## Fonts and images
|
package/package.json
CHANGED
package/src/head-client/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ const managedHreflangTags = new Set<string>();
|
|
|
17
17
|
/* CSS lifecycle: per-route elements + refcounted dedup for shared stylesheets */
|
|
18
18
|
const cssElementsByRoute = new Map<string, HTMLElement[]>();
|
|
19
19
|
const cssRefCount = new Map<string, { element: HTMLElement; refCount: number }>();
|
|
20
|
+
const styleRefCount = new Map<string, { element: HTMLElement; refCount: number }>();
|
|
20
21
|
|
|
21
22
|
export function clearRouteTracking(): void {
|
|
22
23
|
baseTitle = "";
|
|
@@ -26,6 +27,7 @@ export function clearRouteTracking(): void {
|
|
|
26
27
|
managedHreflangTags.clear();
|
|
27
28
|
cssElementsByRoute.clear();
|
|
28
29
|
cssRefCount.clear();
|
|
30
|
+
styleRefCount.clear();
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export function initRouteHierarchy(matchIds: string[], rootTitle?: string): void {
|
|
@@ -561,24 +563,72 @@ function addCssLinkForRoute(matchId: string, href: string): void {
|
|
|
561
563
|
cssElementsByRoute.set(matchId, list);
|
|
562
564
|
}
|
|
563
565
|
|
|
564
|
-
function
|
|
565
|
-
if (
|
|
566
|
-
const el = document.createElement("style");
|
|
566
|
+
function stampStyleNonce(el: HTMLElement): void {
|
|
567
|
+
if (el.getAttribute("nonce") || el.nonce) return;
|
|
567
568
|
const meta = document.querySelector?.('meta[name="csp-nonce"]');
|
|
568
569
|
const tagged = document.querySelector?.("script[nonce], style[nonce]") as HTMLElement | null;
|
|
569
570
|
const nonce = meta?.getAttribute("content") || tagged?.nonce || tagged?.getAttribute("nonce") || "";
|
|
570
|
-
if (nonce)
|
|
571
|
-
|
|
572
|
-
|
|
571
|
+
if (!nonce) return;
|
|
572
|
+
el.setAttribute("nonce", nonce);
|
|
573
|
+
el.nonce = nonce;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function isInfraStyle(el: HTMLElement): boolean {
|
|
577
|
+
const id = el.getAttribute("id");
|
|
578
|
+
return id === "flare-runtime" || id === "flare-critical";
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function isStyleTracked(el: HTMLElement): boolean {
|
|
582
|
+
for (const ref of styleRefCount.values()) {
|
|
583
|
+
if (ref.element === el) return true;
|
|
573
584
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function findAdoptableStyle(children: string): HTMLElement | null {
|
|
589
|
+
const nodes = document.head?.querySelectorAll("style");
|
|
590
|
+
if (!nodes) return null;
|
|
591
|
+
for (const node of nodes) {
|
|
592
|
+
const el = node as HTMLElement;
|
|
593
|
+
if (isInfraStyle(el) || isStyleTracked(el)) continue;
|
|
594
|
+
if (el.textContent === children) return el;
|
|
595
|
+
}
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function trackStyleForRoute(matchId: string, el: HTMLElement): void {
|
|
577
600
|
const list = cssElementsByRoute.get(matchId) ?? [];
|
|
578
601
|
list.push(el);
|
|
579
602
|
cssElementsByRoute.set(matchId, list);
|
|
580
603
|
}
|
|
581
604
|
|
|
605
|
+
function addCssStyleForRoute(matchId: string, children: string): void {
|
|
606
|
+
if (typeof document === "undefined") return;
|
|
607
|
+
const ref = styleRefCount.get(children);
|
|
608
|
+
if (ref) {
|
|
609
|
+
ref.refCount++;
|
|
610
|
+
trackStyleForRoute(matchId, ref.element);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const existing = findAdoptableStyle(children);
|
|
615
|
+
if (existing) {
|
|
616
|
+
existing.setAttribute("data-flare-route", matchId);
|
|
617
|
+
stampStyleNonce(existing);
|
|
618
|
+
styleRefCount.set(children, { element: existing, refCount: 1 });
|
|
619
|
+
trackStyleForRoute(matchId, existing);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const el = document.createElement("style");
|
|
624
|
+
stampStyleNonce(el);
|
|
625
|
+
el.setAttribute("data-flare-route", matchId);
|
|
626
|
+
el.textContent = children;
|
|
627
|
+
document.head.appendChild(el);
|
|
628
|
+
styleRefCount.set(children, { element: el, refCount: 1 });
|
|
629
|
+
trackStyleForRoute(matchId, el);
|
|
630
|
+
}
|
|
631
|
+
|
|
582
632
|
function applyCssForRoute(matchId: string, config: HeadConfig): void {
|
|
583
633
|
/* Snapshot old elements before adding new ones */
|
|
584
634
|
const oldElements = cssElementsByRoute.get(matchId) ?? [];
|
|
@@ -608,20 +658,34 @@ function applyCssForRoute(matchId: string, config: HeadConfig): void {
|
|
|
608
658
|
|
|
609
659
|
/* THEN decrement old refcounts — shared elements stay because new refs were added above */
|
|
610
660
|
for (const el of oldElements) {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
661
|
+
releaseCssElement(el);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function releaseCssElement(el: HTMLElement): void {
|
|
666
|
+
const href = el.getAttribute("href");
|
|
667
|
+
if (href && el.getAttribute("rel") === "stylesheet") {
|
|
668
|
+
const ref = cssRefCount.get(href);
|
|
669
|
+
if (ref) {
|
|
670
|
+
ref.refCount--;
|
|
671
|
+
if (ref.refCount <= 0) {
|
|
672
|
+
ref.element.remove();
|
|
673
|
+
cssRefCount.delete(href);
|
|
621
674
|
}
|
|
675
|
+
return;
|
|
622
676
|
}
|
|
623
|
-
el.remove();
|
|
624
677
|
}
|
|
678
|
+
const content = el.textContent ?? "";
|
|
679
|
+
const styleRef = styleRefCount.get(content);
|
|
680
|
+
if (styleRef && styleRef.element === el) {
|
|
681
|
+
styleRef.refCount--;
|
|
682
|
+
if (styleRef.refCount <= 0) {
|
|
683
|
+
styleRef.element.remove();
|
|
684
|
+
styleRefCount.delete(content);
|
|
685
|
+
}
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
el.remove();
|
|
625
689
|
}
|
|
626
690
|
|
|
627
691
|
function removeCssForRoute(routeId: string): void {
|
|
@@ -629,21 +693,7 @@ function removeCssForRoute(routeId: string): void {
|
|
|
629
693
|
if (!elements) return;
|
|
630
694
|
|
|
631
695
|
for (const el of elements) {
|
|
632
|
-
|
|
633
|
-
/* Refcounted stylesheet links */
|
|
634
|
-
if (href && el.getAttribute("rel") === "stylesheet") {
|
|
635
|
-
const ref = cssRefCount.get(href);
|
|
636
|
-
if (ref) {
|
|
637
|
-
ref.refCount--;
|
|
638
|
-
if (ref.refCount <= 0) {
|
|
639
|
-
ref.element.remove();
|
|
640
|
-
cssRefCount.delete(href);
|
|
641
|
-
}
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
/* <style> elements or untracked links — just remove */
|
|
646
|
-
el.remove();
|
|
696
|
+
releaseCssElement(el);
|
|
647
697
|
}
|
|
648
698
|
cssElementsByRoute.delete(routeId);
|
|
649
699
|
}
|
|
@@ -96,6 +96,8 @@ export interface PipelineResult {
|
|
|
96
96
|
auth: unknown | null;
|
|
97
97
|
deferContexts: Map<string, DeferContext>;
|
|
98
98
|
matches: PipelineMatch[];
|
|
99
|
+
/** Accumulated merge (root → page) for SSR `resolvedHead`. */
|
|
100
|
+
mergedHead?: HeadConfig;
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
export interface PipelineMatch {
|
|
@@ -237,6 +239,23 @@ async function buildInternalRoutes(
|
|
|
237
239
|
return result;
|
|
238
240
|
}
|
|
239
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Per-match head for client apply (`ph`, NDJSON, match cache).
|
|
244
|
+
* Scalars / objects / images / jsonLd stay merged so upsert and
|
|
245
|
+
* remove-all-then-append fields still see the full set. Concatenate
|
|
246
|
+
* `custom.*` arrays stay on the declaring route so descendants do not replay them.
|
|
247
|
+
*/
|
|
248
|
+
function toClientHeadConfig(merged: HeadConfig | undefined, own: HeadConfig | undefined): HeadConfig | undefined {
|
|
249
|
+
if (!merged) return undefined;
|
|
250
|
+
const client: HeadConfig = { ...merged };
|
|
251
|
+
if (own?.custom !== undefined) {
|
|
252
|
+
client.custom = own.custom;
|
|
253
|
+
} else {
|
|
254
|
+
delete client.custom;
|
|
255
|
+
}
|
|
256
|
+
return client;
|
|
257
|
+
}
|
|
258
|
+
|
|
240
259
|
export async function runPipeline<TEnv = unknown>(config: PipelineConfig<TEnv>): Promise<PipelineResult> {
|
|
241
260
|
const { abortController, cause, env, localeConfig, prefetch, request, routes, ssrCacheDefaults, url } = config;
|
|
242
261
|
const tracer = config.tracer ?? noopTracer;
|
|
@@ -605,11 +624,12 @@ export async function runPipeline<TEnv = unknown>(config: PipelineConfig<TEnv>):
|
|
|
605
624
|
let mergedHead: HeadConfig | undefined;
|
|
606
625
|
|
|
607
626
|
for (const match of matches) {
|
|
627
|
+
let routeHead: HeadConfig | undefined;
|
|
608
628
|
if (match.route.head && match.status === "success") {
|
|
609
629
|
const location = routeLocationMap.get(match.route);
|
|
610
630
|
if (location) {
|
|
611
631
|
try {
|
|
612
|
-
|
|
632
|
+
routeHead = match.route.head({
|
|
613
633
|
...urlHelpers,
|
|
614
634
|
cause,
|
|
615
635
|
loaderData: match.loaderData,
|
|
@@ -631,10 +651,13 @@ export async function runPipeline<TEnv = unknown>(config: PipelineConfig<TEnv>):
|
|
|
631
651
|
}
|
|
632
652
|
} catch (e: unknown) {
|
|
633
653
|
match.headError = e instanceof Error ? e : new Error(String(e));
|
|
654
|
+
routeHead = undefined;
|
|
634
655
|
}
|
|
635
656
|
}
|
|
636
657
|
}
|
|
637
|
-
match
|
|
658
|
+
/* Client apply uses this match's own custom.* so concatenate fields
|
|
659
|
+
* are not replayed once per descendant. SSR uses `mergedHead`. */
|
|
660
|
+
match.headConfig = toClientHeadConfig(mergedHead, routeHead);
|
|
638
661
|
}
|
|
639
662
|
headSpan.end();
|
|
640
663
|
|
|
@@ -693,5 +716,5 @@ export async function runPipeline<TEnv = unknown>(config: PipelineConfig<TEnv>):
|
|
|
693
716
|
}
|
|
694
717
|
headersSpan.end();
|
|
695
718
|
|
|
696
|
-
return { auth, deferContexts, matches };
|
|
719
|
+
return { auth, deferContexts, matches, mergedHead };
|
|
697
720
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import MagicString from "magic-string";
|
|
2
2
|
import type { Expression, JSXAttribute, JSXOpeningElement, Node, ObjectExpression, Program } from "oxc-parser";
|
|
3
|
+
import { mergeClassList } from "../../styles/cn.ts";
|
|
3
4
|
import { parseSource } from "./parser.ts";
|
|
4
5
|
import { evaluateSxObject } from "./evaluator.ts";
|
|
5
6
|
import { emitAtomic } from "./atomic.ts";
|
|
@@ -82,7 +83,13 @@ function isDOMElement(tag: string): boolean {
|
|
|
82
83
|
return tag.length > 0 && tag[0] !== "" && tag[0] === tag[0].toLowerCase();
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
function isCnCall(expr:
|
|
86
|
+
function isCnCall(expr: unknown): expr is {
|
|
87
|
+
type: "CallExpression";
|
|
88
|
+
callee: { type: "Identifier"; name: string };
|
|
89
|
+
arguments: Expression[];
|
|
90
|
+
start: number;
|
|
91
|
+
end: number;
|
|
92
|
+
} {
|
|
86
93
|
if (!isCallExpression(expr)) return false;
|
|
87
94
|
return expr.callee.type === "Identifier" && expr.callee.name === "cn";
|
|
88
95
|
}
|
|
@@ -236,67 +243,91 @@ function buildTwRule(token: string, body: string): string {
|
|
|
236
243
|
return `.${escaped} { ${body} }`;
|
|
237
244
|
}
|
|
238
245
|
|
|
239
|
-
function
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
246
|
+
function isIgnorableClassNode(el: unknown): boolean {
|
|
247
|
+
if (isIdentifier(el) && el.name === "undefined") return true;
|
|
248
|
+
if (typeof el !== "object" || el === null) return false;
|
|
249
|
+
const node = el as { type?: string; value?: unknown };
|
|
250
|
+
return node.type === "Literal" && (node.value === null || node.value === false);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/*
|
|
254
|
+
* Fully static cn() arguments become one class string (before conflict merge).
|
|
255
|
+
* Any dynamic argument returns null so the call stays at runtime.
|
|
256
|
+
*/
|
|
257
|
+
function staticCnParts(args: readonly unknown[]): string[] | null {
|
|
258
|
+
const parts: string[] = [];
|
|
259
|
+
for (const arg of args) {
|
|
260
|
+
if (arg === null) continue;
|
|
261
|
+
if (isStringLiteral(arg)) {
|
|
262
|
+
if (arg.value) parts.push(arg.value);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (isIgnorableClassNode(arg)) continue;
|
|
266
|
+
if (isArrayExpression(arg)) {
|
|
267
|
+
const inner = staticCnParts(arg.elements);
|
|
268
|
+
if (inner === null) return null;
|
|
269
|
+
parts.push(...inner);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (isCnCall(arg)) {
|
|
273
|
+
const inner = staticCnParts(arg.arguments);
|
|
274
|
+
if (inner === null) return null;
|
|
275
|
+
parts.push(...inner);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
245
279
|
}
|
|
246
|
-
return
|
|
280
|
+
return parts;
|
|
247
281
|
}
|
|
248
282
|
|
|
249
283
|
/*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
284
|
+
* Static arrays become a literal. Mixed arrays become a cn() call so runtime
|
|
285
|
+
* merge sees every token, including ones hidden behind a conditional.
|
|
252
286
|
*/
|
|
253
287
|
function resolveClassArray(
|
|
254
288
|
source: string,
|
|
255
289
|
elements: Array<Expression | null>,
|
|
256
|
-
): { kind: "literal"; value: string } | { kind: "
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
return { kind: "literal", value: allStatic.filter(Boolean).join(" ") };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const parts: string[] = [];
|
|
263
|
-
let first = true;
|
|
290
|
+
): { kind: "literal"; value: string } | { kind: "cn"; expr: string } {
|
|
291
|
+
const args: string[] = [];
|
|
292
|
+
let dynamic = false;
|
|
264
293
|
for (const el of elements) {
|
|
265
|
-
if (el === null) continue;
|
|
294
|
+
if (el === null || isIgnorableClassNode(el)) continue;
|
|
266
295
|
if (isStringLiteral(el)) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
} else if (
|
|
270
|
-
typeof el === "object" &&
|
|
271
|
-
el !== null &&
|
|
272
|
-
(el as { type: string }).type === "LogicalExpression" &&
|
|
273
|
-
(el as { operator?: string }).operator === "&&"
|
|
274
|
-
) {
|
|
275
|
-
const logic = el as { left: Expression; right: Expression };
|
|
276
|
-
const leftSrc = srcOf(source, logic.left as { start: number; end: number });
|
|
277
|
-
const rightEl = logic.right;
|
|
278
|
-
if (isStringLiteral(rightEl)) {
|
|
279
|
-
const sep = first ? "" : " ";
|
|
280
|
-
parts.push(`\${${leftSrc} ? "${sep}${rightEl.value}" : ""}`);
|
|
281
|
-
} else {
|
|
282
|
-
const sep = first ? "" : " ";
|
|
283
|
-
const rightSrc = srcOf(source, rightEl as { start: number; end: number });
|
|
284
|
-
parts.push(`\${${leftSrc} ? \`${sep}\${${rightSrc}}\` : ""}`);
|
|
285
|
-
}
|
|
286
|
-
first = false;
|
|
287
|
-
} else {
|
|
288
|
-
/* Skip null/false literals and the `undefined` identifier — they contribute nothing. */
|
|
289
|
-
const isNullLit = (el as { type: string }).type === "Literal" && (el as { value: unknown }).value === null;
|
|
290
|
-
const isFalseLit = (el as { type: string }).type === "Literal" && (el as { value: unknown }).value === false;
|
|
291
|
-
const isUndefined = isIdentifier(el) && el.name === "undefined";
|
|
292
|
-
if (isNullLit || isFalseLit || isUndefined) continue;
|
|
293
|
-
const sep = first ? "" : " ";
|
|
294
|
-
parts.push(`${sep}\${${srcOf(source, el as { start: number; end: number })}}`);
|
|
295
|
-
first = false;
|
|
296
|
+
if (el.value) args.push(JSON.stringify(el.value));
|
|
297
|
+
continue;
|
|
296
298
|
}
|
|
299
|
+
dynamic = true;
|
|
300
|
+
args.push(srcOf(source, el as { start: number; end: number }));
|
|
301
|
+
}
|
|
302
|
+
if (!dynamic) {
|
|
303
|
+
return { kind: "literal", value: args.map((part) => JSON.parse(part) as string).join(" ") };
|
|
297
304
|
}
|
|
305
|
+
return { expr: `cn(${args.join(", ")})`, kind: "cn" };
|
|
306
|
+
}
|
|
298
307
|
|
|
299
|
-
|
|
308
|
+
function dropUnusedCnImport(code: string): string {
|
|
309
|
+
const withoutImports = code.replace(/import\s*\{[^}]*\}\s*from\s*["'][^"']+["']\s*;?/g, "");
|
|
310
|
+
if (/\bcn\b/.test(withoutImports)) return code;
|
|
311
|
+
return code.replace(
|
|
312
|
+
/import\s*\{([^}]*)\}\s*from\s*(["'](?:@lovrozagar\/flare\/styles|flare\/styles)["'])\s*;?/g,
|
|
313
|
+
(full, specifiers: string, from: string) => {
|
|
314
|
+
const parts = specifiers
|
|
315
|
+
.split(",")
|
|
316
|
+
.map((part) => part.trim())
|
|
317
|
+
.filter(Boolean);
|
|
318
|
+
const kept = parts.filter(
|
|
319
|
+
(part) =>
|
|
320
|
+
(part
|
|
321
|
+
.split(/\s+as\s+/)
|
|
322
|
+
.pop()
|
|
323
|
+
?.trim() ?? part) !== "cn",
|
|
324
|
+
);
|
|
325
|
+
if (kept.length === parts.length) return full;
|
|
326
|
+
if (kept.length === 0) return "";
|
|
327
|
+
const semicolon = full.trimEnd().endsWith(";") ? ";" : "";
|
|
328
|
+
return `import { ${kept.join(", ")} } from ${from}${semicolon}`;
|
|
329
|
+
},
|
|
330
|
+
);
|
|
300
331
|
}
|
|
301
332
|
|
|
302
333
|
function findAttr(opening: JSXOpeningElement, name: string): JSXAttribute | null {
|
|
@@ -327,7 +358,9 @@ function hasSpread(opening: JSXOpeningElement): { identName: string } | null {
|
|
|
327
358
|
function resolveClassAttr(
|
|
328
359
|
source: string,
|
|
329
360
|
classAttr: JSXAttribute,
|
|
330
|
-
):
|
|
361
|
+
):
|
|
362
|
+
| { kind: "literal"; value: string; rewrite?: boolean }
|
|
363
|
+
| { kind: "expr"; expr: string; wasArray: boolean; wasDynamic: boolean } {
|
|
331
364
|
const cv = classAttr.value;
|
|
332
365
|
if (cv === null) return { kind: "literal", value: "" };
|
|
333
366
|
|
|
@@ -339,15 +372,18 @@ function resolveClassAttr(
|
|
|
339
372
|
if (isArrayExpression(expr)) {
|
|
340
373
|
const resolved = resolveClassArray(source, expr.elements);
|
|
341
374
|
if (resolved.kind === "literal") return { kind: "literal", value: resolved.value };
|
|
342
|
-
return { expr:
|
|
375
|
+
return { expr: resolved.expr, kind: "expr", wasArray: true, wasDynamic: true };
|
|
343
376
|
}
|
|
344
377
|
if (isCnCall(expr)) {
|
|
345
|
-
|
|
378
|
+
const parts = staticCnParts(expr.arguments);
|
|
379
|
+
if (parts !== null) {
|
|
380
|
+
return { kind: "literal", rewrite: true, value: parts.join(" ") };
|
|
381
|
+
}
|
|
346
382
|
return {
|
|
347
383
|
expr: srcOf(source, expr as { start: number; end: number }),
|
|
348
384
|
kind: "expr",
|
|
349
385
|
wasArray: false,
|
|
350
|
-
wasDynamic:
|
|
386
|
+
wasDynamic: true,
|
|
351
387
|
};
|
|
352
388
|
}
|
|
353
389
|
/* Dynamic expression → wrap in cn() */
|
|
@@ -379,6 +415,7 @@ export function rewriteModule(source: string, ctx: RewriteCtx): RewriteResult |
|
|
|
379
415
|
const insertsBefore: Array<{ pos: number; text: string }> = [];
|
|
380
416
|
|
|
381
417
|
const neededImports = new Set<FlareImport>();
|
|
418
|
+
let foldedStaticCn = false;
|
|
382
419
|
const emittedClasses = new Set<string>();
|
|
383
420
|
let changed = false;
|
|
384
421
|
|
|
@@ -451,26 +488,19 @@ export function rewriteModule(source: string, ctx: RewriteCtx): RewriteResult |
|
|
|
451
488
|
if (classAttr) {
|
|
452
489
|
const resolved = resolveClassAttr(source, classAttr);
|
|
453
490
|
if (resolved.kind === "literal") {
|
|
454
|
-
|
|
491
|
+
const merged = mergeClassList(resolved.value);
|
|
492
|
+
classLiteral = merged;
|
|
493
|
+
if (resolved.rewrite || merged !== resolved.value) classNeedsRewrite = true;
|
|
494
|
+
if (resolved.rewrite) foldedStaticCn = true;
|
|
495
|
+
compileTwFromString(merged, ctx);
|
|
455
496
|
} else {
|
|
456
497
|
classExpr = resolved.expr;
|
|
457
498
|
classWasArray = resolved.wasArray;
|
|
458
499
|
classWasDynamic = resolved.wasDynamic;
|
|
459
500
|
if (classWasDynamic) neededImports.add("cn");
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const cv = classAttr.value;
|
|
464
|
-
if (cv === null) {
|
|
465
|
-
/* boolean attr — nothing to compile */
|
|
466
|
-
} else if (isStringLiteral(cv)) {
|
|
467
|
-
compileTwFromString(cv.value, ctx);
|
|
468
|
-
} else if (isJSXExpressionContainer(cv)) {
|
|
469
|
-
const expr = cv.expression;
|
|
470
|
-
if (isStringLiteral(expr)) {
|
|
471
|
-
compileTwFromString(expr.value, ctx);
|
|
472
|
-
} else {
|
|
473
|
-
compileTwFromExpr(expr, ctx);
|
|
501
|
+
const cv = classAttr.value;
|
|
502
|
+
if (cv !== null && isJSXExpressionContainer(cv)) {
|
|
503
|
+
compileTwFromExpr(cv.expression, ctx);
|
|
474
504
|
}
|
|
475
505
|
}
|
|
476
506
|
}
|
|
@@ -601,7 +631,12 @@ export function rewriteModule(source: string, ctx: RewriteCtx): RewriteResult |
|
|
|
601
631
|
return isArrayExpression((cv as { expression: unknown }).expression);
|
|
602
632
|
})()
|
|
603
633
|
: false;
|
|
604
|
-
|
|
634
|
+
const cssRewritesClass = cssAttr !== null && cssAttr.value !== null;
|
|
635
|
+
if (
|
|
636
|
+
classAttr &&
|
|
637
|
+
!cssRewritesClass &&
|
|
638
|
+
(classAttrWasArray || classWasArray || classWasDynamic || classNeedsRewrite)
|
|
639
|
+
) {
|
|
605
640
|
emitClassAttr(classAttr, classAttr.start, classLiteral, classExpr);
|
|
606
641
|
} else if (!classAttr && spread && dom) {
|
|
607
642
|
/* Spread on DOM with no class attr — don't inject anything */
|
|
@@ -672,8 +707,9 @@ export function rewriteModule(source: string, ctx: RewriteCtx): RewriteResult |
|
|
|
672
707
|
} else if (classAttr) {
|
|
673
708
|
const existing = resolveClassAttr(source, classAttr);
|
|
674
709
|
if (existing.kind === "literal") {
|
|
675
|
-
|
|
676
|
-
|
|
710
|
+
const merged = mergeClassList(existing.value);
|
|
711
|
+
if (merged) {
|
|
712
|
+
replace(classAttr.start, classAttr.end, `class={cn("${merged}", ${compileCssCall})}`);
|
|
677
713
|
neededImports.add("cn");
|
|
678
714
|
} else {
|
|
679
715
|
replace(classAttr.start, classAttr.end, `class={${compileCssCall}}`);
|
|
@@ -759,5 +795,6 @@ export function rewriteModule(source: string, ctx: RewriteCtx): RewriteResult |
|
|
|
759
795
|
}
|
|
760
796
|
}
|
|
761
797
|
|
|
762
|
-
|
|
798
|
+
const code = foldedStaticCn ? dropUnusedCnImport(ms.toString()) : ms.toString();
|
|
799
|
+
return { code, emittedClasses, map: null };
|
|
763
800
|
}
|
|
@@ -1214,7 +1214,7 @@ export function createServerHandler<
|
|
|
1214
1214
|
});
|
|
1215
1215
|
}
|
|
1216
1216
|
|
|
1217
|
-
const resolvedHead = pipelineResult.
|
|
1217
|
+
const resolvedHead = pipelineResult.mergedHead ?? {};
|
|
1218
1218
|
|
|
1219
1219
|
const fuzzyLogs = enableServerLogs ? getServerLogs() : undefined;
|
|
1220
1220
|
if (request.headers.get(HEADER_DATA) === HEADER_FLAG) {
|
|
@@ -1517,7 +1517,7 @@ export function createServerHandler<
|
|
|
1517
1517
|
const logsForTransport = collectedLogs && collectedLogs.length > 0 ? collectedLogs : undefined;
|
|
1518
1518
|
|
|
1519
1519
|
if (!isDataRequest) {
|
|
1520
|
-
const resolvedHead = pipelineResult.
|
|
1520
|
+
const resolvedHead = pipelineResult.mergedHead ?? {};
|
|
1521
1521
|
|
|
1522
1522
|
let ssrResult: ReturnType<typeof renderToStream>;
|
|
1523
1523
|
setRewrite(composedRewrite);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Vendored from shadcn-ui/cn
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2026 shadcn
|
|
6
|
+
|
|
7
|
+
https://github.com/shadcn-ui/cn
|
|
8
|
+
|
|
9
|
+
- tag: `cn@0.4.0`
|
|
10
|
+
- commit: `84db83298f69a229d6f1ffc5d8c8d99ef451fb49`
|
|
11
|
+
|
|
12
|
+
Copied files (runtime slice only):
|
|
13
|
+
|
|
14
|
+
- `engine.ts`
|
|
15
|
+
- `tables.generated.ts`
|
|
16
|
+
- `types.ts`
|
|
17
|
+
|
|
18
|
+
Not copied: `compiler.ts`, `build.ts`, `config.ts`, `default-config.generated.ts`, `vite.ts`, `next.ts`, `lite.ts`, `theme-css.ts`, `validators.ts`, `index.ts`.
|