@jxsuite/runtime 2.0.5 → 2.1.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/runtime.js +158 -22
- package/dist/runtime.js.map +5 -4
- package/package.json +2 -2
- package/src/runtime.ts +259 -33
- package/src/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/runtime",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "JSON-native reactive web component runtime",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"upgrade": "bunx npm-check-updates -u && bun install"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@jxsuite/schema": "^1.
|
|
34
|
+
"@jxsuite/schema": "^1.9.0",
|
|
35
35
|
"@vue/reactivity": "3.5.41"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
package/src/runtime.ts
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
isTemplateString,
|
|
43
43
|
paramNames,
|
|
44
44
|
} from "@jxsuite/schema/guards";
|
|
45
|
+
import { formatSrcset, parseSrcset } from "@jxsuite/schema/asset-paths";
|
|
45
46
|
import { runStatements } from "./statements.ts";
|
|
46
47
|
import { readCookie, serializeCookie } from "./cookie.ts";
|
|
47
48
|
import type {
|
|
@@ -331,6 +332,119 @@ function canvasAttrName(el: HTMLElement, key: string): string {
|
|
|
331
332
|
return key;
|
|
332
333
|
}
|
|
333
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Rewrite one asset reference for a host that does not serve the site's own URL space.
|
|
337
|
+
*
|
|
338
|
+
* The runtime writes a reference VERBATIM — `el.src = "/hero.jpg"` — so the browser resolves it
|
|
339
|
+
* against the document, and the document is whatever page the renderer happens to be running in. On
|
|
340
|
+
* a real site and on an editing server that serves the project tree, that is correct and this hook
|
|
341
|
+
* stays null. On a multi-tenant editor origin it is not: `/hero.jpg` resolves against the editor,
|
|
342
|
+
* misses, and — behind a single-page-app fallback — comes back as HTML at HTTP 200, so the image
|
|
343
|
+
* renders broken and nothing is logged.
|
|
344
|
+
*
|
|
345
|
+
* A function rather than data because it cannot be one: the values it must fix are produced INSIDE
|
|
346
|
+
* reactive effects (`{"$ref": "#/state/hero"}` is not a string until the effect runs), so no
|
|
347
|
+
* document walk can see them. The Studio canvas iframe sets this at render time.
|
|
348
|
+
*
|
|
349
|
+
* Returning null leaves the value exactly as written, and a null resolver makes every call site
|
|
350
|
+
* below the identity — so production rendering is byte-identical.
|
|
351
|
+
*
|
|
352
|
+
* @param {CanvasAssetResolver | null} fn - The resolver, or null to restore verbatim rendering
|
|
353
|
+
*/
|
|
354
|
+
export type CanvasAssetResolver = (value: string, key: string) => string | null;
|
|
355
|
+
|
|
356
|
+
let _canvasAssetResolver: CanvasAssetResolver | null = null;
|
|
357
|
+
export function setCanvasAssetResolver(fn: CanvasAssetResolver | null) {
|
|
358
|
+
_canvasAssetResolver = fn;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Attribute/property names whose value is an asset reference, on any element. */
|
|
362
|
+
const ASSET_KEYS = new Set(["src", "poster", "data"]);
|
|
363
|
+
|
|
364
|
+
/** Names whose value is a `srcset`-shaped candidate list rather than one reference. */
|
|
365
|
+
const SRCSET_KEYS = new Set(["srcset", "imagesrcset"]);
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Elements on which `href` names an ASSET rather than a destination.
|
|
369
|
+
*
|
|
370
|
+
* `<a href>` and `<area href>` are places to go, not files to load; rewriting one would send a
|
|
371
|
+
* click at the file that backs the page instead of at the page. `<link href>` is the opposite — a
|
|
372
|
+
* stylesheet, an icon, a preload — and is the reason `$head` needs this at all.
|
|
373
|
+
*/
|
|
374
|
+
const HREF_ASSET_TAGS = new Set(["LINK"]);
|
|
375
|
+
|
|
376
|
+
/** True when writing `key` on a `<tagName>` writes an asset reference. */
|
|
377
|
+
function isAssetKey(tagName: string, key: string): boolean {
|
|
378
|
+
return ASSET_KEYS.has(key) || (key === "href" && HREF_ASSET_TAGS.has(tagName));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* The value to actually write for `key` on a `<tagName>` — resolved when it is an asset reference,
|
|
383
|
+
* and the input itself in every other case, including whenever no resolver is installed.
|
|
384
|
+
*
|
|
385
|
+
* `srcset` is split into its candidates and resolved one at a time, because it is N references in
|
|
386
|
+
* one attribute and a resolver handed the whole string would have to re-implement HTML's parser to
|
|
387
|
+
* find them.
|
|
388
|
+
*/
|
|
389
|
+
function canvasAssetValue(tagName: string, key: string, value: string): string {
|
|
390
|
+
if (!_canvasAssetResolver || value === "") {
|
|
391
|
+
return value;
|
|
392
|
+
}
|
|
393
|
+
const name = key.toLowerCase();
|
|
394
|
+
if (SRCSET_KEYS.has(name)) {
|
|
395
|
+
const candidates = parseSrcset(value);
|
|
396
|
+
let changed = false;
|
|
397
|
+
const next = candidates.map((candidate) => {
|
|
398
|
+
const resolved = _canvasAssetResolver?.(candidate.url, name) ?? null;
|
|
399
|
+
if (resolved === null || resolved === candidate.url) {
|
|
400
|
+
return candidate;
|
|
401
|
+
}
|
|
402
|
+
changed = true;
|
|
403
|
+
return { descriptor: candidate.descriptor, url: resolved };
|
|
404
|
+
});
|
|
405
|
+
return changed ? formatSrcset(next) : value;
|
|
406
|
+
}
|
|
407
|
+
if (!isAssetKey(tagName, name)) {
|
|
408
|
+
return value;
|
|
409
|
+
}
|
|
410
|
+
return _canvasAssetResolver(value, name) ?? value;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Every `url(...)` inside a CSS value, unquoted or quoted with either quote. */
|
|
414
|
+
const CSS_URL_RE = /\burl\(\s*(?<q>["']?)(?<u>[^"')]*)\k<q>\s*\)/g;
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Resolve every `url()` inside a CSS value.
|
|
418
|
+
*
|
|
419
|
+
* Applied to CSS rather than to a list of properties because in CSS a `url()` is ALWAYS a resource
|
|
420
|
+
* reference — `background-image`, `mask`, `cursor`, `@font-face src`, `list-style-image` and every
|
|
421
|
+
* shorthand that contains them — so enumerating the properties would be a list that goes stale
|
|
422
|
+
* while the syntax does not.
|
|
423
|
+
*/
|
|
424
|
+
function canvasStyleUrls(value: string): string {
|
|
425
|
+
if (!_canvasAssetResolver || !value.includes("url(")) {
|
|
426
|
+
return value;
|
|
427
|
+
}
|
|
428
|
+
return value.replaceAll(CSS_URL_RE, (whole, quote: string, url: string) => {
|
|
429
|
+
const resolved = url === "" ? null : (_canvasAssetResolver?.(url, "url()") ?? null);
|
|
430
|
+
return resolved === null ? whole : `url(${quote}${resolved}${quote})`;
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* A style scalar as the canvas wants it written: assets resolved, then viewport units transposed.
|
|
436
|
+
*
|
|
437
|
+
* Exported because the canvas emits some CSS itself — the site-level `style` block, which never
|
|
438
|
+
* passes through {@link applyStyle} — and a second implementation of "what the canvas does to a
|
|
439
|
+
* style value" is a second thing to keep in step.
|
|
440
|
+
*
|
|
441
|
+
* @param {string} value - A CSS declaration value
|
|
442
|
+
* @returns {string} The value as the canvas should write it
|
|
443
|
+
*/
|
|
444
|
+
export function canvasStyleValue(value: string): string {
|
|
445
|
+
return transposeCanvasUnits(canvasStyleUrls(value));
|
|
446
|
+
}
|
|
447
|
+
|
|
334
448
|
/**
|
|
335
449
|
* Studio-canvas prop-binding markers. When on, `bindProperty` stamps `data-jx-bound-prop="<key>"`
|
|
336
450
|
* on elements whose `textContent` is an invertible single-key state binding — the pure template
|
|
@@ -761,6 +875,32 @@ export const RESERVED_KEYS = new Set([
|
|
|
761
875
|
/** Refs already warned about, so the §13 diagnostic fires once per distinct target. */
|
|
762
876
|
const warnedRefChildren = new Set<string>();
|
|
763
877
|
|
|
878
|
+
/** Foreign-content namespaces the HTML parser switches into, and the runtime must too. */
|
|
879
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
880
|
+
const MATHML_NS = "http://www.w3.org/1998/Math/MathML";
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* The namespace an element belongs in, given the one its parent established.
|
|
884
|
+
*
|
|
885
|
+
* The static build emits markup text, so the HTML parser applied foreign-content rules for it and
|
|
886
|
+
* SVG worked there. The runtime built every node with `document.createElement`, which is HTML-only,
|
|
887
|
+
* so the same document rendered blank in the Studio canvas and in any hydrated component — the
|
|
888
|
+
* editor and the deployed site disagreeing, with only the editor wrong.
|
|
889
|
+
*
|
|
890
|
+
* @param {string} tagName
|
|
891
|
+
* @param {string | null | undefined} inherited - The namespace the parent element established
|
|
892
|
+
* @returns {string | null} Null means HTML
|
|
893
|
+
*/
|
|
894
|
+
function elementNamespace(tagName: string, inherited: string | null | undefined): string | null {
|
|
895
|
+
if (tagName === "svg") {
|
|
896
|
+
return SVG_NS;
|
|
897
|
+
}
|
|
898
|
+
if (tagName === "math") {
|
|
899
|
+
return MATHML_NS;
|
|
900
|
+
}
|
|
901
|
+
return inherited ?? null;
|
|
902
|
+
}
|
|
903
|
+
|
|
764
904
|
export function renderNode(
|
|
765
905
|
def: JxElement | string | number | boolean,
|
|
766
906
|
state: JxScope,
|
|
@@ -843,7 +983,19 @@ export function renderNode(
|
|
|
843
983
|
return renderSwitch(def, localState, options);
|
|
844
984
|
}
|
|
845
985
|
|
|
846
|
-
const
|
|
986
|
+
const ns = elementNamespace(tagName, options?._ns);
|
|
987
|
+
/*
|
|
988
|
+
* `foreignObject` is itself an SVG element, and is the documented way back into HTML: its
|
|
989
|
+
* DESCENDANTS are HTML, so the element and its children carry different namespaces.
|
|
990
|
+
*/
|
|
991
|
+
const childNs = tagName === "foreignObject" ? null : ns;
|
|
992
|
+
/*
|
|
993
|
+
* A namespaced node comes back as Element; every apply* helper below is typed for HTMLElement
|
|
994
|
+
* and touches only members both share (style, dataset, setAttribute, append).
|
|
995
|
+
*/
|
|
996
|
+
const el = (ns
|
|
997
|
+
? document.createElementNS(ns, tagName)
|
|
998
|
+
: document.createElement(tagName)) as unknown as HTMLElement;
|
|
847
999
|
|
|
848
1000
|
if (options?.onNodeCreated) {
|
|
849
1001
|
options.onNodeCreated(el, path, def, localState);
|
|
@@ -862,12 +1014,16 @@ export function renderNode(
|
|
|
862
1014
|
if (isMappedArray(kids)) {
|
|
863
1015
|
// Legacy whole-children repeater: the items render directly into `el` (which keeps its own
|
|
864
1016
|
// TagName, e.g. <ul>), with no extra wrapper element.
|
|
865
|
-
const arrOpts =
|
|
1017
|
+
const arrOpts =
|
|
1018
|
+
options || childNs ? { ...options, _ns: childNs, _path: [...path, "children"] } : undefined;
|
|
866
1019
|
renderMappedArrayInto(el, kids, localState, arrOpts);
|
|
867
1020
|
} else if (Array.isArray(kids)) {
|
|
868
1021
|
for (let i = 0; i < kids.length; i++) {
|
|
869
1022
|
const child = kids[i]!;
|
|
870
|
-
const childOpts =
|
|
1023
|
+
const childOpts =
|
|
1024
|
+
options || childNs
|
|
1025
|
+
? { ...options, _ns: childNs, _path: [...path, "children", i] }
|
|
1026
|
+
: undefined;
|
|
871
1027
|
if (isMappedArray(child)) {
|
|
872
1028
|
// Array pseudo-element among siblings: expand inline, no wrapper.
|
|
873
1029
|
renderMappedArrayInto(el, child, localState, childOpts);
|
|
@@ -957,6 +1113,24 @@ function bindProperty(el: HTMLElement, key: string, val: unknown, state: JxScope
|
|
|
957
1113
|
if (boundProp) {
|
|
958
1114
|
el.dataset.jxBoundProp = boundProp;
|
|
959
1115
|
}
|
|
1116
|
+
/* An `<img>` in a document says `{"tagName": "img", "src": "/hero.jpg"}`, so `src` arrives HERE
|
|
1117
|
+
as a top-level key and is written as a DOM PROPERTY — `attributes: { src }` is the rarer
|
|
1118
|
+
spelling. Both are the same reference and both need the same resolution. */
|
|
1119
|
+
const asAsset = (resolved: unknown): unknown =>
|
|
1120
|
+
typeof resolved === "string" ? canvasAssetValue(el.tagName, key, resolved) : resolved;
|
|
1121
|
+
/*
|
|
1122
|
+
* `className` on an SVGElement is a read-only SVGAnimatedString, so assigning it throws in the
|
|
1123
|
+
* strict mode an ES module always runs under. It is the most common Jx element property, so the
|
|
1124
|
+
* attribute form is the only one that works on both.
|
|
1125
|
+
*/
|
|
1126
|
+
const write = (resolved: unknown): void => {
|
|
1127
|
+
const node = el as unknown as Element;
|
|
1128
|
+
if (key === "className" && !(node instanceof HTMLElement)) {
|
|
1129
|
+
node.setAttribute("class", resolved == null ? "" : String(resolved));
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
target[key] = resolved;
|
|
1133
|
+
};
|
|
960
1134
|
if (isRefObj(val)) {
|
|
961
1135
|
const refVal = val as { $ref: string };
|
|
962
1136
|
if (key === "id") {
|
|
@@ -964,7 +1138,7 @@ function bindProperty(el: HTMLElement, key: string, val: unknown, state: JxScope
|
|
|
964
1138
|
return;
|
|
965
1139
|
}
|
|
966
1140
|
effect(() => {
|
|
967
|
-
|
|
1141
|
+
write(asAsset(resolveRef(refVal.$ref, state)));
|
|
968
1142
|
});
|
|
969
1143
|
return;
|
|
970
1144
|
}
|
|
@@ -972,12 +1146,12 @@ function bindProperty(el: HTMLElement, key: string, val: unknown, state: JxScope
|
|
|
972
1146
|
// Universal ${} reactivity — template strings in element properties
|
|
973
1147
|
if (isTemplateString(val)) {
|
|
974
1148
|
effect(() => {
|
|
975
|
-
|
|
1149
|
+
write(asAsset(evaluateTemplate(val as string, state)));
|
|
976
1150
|
});
|
|
977
1151
|
return;
|
|
978
1152
|
}
|
|
979
1153
|
|
|
980
|
-
|
|
1154
|
+
write(asAsset(val));
|
|
981
1155
|
}
|
|
982
1156
|
|
|
983
1157
|
/**
|
|
@@ -1038,22 +1212,22 @@ export function applyStyle(
|
|
|
1038
1212
|
if (prop.startsWith("--")) {
|
|
1039
1213
|
if (isTemplateString(val)) {
|
|
1040
1214
|
effect(() => {
|
|
1041
|
-
el.style.setProperty(prop,
|
|
1215
|
+
el.style.setProperty(prop, canvasStyleValue(evaluateTemplate(val, state)));
|
|
1042
1216
|
});
|
|
1043
1217
|
} else {
|
|
1044
|
-
el.style.setProperty(prop,
|
|
1218
|
+
el.style.setProperty(prop, canvasStyleValue(scalar));
|
|
1045
1219
|
}
|
|
1046
1220
|
} else if (isTemplateString(val)) {
|
|
1047
1221
|
effect(() => {
|
|
1048
|
-
(el.style as unknown as Record<string, string>)[prop] =
|
|
1222
|
+
(el.style as unknown as Record<string, string>)[prop] = canvasStyleValue(
|
|
1049
1223
|
evaluateTemplate(val, state),
|
|
1050
1224
|
);
|
|
1051
1225
|
});
|
|
1052
1226
|
} else if (mediaOverriddenProps.has(prop)) {
|
|
1053
|
-
// Goes through toCSSText (which transposes) — don't
|
|
1227
|
+
// Goes through toCSSText (which resolves and transposes) — don't do either twice here.
|
|
1054
1228
|
baseDecls[prop] = scalar;
|
|
1055
1229
|
} else {
|
|
1056
|
-
(el.style as unknown as Record<string, string>)[prop] =
|
|
1230
|
+
(el.style as unknown as Record<string, string>)[prop] = canvasStyleValue(scalar);
|
|
1057
1231
|
}
|
|
1058
1232
|
}
|
|
1059
1233
|
|
|
@@ -1134,11 +1308,7 @@ export function applyStyle(
|
|
|
1134
1308
|
if (key === "@--") {
|
|
1135
1309
|
continue;
|
|
1136
1310
|
} // Base canvas width, not a real media query
|
|
1137
|
-
const query = key
|
|
1138
|
-
? (mediaQueries[key.slice(1)] ?? key.slice(1))
|
|
1139
|
-
: key.startsWith("@(")
|
|
1140
|
-
? key.slice(1)
|
|
1141
|
-
: null;
|
|
1311
|
+
const query = resolveAtQuery(key, mediaQueries);
|
|
1142
1312
|
const atRule = query === null ? key : `@media ${query}`;
|
|
1143
1313
|
const scope = `[data-jx="${uid}"]`;
|
|
1144
1314
|
const scheme = query === null ? null : pureSchemeOf(query);
|
|
@@ -1201,12 +1371,17 @@ export function reapplyStyle(
|
|
|
1201
1371
|
function applyAttributes(el: HTMLElement, attrs: Record<string, JxAttributeValue>, state: JxScope) {
|
|
1202
1372
|
for (const [k, v] of Object.entries(attrs)) {
|
|
1203
1373
|
const attr = canvasAttrName(el, k);
|
|
1374
|
+
/* Inside the effects, not before them. A `$ref` or `${…}` value is not a string until the
|
|
1375
|
+
effect runs — which is exactly why the document walk this replaced could never see it. */
|
|
1376
|
+
const write = (resolved: unknown) => {
|
|
1377
|
+
el.setAttribute(attr, canvasAssetValue(el.tagName, k, String(resolved ?? "")));
|
|
1378
|
+
};
|
|
1204
1379
|
if (isRefObj(v)) {
|
|
1205
|
-
effect(() =>
|
|
1380
|
+
effect(() => write(resolveRef(v.$ref, state)));
|
|
1206
1381
|
} else if (isTemplateString(v)) {
|
|
1207
|
-
effect(() =>
|
|
1382
|
+
effect(() => write(evaluateTemplate(v, state)));
|
|
1208
1383
|
} else {
|
|
1209
|
-
|
|
1384
|
+
write(v);
|
|
1210
1385
|
}
|
|
1211
1386
|
}
|
|
1212
1387
|
}
|
|
@@ -2282,10 +2457,43 @@ export function camelToKebab(s: string) {
|
|
|
2282
2457
|
export function toCSSText(rules: Record<string, unknown> | object) {
|
|
2283
2458
|
return Object.entries(rules)
|
|
2284
2459
|
.filter(([k, v]) => !isNestedSelector(k) && (v === null || typeof v !== "object"))
|
|
2285
|
-
.map(([p, v]) => `${camelToKebab(p)}: ${
|
|
2460
|
+
.map(([p, v]) => `${camelToKebab(p)}: ${canvasStyleValue(String(v))}`)
|
|
2286
2461
|
.join("; ");
|
|
2287
2462
|
}
|
|
2288
2463
|
|
|
2464
|
+
// ─── Style At-Rules ───────────────────────────────────────────────────────────
|
|
2465
|
+
|
|
2466
|
+
/**
|
|
2467
|
+
* The four CSS media types. A media _type_ is bare — `@media print` — while a media _feature_ is
|
|
2468
|
+
* parenthesised, so `@media (print)` reads as a boolean feature named `print`, which does not exist
|
|
2469
|
+
* and evaluates false.
|
|
2470
|
+
*/
|
|
2471
|
+
const MEDIA_TYPES = new Set(["all", "print", "screen", "speech"]);
|
|
2472
|
+
|
|
2473
|
+
/**
|
|
2474
|
+
* The media query an `@`-prefixed style key names, or null when the key is some other at-rule
|
|
2475
|
+
* (`@supports`, `@starting-style`) that is emitted verbatim.
|
|
2476
|
+
*
|
|
2477
|
+
* `@--name` resolves through the project's `$media` map. `@(…)` carries its own parentheses, so
|
|
2478
|
+
* they are stripped only when what they wrap is a bare media type: `@(print)` must become `@media
|
|
2479
|
+
* print`, while `@(min-width: 40rem)` keeps them.
|
|
2480
|
+
*
|
|
2481
|
+
* @param {string} atKey
|
|
2482
|
+
* @param {Record<string, string>} mediaQueries
|
|
2483
|
+
* @returns {string | null}
|
|
2484
|
+
* @docs framework/concepts/styling
|
|
2485
|
+
*/
|
|
2486
|
+
export function resolveAtQuery(atKey: string, mediaQueries: Record<string, string>): string | null {
|
|
2487
|
+
if (atKey.startsWith("@--")) {
|
|
2488
|
+
return mediaQueries[atKey.slice(1)] ?? atKey.slice(1);
|
|
2489
|
+
}
|
|
2490
|
+
if (atKey.startsWith("@(")) {
|
|
2491
|
+
const inner = atKey.slice(2, -1).trim();
|
|
2492
|
+
return MEDIA_TYPES.has(inner.toLowerCase()) ? inner : atKey.slice(1);
|
|
2493
|
+
}
|
|
2494
|
+
return null;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2289
2497
|
// ─── Color Schemes ────────────────────────────────────────────────────────────
|
|
2290
2498
|
|
|
2291
2499
|
/**
|
|
@@ -2424,15 +2632,17 @@ function injectHead(entries: JxHeadEntry[], _base: string) {
|
|
|
2424
2632
|
// Resolve href/src: bare npm specifiers -> /node_modules/ path
|
|
2425
2633
|
for (const key of ["href", "src"] as const) {
|
|
2426
2634
|
const v = attrs[key];
|
|
2427
|
-
if (
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2635
|
+
if (typeof v !== "string" || v === "") {
|
|
2636
|
+
continue;
|
|
2637
|
+
}
|
|
2638
|
+
if (!v.startsWith("/") && !v.startsWith(".") && !v.startsWith("http")) {
|
|
2639
|
+
/* The bare-specifier lane owns `/node_modules/<pkg>`, which is the HOST's URL space and
|
|
2640
|
+
not the project's. A canvas resolver must never claim it: the file is not in the
|
|
2641
|
+
repository, so there is nothing for it to resolve to. */
|
|
2434
2642
|
attrs[key] = `/node_modules/${v}`;
|
|
2643
|
+
continue;
|
|
2435
2644
|
}
|
|
2645
|
+
attrs[key] = canvasAssetValue(tag.toUpperCase(), key, v);
|
|
2436
2646
|
}
|
|
2437
2647
|
|
|
2438
2648
|
// Deduplicate: skip if an identical element already exists
|
|
@@ -2522,7 +2732,8 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
|
|
|
2522
2732
|
|
|
2523
2733
|
const state = await buildScope(def, {}, base);
|
|
2524
2734
|
|
|
2525
|
-
// Read properties from
|
|
2735
|
+
// Read properties from the data-jx-props payload the site build writes on a
|
|
2736
|
+
// Non-static instance, so an upgrade re-renders with the authored props, not the defaults.
|
|
2526
2737
|
const propsAttr = this.dataset.jxProps;
|
|
2527
2738
|
if (propsAttr) {
|
|
2528
2739
|
try {
|
|
@@ -2608,6 +2819,14 @@ export async function defineElement(source: string | JxDocument, baseUrl?: strin
|
|
|
2608
2819
|
applyStyle(this, def.style ?? {}, (state["$media"] as Record<string, string>) ?? {}, state);
|
|
2609
2820
|
applyAttributes(this, def.attributes ?? {}, state);
|
|
2610
2821
|
|
|
2822
|
+
/*
|
|
2823
|
+
* Root-level `textContent` is a definition's content just as much as `children` is — it is
|
|
2824
|
+
* the shape spec.md §17.2 recommends when every child would be a bare string — and it was
|
|
2825
|
+
* dropped here, so such a component rendered empty everywhere the interpreter runs.
|
|
2826
|
+
*/
|
|
2827
|
+
if (!Array.isArray(def.children) && def.textContent !== undefined) {
|
|
2828
|
+
bindProperty(this, "textContent", def.textContent, state);
|
|
2829
|
+
}
|
|
2611
2830
|
const children = Array.isArray(def.children) ? def.children : [];
|
|
2612
2831
|
for (const childDef of children) {
|
|
2613
2832
|
this.append(renderNode(childDef, state));
|
|
@@ -2680,20 +2899,27 @@ function renderCustomElementWithProps(
|
|
|
2680
2899
|
if (refusePrivateProp(key, "$props")) {
|
|
2681
2900
|
continue;
|
|
2682
2901
|
}
|
|
2902
|
+
/* The same resolution `bindProperty` gives an ordinary element. A `$props: { src }` on a custom
|
|
2903
|
+
element is an asset reference authored at the USAGE site, so it belongs to the document that
|
|
2904
|
+
wrote it — a media prop the component forwards into its own `<img>` is resolved separately,
|
|
2905
|
+
inside the component's render, against the same context. */
|
|
2906
|
+
const write = (resolved: unknown): void => {
|
|
2907
|
+
(el as unknown as Record<string, unknown>)[key] =
|
|
2908
|
+
typeof resolved === "string" ? canvasAssetValue(el.tagName, key, resolved) : resolved;
|
|
2909
|
+
};
|
|
2683
2910
|
if (isRefObj(val)) {
|
|
2684
2911
|
const refVal = val;
|
|
2685
|
-
|
|
2686
|
-
(el as unknown as Record<string, unknown>)[key] = resolved;
|
|
2912
|
+
write(resolveRef(refVal.$ref, state));
|
|
2687
2913
|
// Reactive forwarding: re-set the property when the source changes
|
|
2688
2914
|
effect(() => {
|
|
2689
|
-
(
|
|
2915
|
+
write(resolveRef(refVal.$ref, state));
|
|
2690
2916
|
});
|
|
2691
2917
|
} else if (isTemplateString(val)) {
|
|
2692
2918
|
effect(() => {
|
|
2693
|
-
(
|
|
2919
|
+
write(evaluateTemplate(val, state));
|
|
2694
2920
|
});
|
|
2695
2921
|
} else {
|
|
2696
|
-
(
|
|
2922
|
+
write(val);
|
|
2697
2923
|
}
|
|
2698
2924
|
}
|
|
2699
2925
|
|
package/src/types.ts
CHANGED
|
@@ -15,6 +15,8 @@ export type JxEventHandler = (scope: JxScope, event: Event) => unknown;
|
|
|
15
15
|
|
|
16
16
|
export interface JxRenderOptions {
|
|
17
17
|
_path?: JxPath;
|
|
18
|
+
/** The namespace the parent element established — SVG and MathML descendants inherit it. */
|
|
19
|
+
_ns?: string | null;
|
|
18
20
|
/**
|
|
19
21
|
* Called for each created node. `state` is the local scope the node's children render with —
|
|
20
22
|
* callers can capture it to re-render a subtree in isolation later.
|