@jxsuite/compiler 1.1.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 +255 -186
- package/dist/compiler.js +111 -109
- package/package.json +7 -6
- package/src/compiler.ts +18 -2
- package/src/shared.ts +167 -108
- package/src/site/bundler.ts +187 -0
- package/src/site/prototype-resolver.ts +15 -0
- package/src/site/site-build.ts +147 -13
- 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.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.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.3.
|
|
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/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 {
|
|
@@ -36,7 +44,15 @@ import type {
|
|
|
36
44
|
} from "@jxsuite/schema/types";
|
|
37
45
|
|
|
38
46
|
// Re-export runtime utilities used by submodules
|
|
39
|
-
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";
|
|
40
56
|
export { compileExpression, evaluateExpression, isMutating } from "@jxsuite/runtime/expression";
|
|
41
57
|
export { compileStatements, runStatements } from "@jxsuite/runtime/statements";
|
|
42
58
|
|
|
@@ -642,6 +658,94 @@ export function buildInner(
|
|
|
642
658
|
|
|
643
659
|
// ─── CSS extraction ───────────────────────────────────────────────────────────
|
|
644
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
|
+
|
|
645
749
|
/**
|
|
646
750
|
* Walk the entire document tree and collect all static nested CSS rules.
|
|
647
751
|
*
|
|
@@ -671,30 +775,7 @@ export function compileStyles(
|
|
|
671
775
|
continue;
|
|
672
776
|
}
|
|
673
777
|
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
|
-
}
|
|
778
|
+
pushConditionalRule(rules, key, mediaQueries, selector, val as Record<string, unknown>);
|
|
698
779
|
continue;
|
|
699
780
|
}
|
|
700
781
|
const resolved = key.startsWith("&")
|
|
@@ -706,27 +787,6 @@ export function compileStyles(
|
|
|
706
787
|
}
|
|
707
788
|
};
|
|
708
789
|
|
|
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
790
|
// Collect CSS custom properties into :root {}
|
|
731
791
|
const rootProps: Record<string, unknown> = {};
|
|
732
792
|
// Collect direct CSS properties into body {}
|
|
@@ -749,6 +809,7 @@ export function compileStyles(
|
|
|
749
809
|
bodyProps[key] = val;
|
|
750
810
|
}
|
|
751
811
|
}
|
|
812
|
+
// Base rules precede conditional blocks so equal-specificity overrides win by source order.
|
|
752
813
|
const rootCSS = toCSSText(rootProps);
|
|
753
814
|
if (rootCSS) {
|
|
754
815
|
rules.push(`:root { ${rootCSS} }`);
|
|
@@ -757,6 +818,62 @@ export function compileStyles(
|
|
|
757
818
|
if (bodyCSS) {
|
|
758
819
|
rules.push(`body { ${bodyCSS} }`);
|
|
759
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
|
+
);
|
|
760
877
|
}
|
|
761
878
|
|
|
762
879
|
const counter = { n: 0 };
|
|
@@ -790,32 +907,7 @@ function emitNestedElement(
|
|
|
790
907
|
continue;
|
|
791
908
|
}
|
|
792
909
|
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
|
-
}
|
|
910
|
+
pushConditionalRule(rules, key, mediaQueries, selector, val as Record<string, unknown>);
|
|
819
911
|
continue;
|
|
820
912
|
}
|
|
821
913
|
const resolved = key.startsWith("&")
|
|
@@ -887,35 +979,7 @@ export function collectStyles(
|
|
|
887
979
|
continue;
|
|
888
980
|
}
|
|
889
981
|
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
|
-
}
|
|
982
|
+
pushConditionalRule(rules, prop, mediaQueries, selector, val as Record<string, unknown>);
|
|
919
983
|
} else {
|
|
920
984
|
const resolved = prop.startsWith("&")
|
|
921
985
|
? prop.replace("&", selector)
|
|
@@ -1325,12 +1389,7 @@ export function buildComponentCSS(
|
|
|
1325
1389
|
|
|
1326
1390
|
for (const [prop, val] of Object.entries(styleDef)) {
|
|
1327
1391
|
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>)} } }`);
|
|
1392
|
+
pushConditionalRule(rules, prop, mediaQueries, tagName, val as Record<string, unknown>);
|
|
1334
1393
|
} else if (
|
|
1335
1394
|
prop.startsWith(":") ||
|
|
1336
1395
|
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
|
+
}
|
|
@@ -79,6 +79,7 @@ export async function resolvePrototypes(
|
|
|
79
79
|
config?: Record<string, unknown>;
|
|
80
80
|
sections?: Record<string, unknown>;
|
|
81
81
|
registry?: ExtensionRegistry;
|
|
82
|
+
registerBundle?: (specifier: string) => void;
|
|
82
83
|
} = {},
|
|
83
84
|
) {
|
|
84
85
|
const imports = doc.imports ?? {};
|
|
@@ -107,6 +108,20 @@ export async function resolvePrototypes(
|
|
|
107
108
|
root: projectRoot,
|
|
108
109
|
route,
|
|
109
110
|
});
|
|
111
|
+
// A lowered def may name client modules it depends on via `$bundle`
|
|
112
|
+
// (extensions.md §8.3): the site build bundles each specifier for the browser.
|
|
113
|
+
// The key is host metadata, not part of the core def shape — strip it.
|
|
114
|
+
if (lowered && typeof lowered === "object" && !Array.isArray(lowered)) {
|
|
115
|
+
const bundles = (lowered as JsonObject).$bundle;
|
|
116
|
+
if (Array.isArray(bundles)) {
|
|
117
|
+
for (const specifier of bundles) {
|
|
118
|
+
if (typeof specifier === "string") {
|
|
119
|
+
projectContext.registerBundle?.(specifier);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
delete (lowered as JsonObject).$bundle;
|
|
124
|
+
}
|
|
110
125
|
state[key] = lowered as JxStateDefinition;
|
|
111
126
|
} catch (error) {
|
|
112
127
|
console.warn(
|