@jxsuite/runtime 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/README.md +7 -7
- package/dist/runtime.js +71 -14
- package/dist/runtime.js.map +4 -4
- package/package.json +4 -4
- package/src/runtime.ts +122 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/runtime",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "JSON-native reactive web component runtime",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"upgrade": "bunx npm-check-updates -u && bun install"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@jxsuite/schema": "^1.
|
|
34
|
-
"@vue/reactivity": "
|
|
33
|
+
"@jxsuite/schema": "^1.3.0",
|
|
34
|
+
"@vue/reactivity": "3.5.40"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@happy-dom/global-registrator": "^20.
|
|
37
|
+
"@happy-dom/global-registrator": "^20.11.0"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { evaluateExpression, isMutating } from "./expression.ts";
|
|
27
27
|
import type { DynamicClass, JxEventHandler, JxPath, JxRenderOptions, JxScope } from "./types.ts";
|
|
28
28
|
import {
|
|
29
|
+
bodyReturnsValue,
|
|
29
30
|
hasStructuredBody,
|
|
30
31
|
isExpressionDef,
|
|
31
32
|
isFunctionDef,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
isPrototypeDef,
|
|
36
37
|
isRef as isRefValue,
|
|
37
38
|
isServerFnDef,
|
|
39
|
+
isTemplateString,
|
|
38
40
|
paramNames,
|
|
39
41
|
} from "@jxsuite/schema/guards";
|
|
40
42
|
import { runStatements } from "./statements.ts";
|
|
@@ -68,6 +70,29 @@ import type { Ref } from "@vue/reactivity";
|
|
|
68
70
|
* @returns {Promise<JxScope>} Resolves with the live component scope (state reactive
|
|
69
71
|
* proxy)
|
|
70
72
|
*/
|
|
73
|
+
/** The document-schema version this runtime understands (the `/schema/vN` in a hosted `$schema`). */
|
|
74
|
+
export const SUPPORTED_SCHEMA_VERSION = 1;
|
|
75
|
+
let _schemaVersionWarned = false;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Warn once when a document declares a hosted `$schema` whose version differs from the one this
|
|
79
|
+
* runtime supports. A relative or non-hosted `$schema` (local dev schema paths) is ignored. This is
|
|
80
|
+
* a diagnostic only — there is no document-format migration story yet (see spec §3.2 / §21.4).
|
|
81
|
+
*/
|
|
82
|
+
function checkSchemaVersion($schema: unknown): void {
|
|
83
|
+
if (_schemaVersionWarned || typeof $schema !== "string") {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const match = $schema.match(/\/schema\/v(\d+)\b/);
|
|
87
|
+
if (match && Number(match[1]) !== SUPPORTED_SCHEMA_VERSION) {
|
|
88
|
+
_schemaVersionWarned = true;
|
|
89
|
+
console.warn(
|
|
90
|
+
`Jx: document $schema is version v${match[1]} but this runtime supports ` +
|
|
91
|
+
`v${SUPPORTED_SCHEMA_VERSION}. Behavior may be undefined; there is no format migrator yet.`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
71
96
|
export async function Jx(
|
|
72
97
|
source: string | JxDocument,
|
|
73
98
|
target: HTMLElement = document.body,
|
|
@@ -75,6 +100,7 @@ export async function Jx(
|
|
|
75
100
|
) {
|
|
76
101
|
const base = typeof source === "string" ? new URL(source, location.href).href : location.href;
|
|
77
102
|
const doc = await resolve(source);
|
|
103
|
+
checkSchemaVersion(doc.$schema);
|
|
78
104
|
|
|
79
105
|
// Register custom elements declared in $elements (depth-first)
|
|
80
106
|
if (doc.$elements) {
|
|
@@ -615,9 +641,9 @@ async function resolveFunction(def: JxFunctionDef, state: JxScope, key: string,
|
|
|
615
641
|
let isComputed = false;
|
|
616
642
|
if (!hasParams) {
|
|
617
643
|
if (typeof def.body === "string") {
|
|
618
|
-
isComputed =
|
|
644
|
+
isComputed = bodyReturnsValue(def.body);
|
|
619
645
|
} else if (fn) {
|
|
620
|
-
isComputed = fn.length <= 1 &&
|
|
646
|
+
isComputed = fn.length <= 1 && bodyReturnsValue(fn.toString());
|
|
621
647
|
}
|
|
622
648
|
}
|
|
623
649
|
if (isComputed) {
|
|
@@ -690,6 +716,9 @@ export const RESERVED_KEYS = new Set([
|
|
|
690
716
|
* @param {JxRenderOptions} [options]
|
|
691
717
|
* @returns {HTMLElement | Text}
|
|
692
718
|
*/
|
|
719
|
+
/** Refs already warned about, so the §13 diagnostic fires once per distinct target. */
|
|
720
|
+
const warnedRefChildren = new Set<string>();
|
|
721
|
+
|
|
693
722
|
export function renderNode(
|
|
694
723
|
def: JxElement | string | number | boolean,
|
|
695
724
|
state: JxScope,
|
|
@@ -719,6 +748,22 @@ export function renderNode(
|
|
|
719
748
|
}
|
|
720
749
|
}
|
|
721
750
|
|
|
751
|
+
/*
|
|
752
|
+
* §13 diagnostic: a node-level external `$ref` (the withdrawn component-instance syntax) has no
|
|
753
|
+
* tagName and silently renders an empty <div>. Warn once per target instead of failing silently —
|
|
754
|
+
* the supported mechanism is $elements + a custom-element tag (see spec §13).
|
|
755
|
+
*/
|
|
756
|
+
if ("$ref" in def && !def.tagName) {
|
|
757
|
+
const refTarget = String((def as { $ref?: unknown }).$ref ?? "");
|
|
758
|
+
if (!warnedRefChildren.has(refTarget)) {
|
|
759
|
+
warnedRefChildren.add(refTarget);
|
|
760
|
+
console.warn(
|
|
761
|
+
`Jx: a $ref child ("${refTarget}") is not a supported component instance; register the ` +
|
|
762
|
+
`component in $elements and use its custom-element tag instead (spec §13).`,
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
722
767
|
// Custom element with $props: set JS properties on the element instance
|
|
723
768
|
const tagName = def.tagName ?? "div";
|
|
724
769
|
const isCustomEl = tagName.includes("-") && customElements.get(tagName);
|
|
@@ -780,10 +825,6 @@ export function renderNode(
|
|
|
780
825
|
* @param {unknown} val
|
|
781
826
|
* @returns {boolean}
|
|
782
827
|
*/
|
|
783
|
-
function isTemplateString(val: unknown): val is string {
|
|
784
|
-
return typeof val === "string" && val.includes("${");
|
|
785
|
-
}
|
|
786
|
-
|
|
787
828
|
// ─── Property / style / attribute application ─────────────────────────────────
|
|
788
829
|
|
|
789
830
|
/**
|
|
@@ -1030,14 +1071,25 @@ export function applyStyle(
|
|
|
1030
1071
|
if (key === "@--") {
|
|
1031
1072
|
continue;
|
|
1032
1073
|
} // Base canvas width, not a real media query
|
|
1033
|
-
const
|
|
1034
|
-
?
|
|
1074
|
+
const query = key.startsWith("@--")
|
|
1075
|
+
? (mediaQueries[key.slice(1)] ?? key.slice(1))
|
|
1035
1076
|
: key.startsWith("@(")
|
|
1036
|
-
?
|
|
1037
|
-
:
|
|
1077
|
+
? key.slice(1)
|
|
1078
|
+
: null;
|
|
1079
|
+
const atRule = query === null ? key : `@media ${query}`;
|
|
1038
1080
|
const scope = `[data-jx="${uid}"]`;
|
|
1039
|
-
|
|
1040
|
-
|
|
1081
|
+
const scheme = query === null ? null : pureSchemeOf(query);
|
|
1082
|
+
if (scheme) {
|
|
1083
|
+
// Dual-emit scheme rules: a media-guarded copy for auto plus a forced-attribute copy.
|
|
1084
|
+
// The forced root attribute wins over the OS preference.
|
|
1085
|
+
const { auto, forced } = schemeSelectors(scope, scheme);
|
|
1086
|
+
css += `${atRule} { ${auto} { ${toCSSText(rules)} } }\n`;
|
|
1087
|
+
emitMediaNested(atRule, auto, rules);
|
|
1088
|
+
emitNested(forced, rules);
|
|
1089
|
+
} else {
|
|
1090
|
+
css += `${atRule} { ${scope} { ${toCSSText(rules)} } }\n`;
|
|
1091
|
+
emitMediaNested(atRule, scope, rules);
|
|
1092
|
+
}
|
|
1041
1093
|
}
|
|
1042
1094
|
|
|
1043
1095
|
const tag = document.createElement("style");
|
|
@@ -2083,6 +2135,64 @@ export function toCSSText(rules: Record<string, unknown> | object) {
|
|
|
2083
2135
|
.join("; ");
|
|
2084
2136
|
}
|
|
2085
2137
|
|
|
2138
|
+
// ─── Color Schemes ────────────────────────────────────────────────────────────
|
|
2139
|
+
|
|
2140
|
+
/**
|
|
2141
|
+
* Attribute on the root element (`<html>`) that forces a color scheme. Absent = auto (follow the OS
|
|
2142
|
+
* `prefers-color-scheme`). Values: "light" | "dark".
|
|
2143
|
+
*
|
|
2144
|
+
* @docs framework/concepts/color-schemes
|
|
2145
|
+
*/
|
|
2146
|
+
export const COLOR_SCHEME_ATTR = "data-color-scheme";
|
|
2147
|
+
|
|
2148
|
+
/**
|
|
2149
|
+
* The localStorage key a site switcher persists the visitor's forced scheme under; read by the
|
|
2150
|
+
* compiler-injected pre-paint script. Values: "light" | "dark" (absent = auto).
|
|
2151
|
+
*
|
|
2152
|
+
* @docs framework/concepts/color-schemes
|
|
2153
|
+
*/
|
|
2154
|
+
export const COLOR_SCHEME_STORAGE_KEY = "jx-color-scheme";
|
|
2155
|
+
|
|
2156
|
+
/**
|
|
2157
|
+
* The scheme a _pure_ `prefers-color-scheme` media query targets. Compound queries (any other
|
|
2158
|
+
* conditions attached) return null — they are not eligible for forced-scheme dual emission.
|
|
2159
|
+
*
|
|
2160
|
+
* @param {string} query
|
|
2161
|
+
* @returns {"light" | "dark" | null}
|
|
2162
|
+
* @docs framework/concepts/color-schemes
|
|
2163
|
+
*/
|
|
2164
|
+
export function pureSchemeOf(query: string): "light" | "dark" | null {
|
|
2165
|
+
const m = /^\(\s*prefers-color-scheme\s*:\s*(light|dark)\s*\)$/.exec(query.trim());
|
|
2166
|
+
return (m?.[1] as "light" | "dark") ?? null;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
/**
|
|
2170
|
+
* Selector pair for a scheme-conditional rule: `auto` applies inside the scheme's media query only
|
|
2171
|
+
* while no scheme is forced; `forced` applies unconditionally when the root attribute forces the
|
|
2172
|
+
* scheme. Guards are wrapped in `:where()` so specificity matches the unguarded selector and source
|
|
2173
|
+
* order decides the cascade.
|
|
2174
|
+
*
|
|
2175
|
+
* @param {string} selector
|
|
2176
|
+
* @param {"light" | "dark"} scheme
|
|
2177
|
+
* @returns {{ auto: string; forced: string }}
|
|
2178
|
+
* @docs framework/concepts/color-schemes
|
|
2179
|
+
*/
|
|
2180
|
+
export function schemeSelectors(
|
|
2181
|
+
selector: string,
|
|
2182
|
+
scheme: "light" | "dark",
|
|
2183
|
+
): { auto: string; forced: string } {
|
|
2184
|
+
if (selector === ":root" || selector === "html") {
|
|
2185
|
+
return {
|
|
2186
|
+
auto: `${selector}:where(:not([${COLOR_SCHEME_ATTR}]))`,
|
|
2187
|
+
forced: `${selector}:where([${COLOR_SCHEME_ATTR}="${scheme}"])`,
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
return {
|
|
2191
|
+
auto: `:where(:root:not([${COLOR_SCHEME_ATTR}])) ${selector}`,
|
|
2192
|
+
forced: `:where(:root[${COLOR_SCHEME_ATTR}="${scheme}"]) ${selector}`,
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2086
2196
|
// ─── Custom Element Registration ──────────────────────────────────────────────
|
|
2087
2197
|
|
|
2088
2198
|
let _rootMedia: Record<string, string> = {};
|