@webjsdev/core 0.7.65 → 0.9.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/README.md +5 -0
- package/dist/webjs-core-browser.js +6 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +22 -21
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +7 -6
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +18 -9
- package/index.d.ts +26 -10
- package/index.js +21 -9
- package/package.json +15 -2
- package/src/action-effects-client.d.ts +5 -0
- package/src/action-effects-client.js +84 -0
- package/src/action-seed-client.js +5 -5
- package/src/asset-url.js +12 -13
- package/src/async-directive.d.ts +18 -0
- package/src/async-directive.js +88 -0
- package/src/attribute-directives.d.ts +6 -0
- package/src/attribute-directives.js +49 -0
- package/src/attribute-reader.js +213 -25
- package/src/binding-prefixes.js +10 -53
- package/src/component/lifecycle.js +921 -252
- package/src/component/reactive.js +7 -14
- package/src/component/server-element.js +23 -1
- package/src/component.d.ts +160 -44
- package/src/context.d.ts +94 -18
- package/src/context.js +341 -217
- package/src/converters.d.ts +7 -0
- package/src/converters.js +38 -0
- package/src/css.d.ts +32 -6
- package/src/css.js +178 -14
- package/src/directive.d.ts +58 -0
- package/src/directive.js +119 -0
- package/src/directives.d.ts +40 -1
- package/src/directives.js +160 -1
- package/src/form-action.d.ts +13 -0
- package/src/form-action.js +139 -4
- package/src/hot.js +23 -0
- package/src/html.d.ts +8 -0
- package/src/html.js +55 -2
- package/src/is-server.d.ts +2 -0
- package/src/is-server.js +18 -0
- package/src/jsx-guard.js +130 -0
- package/src/metadata.d.ts +22 -3
- package/src/nav.d.ts +4 -1
- package/src/nav.js +44 -11
- package/src/optimistic.js +2 -2
- package/src/refresh-header.js +52 -0
- package/src/registry.js +10 -1
- package/src/render-client/adopt-slots.js +250 -0
- package/src/render-client/parts.js +790 -42
- package/src/render-client/reconciler.js +84 -23
- package/src/render-client/template-compiler.js +152 -43
- package/src/render-client.d.ts +6 -1
- package/src/render-server/attrs.js +2 -2
- package/src/render-server/compile.js +50 -2
- package/src/render-server/errors.js +37 -0
- package/src/render-server/expand.js +100 -20
- package/src/render-server/served-attrs.js +78 -0
- package/src/render-server/walk.js +177 -5
- package/src/render-server.d.ts +3 -0
- package/src/render-server.js +2 -0
- package/src/repeat.d.ts +7 -2
- package/src/repeat.js +20 -6
- package/src/router-client/anchors.js +3 -1
- package/src/router-client/boundaries.js +15 -1
- package/src/router-client/constants.js +30 -0
- package/src/router-client/dom-differ.js +17 -0
- package/src/router-client/fetch-apply.js +107 -5
- package/src/router-client/frames.js +6 -3
- package/src/router-client/head-merge.js +58 -10
- package/src/router-client/navigator.js +49 -9
- package/src/router-client/owned-styles.js +372 -0
- package/src/router-client/prefetch.js +6 -4
- package/src/router-client/root-attrs.js +43 -0
- package/src/router-client/stream.js +7 -0
- package/src/router-client/stylesheets.js +191 -0
- package/src/router-client/swap.js +53 -10
- package/src/routes.d.ts +75 -18
- package/src/sentinels.d.ts +2 -0
- package/src/sentinels.js +4 -0
- package/src/serialize.d.ts +2 -0
- package/src/serialize.js +35 -4
- package/src/signal.d.ts +7 -0
- package/src/signal.js +26 -6
- package/src/slot/polyfills.js +3 -3
- package/src/slot.js +4 -8
- package/src/static-html.d.ts +22 -0
- package/src/static-html.js +144 -0
- package/src/task.d.ts +63 -20
- package/src/task.js +326 -293
- package/src/webjs-config.d.ts +2 -9
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BINDING_PREFIXES } from '../binding-prefixes.js';
|
|
2
2
|
import { isVoidElement, isRawtextTag, isTextOnlyTag } from './html-scan.js';
|
|
3
|
+
import { assertNoJsxIsms } from '../jsx-guard.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* The SSR template compiler (#1646): one tokenizer pass per template, run
|
|
@@ -74,6 +75,8 @@ export function _compileCount() { return compiled; }
|
|
|
74
75
|
export function compile(strings) {
|
|
75
76
|
let ops = cache.get(strings);
|
|
76
77
|
if (ops === undefined) {
|
|
78
|
+
// `className=`, `htmlFor=`, `<Link>`: JSX that HTML silently ignores.
|
|
79
|
+
assertNoJsxIsms(strings);
|
|
77
80
|
ops = tokenize(strings);
|
|
78
81
|
cache.set(strings, ops);
|
|
79
82
|
}
|
|
@@ -115,6 +118,9 @@ function tokenize(strings) {
|
|
|
115
118
|
let attrName = '';
|
|
116
119
|
let attrStart = 0; // index into `chunk` of the attribute's first character
|
|
117
120
|
let attrQuote = '';
|
|
121
|
+
// An unquoted value holding a hole, written quoted (`a=x${v}` -> `a="x..."`),
|
|
122
|
+
// so a value with a space or `>` in it cannot add an attribute or close the tag.
|
|
123
|
+
let synthQuote = false;
|
|
118
124
|
let commentDashes = 0;
|
|
119
125
|
let currentTag = '';
|
|
120
126
|
/** @type {Op | null} */
|
|
@@ -181,8 +187,10 @@ function tokenize(strings) {
|
|
|
181
187
|
state = 'text';
|
|
182
188
|
};
|
|
183
189
|
|
|
190
|
+
let bindingQuote = '';
|
|
184
191
|
for (let i = 0; i < strings.length; i++) {
|
|
185
|
-
const s = strings[i];
|
|
192
|
+
const s = bindingQuote ? strings[i].slice(1) : strings[i];
|
|
193
|
+
bindingQuote = '';
|
|
186
194
|
for (let j = 0; j < s.length; j++) {
|
|
187
195
|
const c = s[j];
|
|
188
196
|
switch (state) {
|
|
@@ -321,6 +329,18 @@ function tokenize(strings) {
|
|
|
321
329
|
else chunk += c;
|
|
322
330
|
break;
|
|
323
331
|
case 'attr-quoted':
|
|
332
|
+
if (synthQuote) {
|
|
333
|
+
// It ends where the unquoted value it stands for ends, and the
|
|
334
|
+
// character that ends it is read again as the tag's own.
|
|
335
|
+
if (WS.test(c) || c === '>' || (c === '/' && s[j + 1] === '>')) {
|
|
336
|
+
chunk += '"';
|
|
337
|
+
synthQuote = false;
|
|
338
|
+
state = 'in-tag';
|
|
339
|
+
attrName = '';
|
|
340
|
+
j--;
|
|
341
|
+
} else chunk += c === '"' ? '"' : c;
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
324
344
|
chunk += c;
|
|
325
345
|
if (c === attrQuote) { state = 'in-tag'; attrName = ''; }
|
|
326
346
|
break;
|
|
@@ -366,6 +386,22 @@ function tokenize(strings) {
|
|
|
366
386
|
}
|
|
367
387
|
|
|
368
388
|
if (i < strings.length - 1) {
|
|
389
|
+
// An unquoted value with more than the one hole in it (`a=x${v}`,
|
|
390
|
+
// `a=${x}${y}`, `a=${x}/z`) is written quoted, so the walker resolves it
|
|
391
|
+
// whole through the quoted-value path, as the client does.
|
|
392
|
+
if (BINDING_PREFIXES[attrName[0]] === undefined && !synthQuote) {
|
|
393
|
+
const next = strings[i + 1];
|
|
394
|
+
const ends = next !== '' && (WS.test(next[0]) || next[0] === '>' || (next[0] === '/' && next[1] === '>'));
|
|
395
|
+
if (state === 'after-eq' && !ends) {
|
|
396
|
+
chunk += '"';
|
|
397
|
+
state = 'attr-quoted'; attrQuote = '"'; synthQuote = true;
|
|
398
|
+
} else if (state === 'attr-unquoted') {
|
|
399
|
+
const eq = chunk.indexOf('=', attrStart) + 1;
|
|
400
|
+
const gap = /^\s*/.exec(chunk.slice(eq))[0].length;
|
|
401
|
+
chunk = chunk.slice(0, eq + gap) + '"' + chunk.slice(eq + gap).replace(/"/g, '"');
|
|
402
|
+
state = 'attr-quoted'; attrQuote = '"'; synthQuote = true;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
369
405
|
// A value hole sits here. Its op says where, and the walker decides what.
|
|
370
406
|
if (state === 'comment' || state === 'comment-start' || state === 'comment-start-dash' || state === 'comment-bang') {
|
|
371
407
|
flush();
|
|
@@ -393,9 +429,21 @@ function tokenize(strings) {
|
|
|
393
429
|
});
|
|
394
430
|
state = 'in-tag';
|
|
395
431
|
attrName = '';
|
|
432
|
+
} else if (state === 'attr-quoted' && BINDING_PREFIXES[attrName[0]] !== undefined) {
|
|
433
|
+
const lead = chunk.slice(attrStart);
|
|
434
|
+
if (!lead.endsWith('=' + attrQuote) || !strings[i + 1].startsWith(attrQuote)) {
|
|
435
|
+
throw new Error('Event, property and boolean bindings require exactly one expression with no surrounding text.');
|
|
436
|
+
}
|
|
437
|
+
chunk = chunk.slice(0, attrStart);
|
|
438
|
+
flush();
|
|
439
|
+
ops.push({ t: HOLE, k: ATTR, kind: BINDING_PREFIXES[attrName[0]], name: attrName.slice(1), tag: currentTag, lead: lead.slice(0, -1) });
|
|
440
|
+
bindingQuote = attrQuote;
|
|
441
|
+
state = 'in-tag';
|
|
442
|
+
attrName = '';
|
|
396
443
|
} else if (state === 'attr-quoted' || state === 'attr-unquoted') {
|
|
444
|
+
const prefix = chunk.slice(attrStart);
|
|
397
445
|
flush();
|
|
398
|
-
ops.push({ t: HOLE, k: VALUE, name: attrName, tag: currentTag });
|
|
446
|
+
ops.push({ t: HOLE, k: VALUE, name: attrName, tag: currentTag, quote: state === 'attr-quoted' ? attrQuote : '', prefix });
|
|
399
447
|
} else {
|
|
400
448
|
// A hole in a tag name, between attributes, in a close tag or a markup
|
|
401
449
|
// declaration: nothing is emitted for it, as before.
|
|
@@ -36,6 +36,43 @@ export function browserMemberHint(e) {
|
|
|
36
36
|
return null;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Lit decorator names. Bun runs legacy decorators, so a decorated class fails
|
|
41
|
+
* there with a `ReferenceError` naming the undefined decorator; Node and the
|
|
42
|
+
* browser fail the whole module with a bare `SyntaxError` instead.
|
|
43
|
+
*/
|
|
44
|
+
const DECORATOR_NAMES = new Set([
|
|
45
|
+
'customElement', 'property', 'state', 'query', 'queryAll', 'queryAsync',
|
|
46
|
+
'queryAssignedElements', 'queryAssignedNodes', 'eventOptions', 'provide', 'consume',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* If `e` is the recognisable failure of a Lit DECORATOR reaching the runtime
|
|
51
|
+
* (WebJs has no build step and decorators are not erasable TypeScript, so the
|
|
52
|
+
* type stripper passes them through), return the WebJs fix; otherwise null.
|
|
53
|
+
* Node and the browser report a bare `SyntaxError: Invalid or unexpected
|
|
54
|
+
* token` for the `@`, which names neither cause nor fix; Bun reports a
|
|
55
|
+
* `ReferenceError: property is not defined`. The `no-non-erasable-typescript`
|
|
56
|
+
* check rule catches the same thing at edit time with a per-decorator fix.
|
|
57
|
+
* @param {unknown} e
|
|
58
|
+
* @returns {string | null}
|
|
59
|
+
*/
|
|
60
|
+
export function decoratorHint(e) {
|
|
61
|
+
const msg = e && typeof (/** @type any */ (e).message) === 'string' ? /** @type any */ (e).message : '';
|
|
62
|
+
const fix =
|
|
63
|
+
'WebJs runs no build step, so decorators cannot run. Declare reactive properties in `static properties` ' +
|
|
64
|
+
'(or the `WebComponent({ ... })` factory), use `state: true` for `@state`, call `MyEl.register(tag)` for ' +
|
|
65
|
+
'`@customElement`, and write `@query` as a getter over `this.renderRoot`.';
|
|
66
|
+
if (e instanceof SyntaxError && /Invalid or unexpected token|Unexpected token '@'|Unexpected character '@'|[Dd]ecorators? /.test(msg)) {
|
|
67
|
+
return `A module failed to parse. If it uses a decorator (\`@property\`, \`@customElement\`, ...), that is the cause. ${fix}`;
|
|
68
|
+
}
|
|
69
|
+
const m = /^(\w+) is not defined\b/.exec(msg);
|
|
70
|
+
if (e instanceof ReferenceError && m && DECORATOR_NAMES.has(m[1])) {
|
|
71
|
+
return `\`${m[1]}\` is a Lit decorator, which WebJs does not provide. ${fix}`;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
39
76
|
// Browser-only names whose absence during SSR produces a recognisable error.
|
|
40
77
|
// Mirrors the `no-browser-globals-in-render` webjs check rule, which catches
|
|
41
78
|
// these at edit time; this turns the runtime SSR crash into the same guidance.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { stylesToString,
|
|
1
|
+
import { stylesToString, finalizeStyles, lightDomStylesMessage } from '../css.js';
|
|
2
2
|
import { unsafeHTML } from '../directives.js';
|
|
3
3
|
import { escapeAttr } from '../escape.js';
|
|
4
|
-
import { defaultSSRErrorTemplate, browserMemberHint } from './errors.js';
|
|
4
|
+
import { defaultSSRErrorTemplate, browserMemberHint, isProd } from './errors.js';
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
applyAttrsToInstance, consumePropAttrs, parseAttrs, seedServerAttrs,
|
|
7
7
|
withHostMarker,
|
|
8
8
|
} from './attrs.js';
|
|
9
|
+
import { serveInstanceAttrs } from './served-attrs.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Custom-element expansion inside the op walk (#1646): what `injectDSD`
|
|
@@ -44,6 +45,7 @@ const NON_BLANK = /\S/;
|
|
|
44
45
|
export function beginComponent(w, tag, Cls, opening, attrs) {
|
|
45
46
|
if (w.ctx && w.ctx.usedComponents) w.ctx.usedComponents.add(tag);
|
|
46
47
|
const isShadow = Cls.shadow === true;
|
|
48
|
+
if (!isShadow) reportLightDomStyles(w, tag, Cls);
|
|
47
49
|
const frame = {
|
|
48
50
|
kind: 'component', tag, Cls, isShadow, instance: null, opening, tpl: undefined,
|
|
49
51
|
pending: /** @type {Promise<unknown> | null} */ (null), presentAttrNames: /** @type {Set<string> | null} */ (null),
|
|
@@ -113,10 +115,11 @@ export async function settleRender(frame) {
|
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
function afterRender(frame) {
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
118
|
+
// Serve the attributes as the component left them up to and including
|
|
119
|
+
// render(): one it added is appended, one it changed carries its new value,
|
|
120
|
+
// and one it removed is dropped, as lit-ssr's renderAttributes() does (see
|
|
121
|
+
// served-attrs.js). Reading after render() captures all.
|
|
122
|
+
frame.opening = serveInstanceAttrs(frame.opening, frame.instance, frame.presentAttrNames);
|
|
120
123
|
// Mark LIGHT-DOM hosts for the `@layer webjs-host` display:block default.
|
|
121
124
|
// Shadow hosts are NOT marked: a document-level rule on the host beats the
|
|
122
125
|
// shadow tree's own `:host { display }`.
|
|
@@ -140,7 +143,7 @@ export async function finishComponent(w, frame, children) {
|
|
|
140
143
|
// Native <slot> stays as-is in the DSD template: the browser projects
|
|
141
144
|
// the host's light children natively. The authored children stay in
|
|
142
145
|
// place after the template, and the source end tag is kept.
|
|
143
|
-
return `${frame.opening}
|
|
146
|
+
return `${frame.opening}${dsdOpenTag(Cls)}${styleString(Cls)}${inner}</template>${children}${frame.closeText}`;
|
|
144
147
|
}
|
|
145
148
|
if (!NON_BLANK.test(inner)) {
|
|
146
149
|
// An empty rendered template (a decorator component with no render(),
|
|
@@ -167,10 +170,12 @@ export async function finishComponent(w, frame, children) {
|
|
|
167
170
|
async function renderOwnTemplate(w, frame, children) {
|
|
168
171
|
const owner = w.owner;
|
|
169
172
|
w.owner = frame.isShadow ? null : { tag: frame.tag, partition: partitionChildren(children, frame.segments), consumed: null };
|
|
173
|
+
w.componentDepth = (w.componentDepth || 0) + 1;
|
|
170
174
|
try {
|
|
171
175
|
return await w.capture(() => w.value(frame.tpl));
|
|
172
176
|
} finally {
|
|
173
177
|
w.owner = owner;
|
|
178
|
+
w.componentDepth -= 1;
|
|
174
179
|
}
|
|
175
180
|
}
|
|
176
181
|
|
|
@@ -202,12 +207,54 @@ function partitionChildren(children, segments) {
|
|
|
202
207
|
return groups;
|
|
203
208
|
}
|
|
204
209
|
|
|
210
|
+
/**
|
|
211
|
+
* The Declarative Shadow DOM opening tag for a shadow component, carrying
|
|
212
|
+
* Lit's `static shadowRootOptions` as the template attributes the HTML parser
|
|
213
|
+
* maps onto `attachShadow()`: the mode, `delegatesFocus`, `clonable` and
|
|
214
|
+
* `serializable`. The common case (no options) is the constant open tag.
|
|
215
|
+
* @param {any} Cls
|
|
216
|
+
* @returns {string}
|
|
217
|
+
*/
|
|
218
|
+
export function dsdOpenTag(Cls) {
|
|
219
|
+
const o = Cls.shadowRootOptions;
|
|
220
|
+
if (!o) return OPEN_DSD;
|
|
221
|
+
const mode = o.mode === 'closed' ? 'closed' : 'open';
|
|
222
|
+
if (mode === 'open' && !o.delegatesFocus && !o.clonable && !o.serializable) return OPEN_DSD;
|
|
223
|
+
return `<template shadowrootmode="${mode}"` +
|
|
224
|
+
(o.delegatesFocus ? ' shadowrootdelegatesfocus' : '') +
|
|
225
|
+
(o.clonable ? ' shadowrootclonable' : '') +
|
|
226
|
+
(o.serializable ? ' shadowrootserializable' : '') + '>';
|
|
227
|
+
}
|
|
228
|
+
const OPEN_DSD = '<template shadowrootmode="open">';
|
|
229
|
+
|
|
205
230
|
function styleString(Cls) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return stylesToString(
|
|
231
|
+
// `elementStyles` is the class's finalized list (Lit's `finalizeStyles`);
|
|
232
|
+
// a class not built on WebComponent has none, so flatten its styles here.
|
|
233
|
+
return stylesToString(Cls.elementStyles ?? finalizeStyles(Cls.styles));
|
|
209
234
|
}
|
|
210
235
|
|
|
236
|
+
/** Light-DOM classes already reported for `static styles`. @type {WeakSet<Function>} */
|
|
237
|
+
const REPORTED_LIGHT_STYLES = new WeakSet();
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* A light-DOM component's `static styles` never apply (there is no shadow
|
|
241
|
+
* root to adopt them into). Report it once per class in dev, on the server,
|
|
242
|
+
* so the cause shows up in the terminal as well as the browser console.
|
|
243
|
+
* @param {import('./walk.js').Walker} w
|
|
244
|
+
* @param {string} tag
|
|
245
|
+
* @param {any} Cls
|
|
246
|
+
*/
|
|
247
|
+
function reportLightDomStyles(w, tag, Cls) {
|
|
248
|
+
if (!Cls.styles || REPORTED_LIGHT_STYLES.has(Cls)) return;
|
|
249
|
+
const dev = w.dev === undefined ? !isProd() : !!w.dev;
|
|
250
|
+
if (!dev) return;
|
|
251
|
+
REPORTED_LIGHT_STYLES.add(Cls);
|
|
252
|
+
console.error(lightDomStylesMessage(tag));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Key of the hook the server installs to hear about isolated component failures. */
|
|
256
|
+
const COMPONENT_ERROR_HOOK = Symbol.for('webjs.ssr.componentError');
|
|
257
|
+
|
|
211
258
|
/**
|
|
212
259
|
* Per-component error isolation (#469). The element renders a
|
|
213
260
|
* component-scoped error state in its place (renderError() customises it;
|
|
@@ -228,20 +275,28 @@ async function errorState(w, frame) {
|
|
|
228
275
|
} else {
|
|
229
276
|
console.error(`[webjs] SSR failed for <${tag}>:`, e);
|
|
230
277
|
}
|
|
278
|
+
// The page still renders (the failure is isolated to this element), so the
|
|
279
|
+
// server hears about it only through this hook: @webjsdev/server installs it
|
|
280
|
+
// and routes it to the current request's dev status.
|
|
281
|
+
const report = /** @type {any} */ (globalThis)[COMPONENT_ERROR_HOOK];
|
|
282
|
+
if (typeof report === 'function') {
|
|
283
|
+
try { report(tag, e); } catch { /* a reporting failure must not affect the render */ }
|
|
284
|
+
}
|
|
231
285
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
232
286
|
let errorInner = '';
|
|
233
287
|
try {
|
|
234
288
|
let errTpl;
|
|
235
289
|
if (instance && typeof instance.renderError === 'function') errTpl = instance.renderError(err);
|
|
236
290
|
if (errTpl === undefined) errTpl = defaultSSRErrorTemplate(tag, err, w.dev);
|
|
237
|
-
|
|
291
|
+
w.componentDepth = (w.componentDepth || 0) + 1;
|
|
292
|
+
try { errorInner = await w.capture(() => w.value(errTpl)); } finally { w.componentDepth -= 1; }
|
|
238
293
|
} catch (renderErrorThrew) {
|
|
239
294
|
console.error(`[webjs] renderError() for <${tag}> also threw:`, renderErrorThrew);
|
|
240
295
|
errorInner = '';
|
|
241
296
|
}
|
|
242
297
|
let opening = frame.opening;
|
|
243
298
|
if (!isShadow) opening = withHostMarker(opening);
|
|
244
|
-
if (isShadow) return `${opening}
|
|
299
|
+
if (isShadow) return `${opening}${dsdOpenTag(Cls)}${styleString(Cls)}${errorInner}</template>`;
|
|
245
300
|
return `${opening}<!--webjs-hydrate-->${errorInner}</${tag}>`;
|
|
246
301
|
}
|
|
247
302
|
|
|
@@ -253,14 +308,39 @@ async function errorState(w, frame) {
|
|
|
253
308
|
* The `slot` attribute value of an element's attribute text, or null when
|
|
254
309
|
* absent. `slot=""` and the reserved alias `default` both mean the default
|
|
255
310
|
* slot (#1015), matching the client record.
|
|
311
|
+
*
|
|
312
|
+
* The attributes are read one by one, the way the HTML tokenizer reads them,
|
|
313
|
+
* rather than by searching for `slot=`. A search has two false matches: the
|
|
314
|
+
* tail of another name (`data-slot="button"`, which every shadcn-shaped part
|
|
315
|
+
* carries, since `\b` sits between the hyphen and the `s`) and text inside a
|
|
316
|
+
* quoted value (`title="a slot=b"`). Either one routed the child to a named
|
|
317
|
+
* slot the template does not have, and the server dropped it (#1666).
|
|
256
318
|
* @param {string} attrsRaw
|
|
257
319
|
* @returns {string | null}
|
|
258
320
|
*/
|
|
259
321
|
export function extractSlotAttr(attrsRaw) {
|
|
260
|
-
const
|
|
261
|
-
if (!
|
|
262
|
-
|
|
263
|
-
|
|
322
|
+
const found = findAttr(attrsRaw, 'slot');
|
|
323
|
+
if (!found) return null;
|
|
324
|
+
return found.value === '' || found.value === 'default' ? null : found.value;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Find one attribute by name in an element's attribute text, reading the
|
|
329
|
+
* attributes in order as the HTML tokenizer does, so a name that merely ENDS
|
|
330
|
+
* in `attrName` or a quoted value that mentions it never matches. Returns
|
|
331
|
+
* the value (empty for a bare attribute) and the span of the whole attribute.
|
|
332
|
+
* @param {string} attrsRaw
|
|
333
|
+
* @param {string} attrName lowercase
|
|
334
|
+
* @returns {{ value: string, start: number, end: number } | null}
|
|
335
|
+
*/
|
|
336
|
+
function findAttr(attrsRaw, attrName) {
|
|
337
|
+
const attr = /([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
338
|
+
let m;
|
|
339
|
+
while ((m = attr.exec(attrsRaw))) {
|
|
340
|
+
if (m[1].toLowerCase() !== attrName) continue;
|
|
341
|
+
return { value: m[2] ?? m[3] ?? m[4] ?? '', start: m.index, end: m.index + m[0].length };
|
|
342
|
+
}
|
|
343
|
+
return null;
|
|
264
344
|
}
|
|
265
345
|
|
|
266
346
|
/**
|
|
@@ -278,9 +358,9 @@ export function finishSlot(frame, fallback) {
|
|
|
278
358
|
let name = null;
|
|
279
359
|
let otherAttrs = '';
|
|
280
360
|
if (NON_BLANK.test(attrsRaw)) {
|
|
281
|
-
const
|
|
282
|
-
name =
|
|
283
|
-
otherAttrs = attrsRaw.
|
|
361
|
+
const found = findAttr(attrsRaw, 'name');
|
|
362
|
+
name = found ? found.value : null;
|
|
363
|
+
otherAttrs = (found ? attrsRaw.slice(0, found.start) + attrsRaw.slice(found.end) : attrsRaw).trim();
|
|
284
364
|
}
|
|
285
365
|
const slotKey = name === 'default' || name === '' ? null : name;
|
|
286
366
|
const projected = owner.partition.get(slotKey);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { escapeAttr } from '../escape.js';
|
|
2
|
+
import { decodeAttrEntities } from './text.js';
|
|
3
|
+
import { appendReflectedAttrs } from './attrs.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Serve a custom element's attributes as the element left them after the
|
|
7
|
+
* server pass, the way lit-ssr's `ElementRenderer.renderAttributes()` emits
|
|
8
|
+
* the instance's final attributes rather than the ones the tag was written
|
|
9
|
+
* with. The browser does the same after upgrade: an element that sets, changes
|
|
10
|
+
* or removes an attribute during its first update shows the result, so the
|
|
11
|
+
* served bytes now match the hydrated DOM instead of the source tag.
|
|
12
|
+
*
|
|
13
|
+
* Three cases, per attribute:
|
|
14
|
+
* - one the element added is appended (`appendReflectedAttrs`, unchanged);
|
|
15
|
+
* - one the element CHANGED is served with its new value;
|
|
16
|
+
* - one the element REMOVED is dropped from the tag.
|
|
17
|
+
* An attribute the element left alone keeps its source text BYTE FOR BYTE
|
|
18
|
+
* (its quoting, its entity spelling, its position), so a page whose
|
|
19
|
+
* components touch nothing renders exactly as before.
|
|
20
|
+
*
|
|
21
|
+
* What stays as written, and why each is safe:
|
|
22
|
+
* - `data-webjs-prop-*`: a parent's `.prop=${value}` binding, consumed into
|
|
23
|
+
* the property BEFORE the instance existed (`consumePropAttrs`), so the
|
|
24
|
+
* instance never holds it and reading it back would drop it. It is kept as
|
|
25
|
+
* written, because the browser decodes it again on upgrade.
|
|
26
|
+
* - `data-wj-host`: added AFTER this step by `withHostMarker`. When a
|
|
27
|
+
* re-processed string already carries it (a streamed boundary's children),
|
|
28
|
+
* it was seeded into the instance like any other attribute and is
|
|
29
|
+
* unchanged, so it is kept.
|
|
30
|
+
* - `data-wj-slot-owner`: the renderer stamps it on `<slot>` elements, not on
|
|
31
|
+
* hosts. A host can only carry it in a re-processed string, where it was
|
|
32
|
+
* seeded and is unchanged, so it is kept.
|
|
33
|
+
* - an attribute that came from a template hole: by this point its value is
|
|
34
|
+
* plain text in the opening tag, seeded like a static one, so the same
|
|
35
|
+
* rule applies. Serving the element's value cannot desynchronize the hole:
|
|
36
|
+
* a host inside a component's template is re-created by that component's
|
|
37
|
+
* client render, which writes the hole's value again, and a host written
|
|
38
|
+
* by a page or a layout never hydrates its attributes at all.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} opening the element's opening tag, `<tag attrs>`, ending in `>`
|
|
41
|
+
* @param {any} instance the server-side instance (the element shim, or a real
|
|
42
|
+
* `HTMLElement` when `renderToString` runs in a browser)
|
|
43
|
+
* @param {Set<string>} presentAttrNames lowercased names already in the source tag
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
export function serveInstanceAttrs(opening, instance, presentAttrNames) {
|
|
47
|
+
if (!instance || typeof instance.getAttribute !== 'function' || typeof instance.getAttributeNames !== 'function') return opening;
|
|
48
|
+
const nameEnd = opening.search(/[\s>]/);
|
|
49
|
+
const head = opening.slice(0, nameEnd);
|
|
50
|
+
const body = opening.slice(nameEnd, -1);
|
|
51
|
+
// The same attribute grammar `parseAttrs` reads the source with, so every
|
|
52
|
+
// name seeded into the instance is found again here.
|
|
53
|
+
const re = /([a-zA-Z_:][\w:.-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
54
|
+
let out = '';
|
|
55
|
+
let last = 0;
|
|
56
|
+
let changed = false;
|
|
57
|
+
let m;
|
|
58
|
+
while ((m = re.exec(body)) !== null) {
|
|
59
|
+
const raw = m[0];
|
|
60
|
+
const name = m[1];
|
|
61
|
+
if (name.toLowerCase().startsWith('data-webjs-prop-')) continue;
|
|
62
|
+
const written = decodeAttrEntities(m[2] ?? m[3] ?? m[4] ?? '');
|
|
63
|
+
const now = instance.getAttribute(name);
|
|
64
|
+
if (now === written) continue;
|
|
65
|
+
changed = true;
|
|
66
|
+
if (now === null) {
|
|
67
|
+
// Removed: drop it together with the whitespace that separated it.
|
|
68
|
+
let start = m.index;
|
|
69
|
+
while (start > last && /\s/.test(body[start - 1])) start--;
|
|
70
|
+
out += body.slice(last, start);
|
|
71
|
+
} else {
|
|
72
|
+
out += body.slice(last, m.index) + (now === '' ? name : `${name}="${escapeAttr(String(now))}"`);
|
|
73
|
+
}
|
|
74
|
+
last = m.index + raw.length;
|
|
75
|
+
}
|
|
76
|
+
const rewritten = changed ? `${head}${out}${body.slice(last)}>` : opening;
|
|
77
|
+
return appendReflectedAttrs(rewritten, instance, presentAttrNames);
|
|
78
|
+
}
|