@jxsuite/compiler 1.0.0 → 1.2.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.js +251 -176
- package/dist/compiler.js +117 -112
- package/package.json +7 -6
- package/src/cli.ts +30 -0
- package/src/compiler.ts +18 -2
- package/src/shared.ts +220 -112
- package/src/site/bundler.ts +187 -0
- package/src/site/dev-command.ts +84 -0
- package/src/site/head-merger.ts +13 -0
- package/src/site/image-cache.ts +57 -6
- package/src/site/image-transform.ts +21 -0
- package/src/site/preview-server.ts +93 -0
- package/src/site/prototype-resolver.ts +15 -0
- package/src/site/site-build.ts +163 -15
- package/src/targets/compile-client.ts +53 -8
- package/src/targets/compile-element.ts +69 -7
- package/src/targets/compile-static.ts +17 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/compiler",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Jx static HTML compiler, island detector, and site builder",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -45,15 +45,16 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@apidevtools/json-schema-ref-parser": "^15.5.0",
|
|
48
|
-
"@jxsuite/create": "^1.
|
|
49
|
-
"@jxsuite/runtime": "^1.
|
|
50
|
-
"@jxsuite/schema": "^1.
|
|
48
|
+
"@jxsuite/create": "^1.1.1",
|
|
49
|
+
"@jxsuite/runtime": "^1.2.0",
|
|
50
|
+
"@jxsuite/schema": "^1.2.0",
|
|
51
|
+
"esbuild": "^0.28.1",
|
|
51
52
|
"sharp": "^0.35.3"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
55
|
"@happy-dom/global-registrator": "^20.10.6",
|
|
55
|
-
"@jxsuite/connector": "^0.
|
|
56
|
-
"@jxsuite/parser": "^1.
|
|
56
|
+
"@jxsuite/connector": "^0.3.1",
|
|
57
|
+
"@jxsuite/parser": "^1.2.0",
|
|
57
58
|
"@types/mdast": "^4.0.4"
|
|
58
59
|
}
|
|
59
60
|
}
|
package/src/cli.ts
CHANGED
|
@@ -23,6 +23,8 @@ async function main() {
|
|
|
23
23
|
|
|
24
24
|
Commands:
|
|
25
25
|
build [root] Build a Jx site to dist/
|
|
26
|
+
dev [root] Start the dev server (requires @jxsuite/server and Bun)
|
|
27
|
+
preview [root] Serve an already-built dist/ directory
|
|
26
28
|
schema [root] Generate project.schema.json + document.schema.json from project.json#/extensions
|
|
27
29
|
validate [root] Validate project.json against its generated project.schema.json
|
|
28
30
|
db push [root] Sync the data section's tables to their connections (additive-only)
|
|
@@ -30,6 +32,7 @@ Commands:
|
|
|
30
32
|
Options:
|
|
31
33
|
--verbose Print detailed build progress
|
|
32
34
|
--no-clean Don't clean outDir before building
|
|
35
|
+
--port <n> dev/preview: port to listen on (default 3000 / 4173)
|
|
33
36
|
--dry-run db push: print the statements without executing them
|
|
34
37
|
--connection db push: restrict to one connection name`);
|
|
35
38
|
process.exit(0);
|
|
@@ -42,11 +45,18 @@ Options:
|
|
|
42
45
|
const flags = new Set<string>();
|
|
43
46
|
const positionals: string[] = [];
|
|
44
47
|
let connectionArg: string | undefined;
|
|
48
|
+
let portArg: number | undefined;
|
|
45
49
|
for (let index = 0; index < rest.length; index += 1) {
|
|
46
50
|
const arg = rest[index]!;
|
|
47
51
|
if (arg === "--connection") {
|
|
48
52
|
index += 1;
|
|
49
53
|
connectionArg = rest[index];
|
|
54
|
+
} else if (arg === "--port") {
|
|
55
|
+
index += 1;
|
|
56
|
+
const parsed = Math.trunc(Number(rest[index] ?? ""));
|
|
57
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
58
|
+
portArg = parsed;
|
|
59
|
+
}
|
|
50
60
|
} else if (arg.startsWith("--")) {
|
|
51
61
|
flags.add(arg);
|
|
52
62
|
} else {
|
|
@@ -118,6 +128,26 @@ Options:
|
|
|
118
128
|
console.error(`Build failed: ${err.message}`);
|
|
119
129
|
process.exit(1);
|
|
120
130
|
}
|
|
131
|
+
} else if (command === "dev") {
|
|
132
|
+
try {
|
|
133
|
+
const { runDev } = await import("./site/dev-command.ts");
|
|
134
|
+
runDev(projectRoot, portArg);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const err = error as Error;
|
|
137
|
+
console.error(err.message);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
} else if (command === "preview") {
|
|
141
|
+
const { existsSync } = await import("node:fs");
|
|
142
|
+
const distDir = resolve(projectRoot, "dist");
|
|
143
|
+
if (!existsSync(distDir)) {
|
|
144
|
+
console.error(`No build output at ${distDir} — run \`jx build\` first.`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
const { startPreviewServer } = await import("./site/preview-server.ts");
|
|
148
|
+
const port = portArg ?? 4173;
|
|
149
|
+
startPreviewServer(distDir, port);
|
|
150
|
+
console.log(`Previewing ${distDir} at http://localhost:${port}`);
|
|
121
151
|
} else if (command === "schema") {
|
|
122
152
|
try {
|
|
123
153
|
const { writeProjectSchemas } = await import("./site/schema-command.ts");
|
package/src/compiler.ts
CHANGED
|
@@ -46,6 +46,13 @@ export interface CompileOptions {
|
|
|
46
46
|
litHtmlSrc?: string;
|
|
47
47
|
projectStyle?: JxStyle | null;
|
|
48
48
|
formats?: FormatRegistry;
|
|
49
|
+
/** Rewrite Function-def `$src` specifiers to served URLs (see CompileElementOptions). */
|
|
50
|
+
rewriteSrc?: (specifier: string) => string;
|
|
51
|
+
/**
|
|
52
|
+
* Inject the color-scheme pre-paint script when `$media` declares a pure scheme query (default
|
|
53
|
+
* true). The site pipeline disables this and injects via the merged <head> instead.
|
|
54
|
+
*/
|
|
55
|
+
prePaintScheme?: boolean;
|
|
49
56
|
[key: string]: unknown;
|
|
50
57
|
}
|
|
51
58
|
|
|
@@ -107,6 +114,8 @@ export async function compile(sourcePath: string | JxDocument, opts: CompileOpti
|
|
|
107
114
|
litHtmlSrc,
|
|
108
115
|
projectStyle,
|
|
109
116
|
reactivitySrc,
|
|
117
|
+
...(opts.prePaintScheme === false ? { prePaintScheme: false } : {}),
|
|
118
|
+
...(opts.rewriteSrc ? { rewriteSrc: opts.rewriteSrc } : {}),
|
|
110
119
|
title,
|
|
111
120
|
});
|
|
112
121
|
}
|
|
@@ -115,7 +124,7 @@ export async function compile(sourcePath: string | JxDocument, opts: CompileOpti
|
|
|
115
124
|
if (raw.tagName && raw.tagName.includes("-")) {
|
|
116
125
|
const { tagName } = raw;
|
|
117
126
|
const className = tagNameToClassName(tagName);
|
|
118
|
-
const moduleContent = emitElementModule(raw, className, []);
|
|
127
|
+
const moduleContent = emitElementModule(raw, className, [], opts.rewriteSrc);
|
|
119
128
|
const moduleFile = {
|
|
120
129
|
content: moduleContent,
|
|
121
130
|
path: `${tagName}.js`,
|
|
@@ -149,7 +158,14 @@ export async function compile(sourcePath: string | JxDocument, opts: CompileOpti
|
|
|
149
158
|
}
|
|
150
159
|
|
|
151
160
|
// Route 3: Dynamic with standard tagName → pre-rendered HTML + reactive bindings
|
|
152
|
-
return compileClient(raw, {
|
|
161
|
+
return compileClient(raw, {
|
|
162
|
+
litHtmlSrc,
|
|
163
|
+
projectStyle,
|
|
164
|
+
reactivitySrc,
|
|
165
|
+
...(opts.prePaintScheme === false ? { prePaintScheme: false } : {}),
|
|
166
|
+
...(opts.rewriteSrc ? { rewriteSrc: opts.rewriteSrc } : {}),
|
|
167
|
+
title,
|
|
168
|
+
});
|
|
153
169
|
}
|
|
154
170
|
|
|
155
171
|
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
package/src/shared.ts
CHANGED
|
@@ -6,13 +6,24 @@
|
|
|
6
6
|
* compilation targets (static, client, element, server).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
COLOR_SCHEME_ATTR,
|
|
11
|
+
COLOR_SCHEME_STORAGE_KEY,
|
|
12
|
+
RESERVED_KEYS,
|
|
13
|
+
camelToKebab,
|
|
14
|
+
pureSchemeOf,
|
|
15
|
+
schemeSelectors,
|
|
16
|
+
toCSSText,
|
|
17
|
+
} from "@jxsuite/runtime";
|
|
10
18
|
import { evaluateExpression, isMutating } from "@jxsuite/runtime/expression";
|
|
19
|
+
import { runStatements } from "@jxsuite/runtime/statements";
|
|
11
20
|
import {
|
|
12
21
|
childrenContainArray,
|
|
22
|
+
hasStructuredBody,
|
|
13
23
|
isExpressionDef,
|
|
14
24
|
isFunctionDef,
|
|
15
25
|
isMappedArray,
|
|
26
|
+
isNamedFormulaDef,
|
|
16
27
|
isPrototypeDef,
|
|
17
28
|
isRef,
|
|
18
29
|
isSchemaOnlyDef as isSchemaOnly,
|
|
@@ -33,8 +44,38 @@ import type {
|
|
|
33
44
|
} from "@jxsuite/schema/types";
|
|
34
45
|
|
|
35
46
|
// Re-export runtime utilities used by submodules
|
|
36
|
-
export {
|
|
47
|
+
export {
|
|
48
|
+
COLOR_SCHEME_ATTR,
|
|
49
|
+
COLOR_SCHEME_STORAGE_KEY,
|
|
50
|
+
RESERVED_KEYS,
|
|
51
|
+
camelToKebab,
|
|
52
|
+
pureSchemeOf,
|
|
53
|
+
schemeSelectors,
|
|
54
|
+
toCSSText,
|
|
55
|
+
} from "@jxsuite/runtime";
|
|
37
56
|
export { compileExpression, evaluateExpression, isMutating } from "@jxsuite/runtime/expression";
|
|
57
|
+
export { compileStatements, runStatements } from "@jxsuite/runtime/statements";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Emit the JS source of a named formula's scope callable: positional arguments are mapped onto the
|
|
61
|
+
* declared parameter names (honoring CemParameter defaults) as the `_args` object the compiled
|
|
62
|
+
* body's `$args/` refs read from. Call sites stay positional, matching the interpreter.
|
|
63
|
+
*/
|
|
64
|
+
export function emitFormulaFn(def: { parameters?: unknown[] }, compiledBody: string): string {
|
|
65
|
+
const entries: string[] = [];
|
|
66
|
+
for (const [i, p] of (def.parameters ?? []).entries()) {
|
|
67
|
+
const name = typeof p === "string" ? p : ((p as { name?: string } | null)?.name ?? "");
|
|
68
|
+
if (!name) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const hasDefault = typeof p === "object" && p !== null && "default" in p;
|
|
72
|
+
const arg = hasDefault
|
|
73
|
+
? `_a[${i}] === undefined ? ${JSON.stringify((p as { default?: unknown }).default)} : _a[${i}]`
|
|
74
|
+
: `_a[${i}]`;
|
|
75
|
+
entries.push(`${JSON.stringify(name)}: ${arg}`);
|
|
76
|
+
}
|
|
77
|
+
return `(..._a) => { const _args = { ${entries.join(", ")} }; return ${compiledBody}; }`;
|
|
78
|
+
}
|
|
38
79
|
export type { ExpressionNode } from "@jxsuite/runtime/expression";
|
|
39
80
|
|
|
40
81
|
// CDN defaults
|
|
@@ -283,7 +324,24 @@ export function buildInitialScope(
|
|
|
283
324
|
}
|
|
284
325
|
if (isExpressionDef(def)) {
|
|
285
326
|
const node = def.$expression as ExpressionNode;
|
|
286
|
-
if (
|
|
327
|
+
if (isNamedFormulaDef(def)) {
|
|
328
|
+
// Named formula: callable, positional args mapped onto its declared parameters.
|
|
329
|
+
const params = def.parameters as (string | { name?: string; default?: unknown })[];
|
|
330
|
+
setOwnScopeValue(scope, key, (...argValues: unknown[]) => {
|
|
331
|
+
const args: Record<string, unknown> = {};
|
|
332
|
+
for (const [i, p] of params.entries()) {
|
|
333
|
+
const name = typeof p === "string" ? p : (p?.name ?? "");
|
|
334
|
+
if (!name) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
args[name] =
|
|
338
|
+
argValues[i] === undefined && typeof p === "object" && p !== null && "default" in p
|
|
339
|
+
? p.default
|
|
340
|
+
: argValues[i];
|
|
341
|
+
}
|
|
342
|
+
return evaluateExpression(node, scope, null, { args });
|
|
343
|
+
});
|
|
344
|
+
} else if (isMutating(node.operator)) {
|
|
287
345
|
setOwnScopeValue(scope, key, (s: Record<string, unknown>, event: Event) =>
|
|
288
346
|
evaluateExpression(node, s, event),
|
|
289
347
|
);
|
|
@@ -293,12 +351,19 @@ export function buildInitialScope(
|
|
|
293
351
|
continue;
|
|
294
352
|
}
|
|
295
353
|
if (isFunctionDef(def)) {
|
|
296
|
-
if (def
|
|
354
|
+
if (hasStructuredBody(def)) {
|
|
355
|
+
// Structured body (spec §20): a side-effecting handler in the build-time scope.
|
|
356
|
+
const { body } = def;
|
|
357
|
+
setOwnScopeValue(scope, key, (s: Record<string, unknown>, event?: Event) => {
|
|
358
|
+
void runStatements(body, s ?? scope, event ?? null);
|
|
359
|
+
});
|
|
360
|
+
} else if (typeof def.body === "string") {
|
|
297
361
|
const names = def.parameters ? paramNames(def.parameters) : (def.arguments ?? []);
|
|
298
|
-
const
|
|
362
|
+
const { body } = def;
|
|
363
|
+
const fn = new Function("state", ...names, body) as (
|
|
299
364
|
state: Record<string, unknown>,
|
|
300
365
|
) => unknown;
|
|
301
|
-
if (
|
|
366
|
+
if (body.includes("return")) {
|
|
302
367
|
defineLazyScopeValue(scope, key, () => fn(scope));
|
|
303
368
|
} else {
|
|
304
369
|
setOwnScopeValue(scope, key, fn);
|
|
@@ -593,6 +658,94 @@ export function buildInner(
|
|
|
593
658
|
|
|
594
659
|
// ─── CSS extraction ───────────────────────────────────────────────────────────
|
|
595
660
|
|
|
661
|
+
/**
|
|
662
|
+
* The inline pre-paint script injected into <head> when a project declares a pure color-scheme
|
|
663
|
+
* media query: restores the visitor's persisted forced scheme before first paint (no FOUC).
|
|
664
|
+
*
|
|
665
|
+
* @returns {string}
|
|
666
|
+
* @docs framework/concepts/color-schemes
|
|
667
|
+
*/
|
|
668
|
+
export function colorSchemePrePaintScript(): string {
|
|
669
|
+
const key = JSON.stringify(COLOR_SCHEME_STORAGE_KEY);
|
|
670
|
+
const attr = JSON.stringify(COLOR_SCHEME_ATTR);
|
|
671
|
+
return `(function(){try{var s=localStorage.getItem(${key});if(s==="light"||s==="dark"){document.documentElement.setAttribute(${attr},s)}}catch(e){}})()`;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Resolve an `@`-prefixed style key into an emit function that pushes conditional rules. Pure
|
|
676
|
+
* color-scheme queries dual-emit per the forced-scheme contract (spec §9.5): a media-guarded copy
|
|
677
|
+
* that applies while no scheme is forced plus an unconditional copy under the forced root
|
|
678
|
+
* attribute.
|
|
679
|
+
*
|
|
680
|
+
* @param {string} atKey
|
|
681
|
+
* @param {Record<string, string>} mediaQueries
|
|
682
|
+
* @param {string[]} rules
|
|
683
|
+
* @returns {(selector: string, props: string) => void}
|
|
684
|
+
* @docs framework/concepts/color-schemes
|
|
685
|
+
*/
|
|
686
|
+
function conditionalRuleEmitter(
|
|
687
|
+
atKey: string,
|
|
688
|
+
mediaQueries: Record<string, string>,
|
|
689
|
+
rules: string[],
|
|
690
|
+
): (selector: string, props: string) => void {
|
|
691
|
+
const query = atKey.startsWith("@--")
|
|
692
|
+
? (mediaQueries[atKey.slice(1)] ?? atKey.slice(1))
|
|
693
|
+
: atKey.startsWith("@(")
|
|
694
|
+
? atKey.slice(1)
|
|
695
|
+
: null;
|
|
696
|
+
const atRule = query === null ? atKey : `@media ${query}`;
|
|
697
|
+
const scheme = query === null ? null : pureSchemeOf(query);
|
|
698
|
+
return (selector: string, props: string) => {
|
|
699
|
+
if (!props) {
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (scheme) {
|
|
703
|
+
const { auto, forced } = schemeSelectors(selector, scheme);
|
|
704
|
+
rules.push(`${atRule} { ${auto} { ${props} } }`, `${forced} { ${props} }`);
|
|
705
|
+
} else {
|
|
706
|
+
rules.push(`${atRule} { ${selector} { ${props} } }`);
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Push the rules for one `@`-prefixed style block scoped to `selector`: flat props plus one level
|
|
713
|
+
* of nested selectors, routed through conditionalRuleEmitter (scheme-aware).
|
|
714
|
+
*
|
|
715
|
+
* @param {string[]} rules
|
|
716
|
+
* @param {string} atKey
|
|
717
|
+
* @param {Record<string, string>} mediaQueries
|
|
718
|
+
* @param {string} selector
|
|
719
|
+
* @param {Record<string, unknown>} obj
|
|
720
|
+
*/
|
|
721
|
+
function pushConditionalRule(
|
|
722
|
+
rules: string[],
|
|
723
|
+
atKey: string,
|
|
724
|
+
mediaQueries: Record<string, string>,
|
|
725
|
+
selector: string,
|
|
726
|
+
obj: Record<string, unknown>,
|
|
727
|
+
) {
|
|
728
|
+
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const emit = conditionalRuleEmitter(atKey, mediaQueries, rules);
|
|
732
|
+
emit(selector, toCSSText(obj));
|
|
733
|
+
for (const [sel, sub] of Object.entries(obj)) {
|
|
734
|
+
if (sub === null || typeof sub !== "object" || Array.isArray(sub)) {
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
if (sel.startsWith("@")) {
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
const resolved = sel.startsWith("&")
|
|
741
|
+
? sel.replace("&", selector)
|
|
742
|
+
: sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
|
|
743
|
+
? `${selector}${sel}`
|
|
744
|
+
: `${selector} ${sel}`;
|
|
745
|
+
emit(resolved, toCSSText(sub));
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
596
749
|
/**
|
|
597
750
|
* Walk the entire document tree and collect all static nested CSS rules.
|
|
598
751
|
*
|
|
@@ -622,30 +775,7 @@ export function compileStyles(
|
|
|
622
775
|
continue;
|
|
623
776
|
}
|
|
624
777
|
if (key.startsWith("@")) {
|
|
625
|
-
|
|
626
|
-
? `@media ${mediaQueries[key.slice(1)] ?? key.slice(1)}`
|
|
627
|
-
: key;
|
|
628
|
-
const mProps = toCSSText(val);
|
|
629
|
-
if (mProps) {
|
|
630
|
-
rules.push(`${atRule} { ${selector} { ${mProps} } }`);
|
|
631
|
-
}
|
|
632
|
-
for (const [sel, sub] of Object.entries(val as Record<string, unknown>)) {
|
|
633
|
-
if (sub === null || typeof sub !== "object" || Array.isArray(sub)) {
|
|
634
|
-
continue;
|
|
635
|
-
}
|
|
636
|
-
if (sel.startsWith("@")) {
|
|
637
|
-
continue;
|
|
638
|
-
}
|
|
639
|
-
const resolved = sel.startsWith("&")
|
|
640
|
-
? sel.replace("&", selector)
|
|
641
|
-
: sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
|
|
642
|
-
? `${selector}${sel}`
|
|
643
|
-
: `${selector} ${sel}`;
|
|
644
|
-
const subProps = toCSSText(sub);
|
|
645
|
-
if (subProps) {
|
|
646
|
-
rules.push(`${atRule} { ${resolved} { ${subProps} } }`);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
778
|
+
pushConditionalRule(rules, key, mediaQueries, selector, val as Record<string, unknown>);
|
|
649
779
|
continue;
|
|
650
780
|
}
|
|
651
781
|
const resolved = key.startsWith("&")
|
|
@@ -657,27 +787,6 @@ export function compileStyles(
|
|
|
657
787
|
}
|
|
658
788
|
};
|
|
659
789
|
|
|
660
|
-
for (const [key, val] of Object.entries(projectStyle)) {
|
|
661
|
-
if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[")) {
|
|
662
|
-
emitProjectRules(key, val as Record<string, unknown>);
|
|
663
|
-
} else if (
|
|
664
|
-
val !== null &&
|
|
665
|
-
typeof val === "object" &&
|
|
666
|
-
!Array.isArray(val) &&
|
|
667
|
-
!key.startsWith("@") &&
|
|
668
|
-
!key.startsWith("--")
|
|
669
|
-
) {
|
|
670
|
-
emitProjectRules(key, val as Record<string, unknown>);
|
|
671
|
-
} else if (key.startsWith("@")) {
|
|
672
|
-
// @media block at top level
|
|
673
|
-
const atRule = key.startsWith("@--")
|
|
674
|
-
? `@media ${mediaQueries[key.slice(1)] ?? key.slice(1)}`
|
|
675
|
-
: key.startsWith("@(")
|
|
676
|
-
? `@media ${key.slice(1)}`
|
|
677
|
-
: key;
|
|
678
|
-
rules.push(`${atRule} { body { ${toCSSText(val as Record<string, unknown>)} } }`);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
790
|
// Collect CSS custom properties into :root {}
|
|
682
791
|
const rootProps: Record<string, unknown> = {};
|
|
683
792
|
// Collect direct CSS properties into body {}
|
|
@@ -700,6 +809,7 @@ export function compileStyles(
|
|
|
700
809
|
bodyProps[key] = val;
|
|
701
810
|
}
|
|
702
811
|
}
|
|
812
|
+
// Base rules precede conditional blocks so equal-specificity overrides win by source order.
|
|
703
813
|
const rootCSS = toCSSText(rootProps);
|
|
704
814
|
if (rootCSS) {
|
|
705
815
|
rules.push(`:root { ${rootCSS} }`);
|
|
@@ -708,6 +818,62 @@ export function compileStyles(
|
|
|
708
818
|
if (bodyCSS) {
|
|
709
819
|
rules.push(`body { ${bodyCSS} }`);
|
|
710
820
|
}
|
|
821
|
+
|
|
822
|
+
for (const [key, val] of Object.entries(projectStyle)) {
|
|
823
|
+
if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[")) {
|
|
824
|
+
emitProjectRules(key, val as Record<string, unknown>);
|
|
825
|
+
} else if (
|
|
826
|
+
val !== null &&
|
|
827
|
+
typeof val === "object" &&
|
|
828
|
+
!Array.isArray(val) &&
|
|
829
|
+
!key.startsWith("@") &&
|
|
830
|
+
!key.startsWith("--")
|
|
831
|
+
) {
|
|
832
|
+
emitProjectRules(key, val as Record<string, unknown>);
|
|
833
|
+
} else if (
|
|
834
|
+
key.startsWith("@") &&
|
|
835
|
+
val !== null &&
|
|
836
|
+
typeof val === "object" &&
|
|
837
|
+
!Array.isArray(val)
|
|
838
|
+
) {
|
|
839
|
+
// Conditional block at project top level: custom properties override :root, direct
|
|
840
|
+
// Properties override body, selector-keyed sub-objects their own selector.
|
|
841
|
+
const emit = conditionalRuleEmitter(key, mediaQueries, rules);
|
|
842
|
+
const condRoot: Record<string, unknown> = {};
|
|
843
|
+
const condBody: Record<string, unknown> = {};
|
|
844
|
+
const condSubs: [string, Record<string, unknown>][] = [];
|
|
845
|
+
for (const [k, v] of Object.entries(val as Record<string, unknown>)) {
|
|
846
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
847
|
+
if (!k.startsWith("@")) {
|
|
848
|
+
condSubs.push([k, v as Record<string, unknown>]);
|
|
849
|
+
}
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
if (k.startsWith("--")) {
|
|
853
|
+
condRoot[k] = v;
|
|
854
|
+
} else {
|
|
855
|
+
condBody[k] = v;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
emit(":root", toCSSText(condRoot));
|
|
859
|
+
emit("body", toCSSText(condBody));
|
|
860
|
+
for (const [sel, sub] of condSubs) {
|
|
861
|
+
emit(sel, toCSSText(sub));
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Forced-scheme UA hint: native widgets follow the forced attribute, not only the OS scheme.
|
|
868
|
+
if (
|
|
869
|
+
Object.values(mediaQueries).some((q) => pureSchemeOf(q) !== null) &&
|
|
870
|
+
!(projectStyle && typeof projectStyle === "object" && "colorScheme" in projectStyle)
|
|
871
|
+
) {
|
|
872
|
+
rules.push(
|
|
873
|
+
":root { color-scheme: light dark }",
|
|
874
|
+
`:root:where([${COLOR_SCHEME_ATTR}="light"]) { color-scheme: light }`,
|
|
875
|
+
`:root:where([${COLOR_SCHEME_ATTR}="dark"]) { color-scheme: dark }`,
|
|
876
|
+
);
|
|
711
877
|
}
|
|
712
878
|
|
|
713
879
|
const counter = { n: 0 };
|
|
@@ -741,32 +907,7 @@ function emitNestedElement(
|
|
|
741
907
|
continue;
|
|
742
908
|
}
|
|
743
909
|
if (key.startsWith("@")) {
|
|
744
|
-
|
|
745
|
-
? `@media ${mediaQueries[key.slice(1)] ?? key.slice(1)}`
|
|
746
|
-
: key.startsWith("@(")
|
|
747
|
-
? `@media ${key.slice(1)}`
|
|
748
|
-
: key;
|
|
749
|
-
const mProps = toCSSText(val);
|
|
750
|
-
if (mProps) {
|
|
751
|
-
rules.push(`${atRule} { ${selector} { ${mProps} } }`);
|
|
752
|
-
}
|
|
753
|
-
for (const [sel, sub] of Object.entries(val as Record<string, unknown>)) {
|
|
754
|
-
if (sub === null || typeof sub !== "object" || Array.isArray(sub)) {
|
|
755
|
-
continue;
|
|
756
|
-
}
|
|
757
|
-
if (sel.startsWith("@")) {
|
|
758
|
-
continue;
|
|
759
|
-
}
|
|
760
|
-
const resolved = sel.startsWith("&")
|
|
761
|
-
? sel.replace("&", selector)
|
|
762
|
-
: sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
|
|
763
|
-
? `${selector}${sel}`
|
|
764
|
-
: `${selector} ${sel}`;
|
|
765
|
-
const subProps = toCSSText(sub);
|
|
766
|
-
if (subProps) {
|
|
767
|
-
rules.push(`${atRule} { ${resolved} { ${subProps} } }`);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
910
|
+
pushConditionalRule(rules, key, mediaQueries, selector, val as Record<string, unknown>);
|
|
770
911
|
continue;
|
|
771
912
|
}
|
|
772
913
|
const resolved = key.startsWith("&")
|
|
@@ -838,35 +979,7 @@ export function collectStyles(
|
|
|
838
979
|
continue;
|
|
839
980
|
}
|
|
840
981
|
if (prop.startsWith("@")) {
|
|
841
|
-
|
|
842
|
-
? `@media ${mediaQueries[prop.slice(1)] ?? prop.slice(1)}`
|
|
843
|
-
: prop.startsWith("@(")
|
|
844
|
-
? `@media ${prop.slice(1)}`
|
|
845
|
-
: prop;
|
|
846
|
-
const atBaseProps = toCSSText(val);
|
|
847
|
-
if (atBaseProps) {
|
|
848
|
-
rules.push(`${atRule} { ${selector} { ${atBaseProps} } }`);
|
|
849
|
-
}
|
|
850
|
-
for (const [sel, nestedRules] of Object.entries(
|
|
851
|
-
/** @type {Record<string, unknown>} */ val,
|
|
852
|
-
)) {
|
|
853
|
-
if (
|
|
854
|
-
nestedRules === null ||
|
|
855
|
-
typeof nestedRules !== "object" ||
|
|
856
|
-
Array.isArray(nestedRules)
|
|
857
|
-
) {
|
|
858
|
-
continue;
|
|
859
|
-
}
|
|
860
|
-
if (sel.startsWith("@")) {
|
|
861
|
-
continue;
|
|
862
|
-
}
|
|
863
|
-
const resolved = sel.startsWith("&")
|
|
864
|
-
? sel.replace("&", selector)
|
|
865
|
-
: sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
|
|
866
|
-
? `${selector}${sel}`
|
|
867
|
-
: `${selector} ${sel}`;
|
|
868
|
-
rules.push(`${atRule} { ${resolved} { ${toCSSText(nestedRules)} } }`);
|
|
869
|
-
}
|
|
982
|
+
pushConditionalRule(rules, prop, mediaQueries, selector, val as Record<string, unknown>);
|
|
870
983
|
} else {
|
|
871
984
|
const resolved = prop.startsWith("&")
|
|
872
985
|
? prop.replace("&", selector)
|
|
@@ -1276,12 +1389,7 @@ export function buildComponentCSS(
|
|
|
1276
1389
|
|
|
1277
1390
|
for (const [prop, val] of Object.entries(styleDef)) {
|
|
1278
1391
|
if (prop.startsWith("@")) {
|
|
1279
|
-
|
|
1280
|
-
? `@media ${mediaQueries[prop.slice(1)] ?? prop.slice(1)}`
|
|
1281
|
-
: prop.startsWith("@(")
|
|
1282
|
-
? `@media ${prop.slice(1)}`
|
|
1283
|
-
: prop;
|
|
1284
|
-
rules.push(`${atRule} { ${tagName} { ${toCSSText(val as Record<string, unknown>)} } }`);
|
|
1392
|
+
pushConditionalRule(rules, prop, mediaQueries, tagName, val as Record<string, unknown>);
|
|
1285
1393
|
} else if (
|
|
1286
1394
|
prop.startsWith(":") ||
|
|
1287
1395
|
prop.startsWith(".") ||
|