@jxsuite/compiler 1.1.0 → 1.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/LICENSE +21 -0
- package/dist/cli.js +263 -185
- package/dist/compiler.js +111 -109
- package/package.json +8 -7
- package/src/cli.ts +9 -12
- package/src/compiler.ts +18 -2
- package/src/shared.ts +170 -110
- package/src/site/bundler.ts +187 -0
- package/src/site/prototype-resolver.ts +15 -0
- package/src/site/schema-command.ts +22 -9
- package/src/site/site-build.ts +147 -13
- package/src/site/validate-command.ts +264 -0
- package/src/targets/compile-client.ts +17 -3
- package/src/targets/compile-element.ts +28 -3
- 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.3.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.2.0",
|
|
49
|
+
"@jxsuite/runtime": "^1.3.0",
|
|
50
|
+
"@jxsuite/schema": "^1.3.0",
|
|
51
|
+
"esbuild": "^0.28.1",
|
|
51
52
|
"sharp": "^0.35.3"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
|
-
"@happy-dom/global-registrator": "^20.
|
|
55
|
-
"@jxsuite/connector": "^0.
|
|
56
|
-
"@jxsuite/parser": "^1.
|
|
55
|
+
"@happy-dom/global-registrator": "^20.11.0",
|
|
56
|
+
"@jxsuite/connector": "^0.4.0",
|
|
57
|
+
"@jxsuite/parser": "^1.3.0",
|
|
57
58
|
"@types/mdast": "^4.0.4"
|
|
58
59
|
}
|
|
59
60
|
}
|
package/src/cli.ts
CHANGED
|
@@ -26,7 +26,7 @@ Commands:
|
|
|
26
26
|
dev [root] Start the dev server (requires @jxsuite/server and Bun)
|
|
27
27
|
preview [root] Serve an already-built dist/ directory
|
|
28
28
|
schema [root] Generate project.schema.json + document.schema.json from project.json#/extensions
|
|
29
|
-
validate [root] Validate project.json
|
|
29
|
+
validate [root] Validate the whole project: project.json, documents, classes, and fragments
|
|
30
30
|
db push [root] Sync the data section's tables to their connections (additive-only)
|
|
31
31
|
|
|
32
32
|
Options:
|
|
@@ -161,18 +161,15 @@ Options:
|
|
|
161
161
|
}
|
|
162
162
|
} else if (command === "validate") {
|
|
163
163
|
try {
|
|
164
|
-
const {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
const { formatProjectTreeIssues, validateProjectTree } =
|
|
165
|
+
await import("./site/validate-command.ts");
|
|
166
|
+
const result = await validateProjectTree(projectRoot);
|
|
167
|
+
if (result.valid) {
|
|
168
|
+
console.log(`Project is valid (${result.checked} files checked in ${projectRoot})`);
|
|
168
169
|
} else {
|
|
169
|
-
console.error(`
|
|
170
|
-
for (const
|
|
171
|
-
|
|
172
|
-
instancePath?: string;
|
|
173
|
-
message?: string;
|
|
174
|
-
};
|
|
175
|
-
console.error(` - ${instancePath || "/"}: ${message ?? JSON.stringify(error)}`);
|
|
170
|
+
console.error(`Project is INVALID (${projectRoot}):`);
|
|
171
|
+
for (const line of formatProjectTreeIssues(result)) {
|
|
172
|
+
console.error(line);
|
|
176
173
|
}
|
|
177
174
|
process.exit(1);
|
|
178
175
|
}
|
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,7 +6,15 @@
|
|
|
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";
|
|
11
19
|
import { runStatements } from "@jxsuite/runtime/statements";
|
|
12
20
|
import {
|
|
@@ -15,6 +23,7 @@ import {
|
|
|
15
23
|
isExpressionDef,
|
|
16
24
|
isFunctionDef,
|
|
17
25
|
isMappedArray,
|
|
26
|
+
bodyReturnsValue,
|
|
18
27
|
isNamedFormulaDef,
|
|
19
28
|
isPrototypeDef,
|
|
20
29
|
isRef,
|
|
@@ -36,7 +45,15 @@ import type {
|
|
|
36
45
|
} from "@jxsuite/schema/types";
|
|
37
46
|
|
|
38
47
|
// Re-export runtime utilities used by submodules
|
|
39
|
-
export {
|
|
48
|
+
export {
|
|
49
|
+
COLOR_SCHEME_ATTR,
|
|
50
|
+
COLOR_SCHEME_STORAGE_KEY,
|
|
51
|
+
RESERVED_KEYS,
|
|
52
|
+
camelToKebab,
|
|
53
|
+
pureSchemeOf,
|
|
54
|
+
schemeSelectors,
|
|
55
|
+
toCSSText,
|
|
56
|
+
} from "@jxsuite/runtime";
|
|
40
57
|
export { compileExpression, evaluateExpression, isMutating } from "@jxsuite/runtime/expression";
|
|
41
58
|
export { compileStatements, runStatements } from "@jxsuite/runtime/statements";
|
|
42
59
|
|
|
@@ -63,7 +80,7 @@ export function emitFormulaFn(def: { parameters?: unknown[] }, compiledBody: str
|
|
|
63
80
|
export type { ExpressionNode } from "@jxsuite/runtime/expression";
|
|
64
81
|
|
|
65
82
|
// CDN defaults
|
|
66
|
-
export const DEFAULT_REACTIVITY_SRC = "https://esm.sh/@vue/reactivity@3.5.
|
|
83
|
+
export const DEFAULT_REACTIVITY_SRC = "https://esm.sh/@vue/reactivity@3.5.40";
|
|
67
84
|
export const DEFAULT_LIT_HTML_SRC = "https://esm.sh/lit-html@3.3.0";
|
|
68
85
|
|
|
69
86
|
// ─── Schema keywords & detection ─────────────────────────────────────────────
|
|
@@ -347,7 +364,7 @@ export function buildInitialScope(
|
|
|
347
364
|
const fn = new Function("state", ...names, body) as (
|
|
348
365
|
state: Record<string, unknown>,
|
|
349
366
|
) => unknown;
|
|
350
|
-
if (body
|
|
367
|
+
if (bodyReturnsValue(body)) {
|
|
351
368
|
defineLazyScopeValue(scope, key, () => fn(scope));
|
|
352
369
|
} else {
|
|
353
370
|
setOwnScopeValue(scope, key, fn);
|
|
@@ -642,6 +659,94 @@ export function buildInner(
|
|
|
642
659
|
|
|
643
660
|
// ─── CSS extraction ───────────────────────────────────────────────────────────
|
|
644
661
|
|
|
662
|
+
/**
|
|
663
|
+
* The inline pre-paint script injected into <head> when a project declares a pure color-scheme
|
|
664
|
+
* media query: restores the visitor's persisted forced scheme before first paint (no FOUC).
|
|
665
|
+
*
|
|
666
|
+
* @returns {string}
|
|
667
|
+
* @docs framework/concepts/color-schemes
|
|
668
|
+
*/
|
|
669
|
+
export function colorSchemePrePaintScript(): string {
|
|
670
|
+
const key = JSON.stringify(COLOR_SCHEME_STORAGE_KEY);
|
|
671
|
+
const attr = JSON.stringify(COLOR_SCHEME_ATTR);
|
|
672
|
+
return `(function(){try{var s=localStorage.getItem(${key});if(s==="light"||s==="dark"){document.documentElement.setAttribute(${attr},s)}}catch(e){}})()`;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Resolve an `@`-prefixed style key into an emit function that pushes conditional rules. Pure
|
|
677
|
+
* color-scheme queries dual-emit per the forced-scheme contract (spec §9.5): a media-guarded copy
|
|
678
|
+
* that applies while no scheme is forced plus an unconditional copy under the forced root
|
|
679
|
+
* attribute.
|
|
680
|
+
*
|
|
681
|
+
* @param {string} atKey
|
|
682
|
+
* @param {Record<string, string>} mediaQueries
|
|
683
|
+
* @param {string[]} rules
|
|
684
|
+
* @returns {(selector: string, props: string) => void}
|
|
685
|
+
* @docs framework/concepts/color-schemes
|
|
686
|
+
*/
|
|
687
|
+
function conditionalRuleEmitter(
|
|
688
|
+
atKey: string,
|
|
689
|
+
mediaQueries: Record<string, string>,
|
|
690
|
+
rules: string[],
|
|
691
|
+
): (selector: string, props: string) => void {
|
|
692
|
+
const query = atKey.startsWith("@--")
|
|
693
|
+
? (mediaQueries[atKey.slice(1)] ?? atKey.slice(1))
|
|
694
|
+
: atKey.startsWith("@(")
|
|
695
|
+
? atKey.slice(1)
|
|
696
|
+
: null;
|
|
697
|
+
const atRule = query === null ? atKey : `@media ${query}`;
|
|
698
|
+
const scheme = query === null ? null : pureSchemeOf(query);
|
|
699
|
+
return (selector: string, props: string) => {
|
|
700
|
+
if (!props) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (scheme) {
|
|
704
|
+
const { auto, forced } = schemeSelectors(selector, scheme);
|
|
705
|
+
rules.push(`${atRule} { ${auto} { ${props} } }`, `${forced} { ${props} }`);
|
|
706
|
+
} else {
|
|
707
|
+
rules.push(`${atRule} { ${selector} { ${props} } }`);
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Push the rules for one `@`-prefixed style block scoped to `selector`: flat props plus one level
|
|
714
|
+
* of nested selectors, routed through conditionalRuleEmitter (scheme-aware).
|
|
715
|
+
*
|
|
716
|
+
* @param {string[]} rules
|
|
717
|
+
* @param {string} atKey
|
|
718
|
+
* @param {Record<string, string>} mediaQueries
|
|
719
|
+
* @param {string} selector
|
|
720
|
+
* @param {Record<string, unknown>} obj
|
|
721
|
+
*/
|
|
722
|
+
function pushConditionalRule(
|
|
723
|
+
rules: string[],
|
|
724
|
+
atKey: string,
|
|
725
|
+
mediaQueries: Record<string, string>,
|
|
726
|
+
selector: string,
|
|
727
|
+
obj: Record<string, unknown>,
|
|
728
|
+
) {
|
|
729
|
+
if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const emit = conditionalRuleEmitter(atKey, mediaQueries, rules);
|
|
733
|
+
emit(selector, toCSSText(obj));
|
|
734
|
+
for (const [sel, sub] of Object.entries(obj)) {
|
|
735
|
+
if (sub === null || typeof sub !== "object" || Array.isArray(sub)) {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (sel.startsWith("@")) {
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
const resolved = sel.startsWith("&")
|
|
742
|
+
? sel.replace("&", selector)
|
|
743
|
+
: sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
|
|
744
|
+
? `${selector}${sel}`
|
|
745
|
+
: `${selector} ${sel}`;
|
|
746
|
+
emit(resolved, toCSSText(sub));
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
645
750
|
/**
|
|
646
751
|
* Walk the entire document tree and collect all static nested CSS rules.
|
|
647
752
|
*
|
|
@@ -671,30 +776,7 @@ export function compileStyles(
|
|
|
671
776
|
continue;
|
|
672
777
|
}
|
|
673
778
|
if (key.startsWith("@")) {
|
|
674
|
-
|
|
675
|
-
? `@media ${mediaQueries[key.slice(1)] ?? key.slice(1)}`
|
|
676
|
-
: key;
|
|
677
|
-
const mProps = toCSSText(val);
|
|
678
|
-
if (mProps) {
|
|
679
|
-
rules.push(`${atRule} { ${selector} { ${mProps} } }`);
|
|
680
|
-
}
|
|
681
|
-
for (const [sel, sub] of Object.entries(val as Record<string, unknown>)) {
|
|
682
|
-
if (sub === null || typeof sub !== "object" || Array.isArray(sub)) {
|
|
683
|
-
continue;
|
|
684
|
-
}
|
|
685
|
-
if (sel.startsWith("@")) {
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
const resolved = sel.startsWith("&")
|
|
689
|
-
? sel.replace("&", selector)
|
|
690
|
-
: sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
|
|
691
|
-
? `${selector}${sel}`
|
|
692
|
-
: `${selector} ${sel}`;
|
|
693
|
-
const subProps = toCSSText(sub);
|
|
694
|
-
if (subProps) {
|
|
695
|
-
rules.push(`${atRule} { ${resolved} { ${subProps} } }`);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
779
|
+
pushConditionalRule(rules, key, mediaQueries, selector, val as Record<string, unknown>);
|
|
698
780
|
continue;
|
|
699
781
|
}
|
|
700
782
|
const resolved = key.startsWith("&")
|
|
@@ -706,27 +788,6 @@ export function compileStyles(
|
|
|
706
788
|
}
|
|
707
789
|
};
|
|
708
790
|
|
|
709
|
-
for (const [key, val] of Object.entries(projectStyle)) {
|
|
710
|
-
if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[")) {
|
|
711
|
-
emitProjectRules(key, val as Record<string, unknown>);
|
|
712
|
-
} else if (
|
|
713
|
-
val !== null &&
|
|
714
|
-
typeof val === "object" &&
|
|
715
|
-
!Array.isArray(val) &&
|
|
716
|
-
!key.startsWith("@") &&
|
|
717
|
-
!key.startsWith("--")
|
|
718
|
-
) {
|
|
719
|
-
emitProjectRules(key, val as Record<string, unknown>);
|
|
720
|
-
} else if (key.startsWith("@")) {
|
|
721
|
-
// @media block at top level
|
|
722
|
-
const atRule = key.startsWith("@--")
|
|
723
|
-
? `@media ${mediaQueries[key.slice(1)] ?? key.slice(1)}`
|
|
724
|
-
: key.startsWith("@(")
|
|
725
|
-
? `@media ${key.slice(1)}`
|
|
726
|
-
: key;
|
|
727
|
-
rules.push(`${atRule} { body { ${toCSSText(val as Record<string, unknown>)} } }`);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
791
|
// Collect CSS custom properties into :root {}
|
|
731
792
|
const rootProps: Record<string, unknown> = {};
|
|
732
793
|
// Collect direct CSS properties into body {}
|
|
@@ -749,6 +810,7 @@ export function compileStyles(
|
|
|
749
810
|
bodyProps[key] = val;
|
|
750
811
|
}
|
|
751
812
|
}
|
|
813
|
+
// Base rules precede conditional blocks so equal-specificity overrides win by source order.
|
|
752
814
|
const rootCSS = toCSSText(rootProps);
|
|
753
815
|
if (rootCSS) {
|
|
754
816
|
rules.push(`:root { ${rootCSS} }`);
|
|
@@ -757,6 +819,62 @@ export function compileStyles(
|
|
|
757
819
|
if (bodyCSS) {
|
|
758
820
|
rules.push(`body { ${bodyCSS} }`);
|
|
759
821
|
}
|
|
822
|
+
|
|
823
|
+
for (const [key, val] of Object.entries(projectStyle)) {
|
|
824
|
+
if (key.startsWith(":") || key.startsWith(".") || key.startsWith("[")) {
|
|
825
|
+
emitProjectRules(key, val as Record<string, unknown>);
|
|
826
|
+
} else if (
|
|
827
|
+
val !== null &&
|
|
828
|
+
typeof val === "object" &&
|
|
829
|
+
!Array.isArray(val) &&
|
|
830
|
+
!key.startsWith("@") &&
|
|
831
|
+
!key.startsWith("--")
|
|
832
|
+
) {
|
|
833
|
+
emitProjectRules(key, val as Record<string, unknown>);
|
|
834
|
+
} else if (
|
|
835
|
+
key.startsWith("@") &&
|
|
836
|
+
val !== null &&
|
|
837
|
+
typeof val === "object" &&
|
|
838
|
+
!Array.isArray(val)
|
|
839
|
+
) {
|
|
840
|
+
// Conditional block at project top level: custom properties override :root, direct
|
|
841
|
+
// Properties override body, selector-keyed sub-objects their own selector.
|
|
842
|
+
const emit = conditionalRuleEmitter(key, mediaQueries, rules);
|
|
843
|
+
const condRoot: Record<string, unknown> = {};
|
|
844
|
+
const condBody: Record<string, unknown> = {};
|
|
845
|
+
const condSubs: [string, Record<string, unknown>][] = [];
|
|
846
|
+
for (const [k, v] of Object.entries(val as Record<string, unknown>)) {
|
|
847
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
848
|
+
if (!k.startsWith("@")) {
|
|
849
|
+
condSubs.push([k, v as Record<string, unknown>]);
|
|
850
|
+
}
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
if (k.startsWith("--")) {
|
|
854
|
+
condRoot[k] = v;
|
|
855
|
+
} else {
|
|
856
|
+
condBody[k] = v;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
emit(":root", toCSSText(condRoot));
|
|
860
|
+
emit("body", toCSSText(condBody));
|
|
861
|
+
for (const [sel, sub] of condSubs) {
|
|
862
|
+
emit(sel, toCSSText(sub));
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Forced-scheme UA hint: native widgets follow the forced attribute, not only the OS scheme.
|
|
869
|
+
if (
|
|
870
|
+
Object.values(mediaQueries).some((q) => pureSchemeOf(q) !== null) &&
|
|
871
|
+
!(projectStyle && typeof projectStyle === "object" && "colorScheme" in projectStyle)
|
|
872
|
+
) {
|
|
873
|
+
rules.push(
|
|
874
|
+
":root { color-scheme: light dark }",
|
|
875
|
+
`:root:where([${COLOR_SCHEME_ATTR}="light"]) { color-scheme: light }`,
|
|
876
|
+
`:root:where([${COLOR_SCHEME_ATTR}="dark"]) { color-scheme: dark }`,
|
|
877
|
+
);
|
|
760
878
|
}
|
|
761
879
|
|
|
762
880
|
const counter = { n: 0 };
|
|
@@ -790,32 +908,7 @@ function emitNestedElement(
|
|
|
790
908
|
continue;
|
|
791
909
|
}
|
|
792
910
|
if (key.startsWith("@")) {
|
|
793
|
-
|
|
794
|
-
? `@media ${mediaQueries[key.slice(1)] ?? key.slice(1)}`
|
|
795
|
-
: key.startsWith("@(")
|
|
796
|
-
? `@media ${key.slice(1)}`
|
|
797
|
-
: key;
|
|
798
|
-
const mProps = toCSSText(val);
|
|
799
|
-
if (mProps) {
|
|
800
|
-
rules.push(`${atRule} { ${selector} { ${mProps} } }`);
|
|
801
|
-
}
|
|
802
|
-
for (const [sel, sub] of Object.entries(val as Record<string, unknown>)) {
|
|
803
|
-
if (sub === null || typeof sub !== "object" || Array.isArray(sub)) {
|
|
804
|
-
continue;
|
|
805
|
-
}
|
|
806
|
-
if (sel.startsWith("@")) {
|
|
807
|
-
continue;
|
|
808
|
-
}
|
|
809
|
-
const resolved = sel.startsWith("&")
|
|
810
|
-
? sel.replace("&", selector)
|
|
811
|
-
: sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
|
|
812
|
-
? `${selector}${sel}`
|
|
813
|
-
: `${selector} ${sel}`;
|
|
814
|
-
const subProps = toCSSText(sub);
|
|
815
|
-
if (subProps) {
|
|
816
|
-
rules.push(`${atRule} { ${resolved} { ${subProps} } }`);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
911
|
+
pushConditionalRule(rules, key, mediaQueries, selector, val as Record<string, unknown>);
|
|
819
912
|
continue;
|
|
820
913
|
}
|
|
821
914
|
const resolved = key.startsWith("&")
|
|
@@ -887,35 +980,7 @@ export function collectStyles(
|
|
|
887
980
|
continue;
|
|
888
981
|
}
|
|
889
982
|
if (prop.startsWith("@")) {
|
|
890
|
-
|
|
891
|
-
? `@media ${mediaQueries[prop.slice(1)] ?? prop.slice(1)}`
|
|
892
|
-
: prop.startsWith("@(")
|
|
893
|
-
? `@media ${prop.slice(1)}`
|
|
894
|
-
: prop;
|
|
895
|
-
const atBaseProps = toCSSText(val);
|
|
896
|
-
if (atBaseProps) {
|
|
897
|
-
rules.push(`${atRule} { ${selector} { ${atBaseProps} } }`);
|
|
898
|
-
}
|
|
899
|
-
for (const [sel, nestedRules] of Object.entries(
|
|
900
|
-
/** @type {Record<string, unknown>} */ val,
|
|
901
|
-
)) {
|
|
902
|
-
if (
|
|
903
|
-
nestedRules === null ||
|
|
904
|
-
typeof nestedRules !== "object" ||
|
|
905
|
-
Array.isArray(nestedRules)
|
|
906
|
-
) {
|
|
907
|
-
continue;
|
|
908
|
-
}
|
|
909
|
-
if (sel.startsWith("@")) {
|
|
910
|
-
continue;
|
|
911
|
-
}
|
|
912
|
-
const resolved = sel.startsWith("&")
|
|
913
|
-
? sel.replace("&", selector)
|
|
914
|
-
: sel.startsWith(":") || sel.startsWith(".") || sel.startsWith("[")
|
|
915
|
-
? `${selector}${sel}`
|
|
916
|
-
: `${selector} ${sel}`;
|
|
917
|
-
rules.push(`${atRule} { ${resolved} { ${toCSSText(nestedRules)} } }`);
|
|
918
|
-
}
|
|
983
|
+
pushConditionalRule(rules, prop, mediaQueries, selector, val as Record<string, unknown>);
|
|
919
984
|
} else {
|
|
920
985
|
const resolved = prop.startsWith("&")
|
|
921
986
|
? prop.replace("&", selector)
|
|
@@ -1325,12 +1390,7 @@ export function buildComponentCSS(
|
|
|
1325
1390
|
|
|
1326
1391
|
for (const [prop, val] of Object.entries(styleDef)) {
|
|
1327
1392
|
if (prop.startsWith("@")) {
|
|
1328
|
-
|
|
1329
|
-
? `@media ${mediaQueries[prop.slice(1)] ?? prop.slice(1)}`
|
|
1330
|
-
: prop.startsWith("@(")
|
|
1331
|
-
? `@media ${prop.slice(1)}`
|
|
1332
|
-
: prop;
|
|
1333
|
-
rules.push(`${atRule} { ${tagName} { ${toCSSText(val as Record<string, unknown>)} } }`);
|
|
1393
|
+
pushConditionalRule(rules, prop, mediaQueries, tagName, val as Record<string, unknown>);
|
|
1334
1394
|
} else if (
|
|
1335
1395
|
prop.startsWith(":") ||
|
|
1336
1396
|
prop.startsWith(".") ||
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundler.js — timing-aware module bundling for compiled sites
|
|
3
|
+
*
|
|
4
|
+
* One backend abstraction serving every bundling need of the build (spec.md §12, compiler.md):
|
|
5
|
+
* client-timing `$src` sidecars today, the server worker in a follow-up. Backend selection is
|
|
6
|
+
* runtime-adaptive: `Bun.build` when the build runs under Bun, esbuild (dynamically imported) under
|
|
7
|
+
* plain Node. Options are kept minimal and identical across backends so output stays as stable as
|
|
8
|
+
* possible; set `JX_BUNDLER=esbuild` to force the fallback (e.g. for parity checks).
|
|
9
|
+
*
|
|
10
|
+
* `timing: "compiler"` code is never bundled — it executes in the build host via the extension
|
|
11
|
+
* registry's `importImplementation` path.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { rmSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
import { join, resolve } from "node:path";
|
|
17
|
+
import { isNpmSpecifier, NPM_SPECIFIER_PREFIX } from "@jxsuite/schema/asset-paths";
|
|
18
|
+
|
|
19
|
+
/** Import specifiers left external in client bundles — provided by the page importmap. */
|
|
20
|
+
export const CLIENT_EXTERNALS = ["@vue/reactivity", "lit-html"];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* True when a Function-def `$src` specifier is bundled by the build: `npm:` package specifiers and
|
|
24
|
+
* project-relative files. Absolute URL paths (`/lib/x.js`, `https://…`) are served as-is.
|
|
25
|
+
*/
|
|
26
|
+
export function isBundleableSrc(specifier: string): boolean {
|
|
27
|
+
return isNpmSpecifier(specifier) || specifier.startsWith("./") || specifier.startsWith("../");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* True when the Node code path (createRequire resolution, esbuild backend) should be used: always
|
|
32
|
+
* under plain Node, or when `JX_BUNDLER=esbuild` forces it for parity checks.
|
|
33
|
+
*/
|
|
34
|
+
function useNodeFallback(): boolean {
|
|
35
|
+
return typeof Bun === "undefined" || process.env.JX_BUNDLER === "esbuild";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve a bundleable `$src` specifier to the absolute entry file the bundler starts from. `npm:`
|
|
40
|
+
* specifiers resolve from the project root (project node_modules first, then up-tree); relative
|
|
41
|
+
* specifiers resolve against the directory of the document that declared them.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveSidecarEntry(
|
|
44
|
+
specifier: string,
|
|
45
|
+
docDir: string,
|
|
46
|
+
projectRoot: string,
|
|
47
|
+
): string {
|
|
48
|
+
if (isNpmSpecifier(specifier)) {
|
|
49
|
+
const bare = specifier.slice(NPM_SPECIFIER_PREFIX.length);
|
|
50
|
+
if (useNodeFallback()) {
|
|
51
|
+
return createRequire(join(projectRoot, "package.json")).resolve(bare);
|
|
52
|
+
}
|
|
53
|
+
return Bun.resolveSync(bare, projectRoot);
|
|
54
|
+
}
|
|
55
|
+
return resolve(docDir, specifier);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface BundleRequest {
|
|
59
|
+
/** Absolute path of the module to bundle. */
|
|
60
|
+
entryPath: string;
|
|
61
|
+
/** Absolute path of the bundled output file. */
|
|
62
|
+
outfile: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Resolution/runtime targets: client sidecars use "browser"; the site worker uses the rest. */
|
|
66
|
+
export type BundleTargetName = "browser" | "node" | "bun" | "workerd";
|
|
67
|
+
|
|
68
|
+
export interface BundleOptions {
|
|
69
|
+
target: BundleTargetName;
|
|
70
|
+
/** Bare specifiers left unresolved in the output. */
|
|
71
|
+
external?: string[];
|
|
72
|
+
/** Package.json export conditions for resolution (e.g. ["workerd", "worker"]). */
|
|
73
|
+
conditions?: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Per-adapter bundle options for the generated site worker (WinterTC runtimes). Cloudflare resolves
|
|
78
|
+
* with workerd conditions and keeps `cloudflare:*`/`node:*` imports external (nodejs_compat
|
|
79
|
+
* provides node builtins at runtime); node/bun bundle platform-native with builtins external by
|
|
80
|
+
* construction.
|
|
81
|
+
*/
|
|
82
|
+
export function workerBundleOptions(adapter: string): BundleOptions {
|
|
83
|
+
if (adapter === "cloudflare-workers" || adapter === "cloudflare-pages") {
|
|
84
|
+
return {
|
|
85
|
+
conditions: ["workerd", "worker"],
|
|
86
|
+
external: ["cloudflare:*", "node:*"],
|
|
87
|
+
target: "workerd",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return { target: adapter === "bun" ? "bun" : "node" };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Map a bundle target to esbuild's platform (conditions carry the workerd specifics). */
|
|
94
|
+
function esbuildPlatform(target: BundleTargetName): "browser" | "node" {
|
|
95
|
+
return target === "node" || target === "bun" ? "node" : "browser";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Map a bundle target to Bun.build's target. */
|
|
99
|
+
function bunTarget(target: BundleTargetName): "browser" | "node" | "bun" {
|
|
100
|
+
if (target === "workerd") {
|
|
101
|
+
return "browser";
|
|
102
|
+
}
|
|
103
|
+
return target;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Bundle one entry module into a single self-contained ESM file. Throws with the backend's
|
|
108
|
+
* diagnostics on failure.
|
|
109
|
+
*/
|
|
110
|
+
export async function bundleEntry(request: BundleRequest, opts: BundleOptions): Promise<void> {
|
|
111
|
+
const external = opts.external ?? [];
|
|
112
|
+
|
|
113
|
+
if (useNodeFallback()) {
|
|
114
|
+
// Dynamic import keeps esbuild (a native-binary dependency) out of Bun-run builds entirely.
|
|
115
|
+
const esbuild = await import("esbuild");
|
|
116
|
+
await esbuild.build({
|
|
117
|
+
bundle: true,
|
|
118
|
+
...(opts.conditions ? { conditions: opts.conditions } : {}),
|
|
119
|
+
entryPoints: [request.entryPath],
|
|
120
|
+
external,
|
|
121
|
+
format: "esm",
|
|
122
|
+
logLevel: "silent",
|
|
123
|
+
minify: false,
|
|
124
|
+
outfile: request.outfile,
|
|
125
|
+
platform: esbuildPlatform(opts.target),
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const result = await Bun.build({
|
|
131
|
+
...(opts.conditions ? { conditions: opts.conditions } : {}),
|
|
132
|
+
entrypoints: [request.entryPath],
|
|
133
|
+
external,
|
|
134
|
+
format: "esm",
|
|
135
|
+
minify: false,
|
|
136
|
+
target: bunTarget(opts.target),
|
|
137
|
+
throw: false,
|
|
138
|
+
});
|
|
139
|
+
if (!result.success || result.outputs.length === 0) {
|
|
140
|
+
const logs = result.logs.map(String).join("\n");
|
|
141
|
+
throw new Error(`Bun.build failed for ${request.entryPath}:\n${logs}`);
|
|
142
|
+
}
|
|
143
|
+
writeFileSync(request.outfile, await result.outputs[0]!.text(), "utf8");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Bundle generated worker source (a string, not a file) into a self-contained module for the
|
|
148
|
+
* adapter's runtime. Bare and relative specifiers resolve from the project root — exactly how the
|
|
149
|
+
* unbundled source would have resolved when served from within the project.
|
|
150
|
+
*/
|
|
151
|
+
export async function bundleWorkerSource(
|
|
152
|
+
source: string,
|
|
153
|
+
{ projectRoot, outfile, adapter }: { projectRoot: string; outfile: string; adapter: string },
|
|
154
|
+
): Promise<void> {
|
|
155
|
+
const opts = workerBundleOptions(adapter);
|
|
156
|
+
|
|
157
|
+
if (useNodeFallback()) {
|
|
158
|
+
const esbuild = await import("esbuild");
|
|
159
|
+
await esbuild.build({
|
|
160
|
+
bundle: true,
|
|
161
|
+
...(opts.conditions ? { conditions: opts.conditions } : {}),
|
|
162
|
+
external: opts.external ?? [],
|
|
163
|
+
format: "esm",
|
|
164
|
+
logLevel: "silent",
|
|
165
|
+
minify: false,
|
|
166
|
+
outfile,
|
|
167
|
+
platform: esbuildPlatform(opts.target),
|
|
168
|
+
stdin: {
|
|
169
|
+
contents: source,
|
|
170
|
+
loader: "js",
|
|
171
|
+
resolveDir: projectRoot,
|
|
172
|
+
sourcefile: "jx-worker-entry.js",
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Bun.build has no stdin input — write a transient entry at the project root so resolution
|
|
179
|
+
// Matches the stdin/resolveDir behavior, and always remove it.
|
|
180
|
+
const entryPath = join(projectRoot, ".jx-worker-entry.mjs");
|
|
181
|
+
writeFileSync(entryPath, source, "utf8");
|
|
182
|
+
try {
|
|
183
|
+
await bundleEntry({ entryPath, outfile }, opts);
|
|
184
|
+
} finally {
|
|
185
|
+
rmSync(entryPath, { force: true });
|
|
186
|
+
}
|
|
187
|
+
}
|