@webjsdev/core 0.8.0 → 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 +20 -19
- 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 +14 -9
- package/index.d.ts +22 -10
- package/index.js +17 -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-reader.js +197 -23
- package/src/binding-prefixes.js +0 -12
- package/src/component/lifecycle.js +760 -209
- package/src/component/reactive.js +2 -10
- package/src/component/server-element.js +23 -1
- package/src/component.d.ts +151 -41
- 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 +38 -1
- package/src/directives.js +158 -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 +4 -0
- package/src/html.js +35 -3
- 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 +8 -3
- package/src/render-client/parts.js +745 -31
- package/src/render-client/reconciler.js +56 -11
- package/src/render-client/template-compiler.js +139 -44
- package/src/render-client.d.ts +5 -1
- package/src/render-server/attrs.js +2 -2
- package/src/render-server/compile.js +34 -0
- package/src/render-server/errors.js +37 -0
- package/src/render-server/expand.js +61 -8
- package/src/render-server/served-attrs.js +1 -2
- package/src/render-server/walk.js +135 -3
- 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/dom-differ.js +17 -0
- package/src/router-client/fetch-apply.js +86 -5
- package/src/router-client/navigator.js +45 -9
- package/src/router-client/prefetch.js +3 -4
- package/src/router-client/root-attrs.js +43 -0
- package/src/router-client/stream.js +7 -0
- package/src/router-client/swap.js +4 -1
- package/src/routes.d.ts +75 -18
- 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 +1 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { nothing, noChange } from '../sentinels.js';
|
|
2
|
-
import { isTemplate, MARKER } from '../html.js';
|
|
2
|
+
import { isTemplate, MARKER, html } from '../html.js';
|
|
3
3
|
import {
|
|
4
4
|
reconcileFormAction, isBoundFormAction, ABSENT, assertSubmitterHasNoName, assertSubmitterType,
|
|
5
5
|
assertSubmitterHasNoValue, assertSubmitterHasNoStaticFormAction,
|
|
@@ -14,8 +14,16 @@ import { compile, submitterActionBindings, INSTANCE } from './template-compiler.
|
|
|
14
14
|
import { commitAdoption, planSlotAdoption, retargetParts } from './adopt-slots.js';
|
|
15
15
|
import {
|
|
16
16
|
applyPart, bindPart, currentRenderHost, setCurrentRenderHost, currentRenderRoot, findSlotHost, setCurrentRenderRoot, updateInstance,
|
|
17
|
+
releaseInstanceDirectives, setInstanceDirectivesConnected, ROOT_DISCONNECTED,
|
|
17
18
|
} from './parts.js';
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* A top-level list as one child binding. One call site, so its strings are a
|
|
22
|
+
* stable identity and a re-render updates the list in place.
|
|
23
|
+
* @param {unknown} list
|
|
24
|
+
*/
|
|
25
|
+
const rootList = (list) => html`${list}`;
|
|
26
|
+
|
|
19
27
|
/**
|
|
20
28
|
* Render a value into a container, reusing DOM where possible.
|
|
21
29
|
*
|
|
@@ -24,7 +32,48 @@ import {
|
|
|
24
32
|
* @param {{host?: object}} [options]
|
|
25
33
|
*/
|
|
26
34
|
export function render(value, container, options = {}) {
|
|
35
|
+
renderValue(value, container, options);
|
|
36
|
+
return rootPart(container);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @type {WeakMap<object, { setConnected(connected: boolean): void }>} */
|
|
40
|
+
const rootParts = new WeakMap();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* What `render()` returns, lit's `RootPart`: `setConnected(false)` reports
|
|
44
|
+
* every async directive in the container disconnected (and starts the ones a
|
|
45
|
+
* later render creates disconnected), `setConnected(true)` reconnects them.
|
|
46
|
+
* @param {any} container
|
|
47
|
+
*/
|
|
48
|
+
function rootPart(container) {
|
|
49
|
+
let part = rootParts.get(container);
|
|
50
|
+
if (!part) {
|
|
51
|
+
part = {
|
|
52
|
+
setConnected(connected) {
|
|
53
|
+
container[ROOT_DISCONNECTED] = !connected;
|
|
54
|
+
setInstanceDirectivesConnected(container[INSTANCE], connected);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
rootParts.set(container, part);
|
|
58
|
+
}
|
|
59
|
+
return part;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {unknown} value
|
|
64
|
+
* @param {Element | DocumentFragment | ShadowRoot} container
|
|
65
|
+
* @param {{host?: object}} options
|
|
66
|
+
*/
|
|
67
|
+
function renderValue(value, container, options) {
|
|
27
68
|
if (value === noChange) return;
|
|
69
|
+
// Any object that is not a template (an array or other iterable such as a
|
|
70
|
+
// `map()` / `join()` / `range()` generator or a Set, a DOM node, a directive)
|
|
71
|
+
// renders through a child binding, as Lit's `render()` renders every value
|
|
72
|
+
// (#1686). It used to be written as text, "[object Generator]" or
|
|
73
|
+
// "[object Object]".
|
|
74
|
+
if (value !== null && typeof value === 'object' && !isTemplate(value)) {
|
|
75
|
+
value = rootList(value);
|
|
76
|
+
}
|
|
28
77
|
const host = /** @type any */ (container);
|
|
29
78
|
// Open the renderer-write window for the whole commit: every host-receiver
|
|
30
79
|
// write below (and in createInstance / updateInstance / clearInstance / all
|
|
@@ -75,13 +124,6 @@ export function render(value, container, options = {}) {
|
|
|
75
124
|
host[INSTANCE] = null;
|
|
76
125
|
container.replaceChildren();
|
|
77
126
|
if (value == null || value === nothing) return;
|
|
78
|
-
if (Array.isArray(value)) {
|
|
79
|
-
for (const v of value) {
|
|
80
|
-
const text = document.createTextNode(String(v ?? ''));
|
|
81
|
-
container.appendChild(text);
|
|
82
|
-
}
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
127
|
container.appendChild(document.createTextNode(String(value)));
|
|
86
128
|
} finally {
|
|
87
129
|
setCurrentRenderRoot(prevRenderRoot);
|
|
@@ -210,7 +252,7 @@ function reconcileSubmitterAction(
|
|
|
210
252
|
if (!id) {
|
|
211
253
|
// A function that was MEANT as an action still refuses, so a button never
|
|
212
254
|
// silently submits the form's action instead of its own.
|
|
213
|
-
if (typeof value === 'function') assertIdentifiableAction(null, el.localName);
|
|
255
|
+
if (typeof value === 'function') assertIdentifiableAction(null, el.localName, value);
|
|
214
256
|
releaseSubmitterAction(el);
|
|
215
257
|
// The submission attributes the framework supplied go with the identity, or
|
|
216
258
|
// a released button keeps a `formmethod` / `formenctype` SSR does not emit
|
|
@@ -316,7 +358,7 @@ function resolveHoleValue(v) {
|
|
|
316
358
|
*/
|
|
317
359
|
function createInstance(tr, container, hydrating = false) {
|
|
318
360
|
const { templateEl, parts, formActions } = compile(tr);
|
|
319
|
-
const frag = /** @type DocumentFragment */ (templateEl.content
|
|
361
|
+
const frag = /** @type DocumentFragment */ (document.importNode(templateEl.content, true));
|
|
320
362
|
// Bookend markers bound the instance so we can tear it down cleanly.
|
|
321
363
|
const startNode = document.createComment(`${MARKER}s`);
|
|
322
364
|
const endNode = document.createComment(`${MARKER}e`);
|
|
@@ -378,6 +420,9 @@ function clearInstance(inst, container) {
|
|
|
378
420
|
// element parts, and rescue any projected children sitting inside
|
|
379
421
|
// slot parts so they survive teardown of a collapsing conditional
|
|
380
422
|
// fragment.
|
|
423
|
+
// Custom directives in the outgoing template are released first (an async
|
|
424
|
+
// one runs its `disconnected()`), as lit does when a part goes away.
|
|
425
|
+
releaseInstanceDirectives(inst);
|
|
381
426
|
for (const p of inst.bound) {
|
|
382
427
|
if (p.kind === 'event') p.el.removeEventListener(p.name, p.dispatcher, p.handler);
|
|
383
428
|
if (p.kind === 'element') {
|
|
@@ -391,7 +436,7 @@ function clearInstance(inst, container) {
|
|
|
391
436
|
const prev = /** @type any */ (p).lastTarget;
|
|
392
437
|
if (prev) {
|
|
393
438
|
if (typeof prev === 'function') {
|
|
394
|
-
try { prev(undefined); } catch {}
|
|
439
|
+
try { prev.call(/** @type any */ (p).refHost, undefined); } catch {}
|
|
395
440
|
} else if (typeof prev === 'object') {
|
|
396
441
|
try { prev.value = undefined; } catch {}
|
|
397
442
|
}
|
|
@@ -2,6 +2,7 @@ import { MARKER } from '../html.js';
|
|
|
2
2
|
import { BINDING_PREFIXES, isBindingPrefix } from '../binding-prefixes.js';
|
|
3
3
|
import { isSubmitterReflectedProp } from '../form-action.js';
|
|
4
4
|
import { LIGHT_SLOT_ATTR } from '../slot.js';
|
|
5
|
+
import { assertNoJsxIsms } from '../jsx-guard.js';
|
|
5
6
|
|
|
6
7
|
/** @type {WeakMap<TemplateStringsArray | string[], { templateEl: HTMLTemplateElement, parts: PartDescriptor[], formActions: FormActionRecord[] | null }>} */
|
|
7
8
|
export const templateCache = new WeakMap();
|
|
@@ -12,6 +13,12 @@ export const templateCache = new WeakMap();
|
|
|
12
13
|
*/
|
|
13
14
|
export const submitterActionBindings = new WeakMap();
|
|
14
15
|
export const INSTANCE = Symbol.for('webjs.instance');
|
|
16
|
+
/** Elements whose content the HTML parser reads as text, never markup. */
|
|
17
|
+
const RAW_TEXT_TAGS = new Set(['script', 'style', 'textarea', 'title']);
|
|
18
|
+
/** HTML's own whitespace; `\s` would also match U+00A0, which is attribute text. */
|
|
19
|
+
const HTML_WS = /[ \t\n\f\r]/;
|
|
20
|
+
/** Delimits a raw-text hole's index; a private-use character no parser alters. */
|
|
21
|
+
const RAW_SENTINEL = '\uE000';
|
|
15
22
|
|
|
16
23
|
/**
|
|
17
24
|
* @typedef {{
|
|
@@ -37,7 +44,7 @@ export const INSTANCE = Symbol.for('webjs.instance');
|
|
|
37
44
|
* | { kind: 'event', el: Element, name: string, handler: (EventListenerOrEventListenerObject & AddEventListenerOptions) | null, eventHost?: object, dispatcher: (e: Event) => void }
|
|
38
45
|
* | { kind: 'prop', el: Element, name: string }
|
|
39
46
|
* | { kind: 'bool', el: Element, name: string }
|
|
40
|
-
* | { kind: 'element', el: Element, lastTarget?: any }
|
|
47
|
+
* | { kind: 'element', el: Element, lastTarget?: any, refHost?: object }
|
|
41
48
|
* | { kind: 'slot', slotEl: HTMLSlotElement, applied: boolean }
|
|
42
49
|
* | { kind: 'noop' }
|
|
43
50
|
* } BoundPart
|
|
@@ -48,6 +55,14 @@ export function compile(tr) {
|
|
|
48
55
|
const { strings } = tr;
|
|
49
56
|
let cached = templateCache.get(strings);
|
|
50
57
|
if (cached) return cached;
|
|
58
|
+
// Only a tagged template's own strings array carries `raw`, so a template
|
|
59
|
+
// result that arrived as data (JSON with the `_$webjs` brand) is refused
|
|
60
|
+
// rather than parsed as markup, lit's `trustFromTemplateString` check.
|
|
61
|
+
if (!Array.isArray(strings) || !Object.prototype.hasOwnProperty.call(strings, 'raw')) {
|
|
62
|
+
throw new Error('[webjs] refusing to render a template result whose strings are not a tagged template\'s (a TemplateResult built from data, not by html``).');
|
|
63
|
+
}
|
|
64
|
+
// `className=`, `htmlFor=`, `<Link>`: JSX that HTML silently ignores.
|
|
65
|
+
assertNoJsxIsms(strings);
|
|
51
66
|
|
|
52
67
|
/** @type {PartDescriptor[]} */
|
|
53
68
|
const parts = [];
|
|
@@ -60,7 +75,15 @@ export function compile(tr) {
|
|
|
60
75
|
/** @type {{ name: string, firstPartIdx: number } | null} */
|
|
61
76
|
let mixedAttr = null;
|
|
62
77
|
let currentTag = '';
|
|
78
|
+
let closingTag = false;
|
|
63
79
|
let rawTail = '';
|
|
80
|
+
// The content state a start tag's `>` opens: raw text for `script` / `style`
|
|
81
|
+
// / `textarea` / `title`, whose content the HTML parser never reads as markup,
|
|
82
|
+
// whichever attribute state the tag ended in (lit's `rawTextElement`).
|
|
83
|
+
const afterTag = () => {
|
|
84
|
+
rawTail = '';
|
|
85
|
+
return (!closingTag && RAW_TEXT_TAGS.has(currentTag)) ? 'rawtext' : 'text';
|
|
86
|
+
};
|
|
64
87
|
|
|
65
88
|
for (let i = 0; i < strings.length; i++) {
|
|
66
89
|
const s = strings[i];
|
|
@@ -74,18 +97,23 @@ export function compile(tr) {
|
|
|
74
97
|
case 'tag-open':
|
|
75
98
|
html += c;
|
|
76
99
|
if (c === '!') state = 'bang-1';
|
|
77
|
-
else if (c === '/') { state = 'tag-name'; currentTag = ''; }
|
|
78
|
-
else if (/[a-zA-Z]/.test(c)) { state = 'tag-name'; currentTag = c.toLowerCase(); }
|
|
100
|
+
else if (c === '/') { state = 'tag-name'; currentTag = ''; closingTag = true; }
|
|
101
|
+
else if (/[a-zA-Z]/.test(c)) { state = 'tag-name'; currentTag = c.toLowerCase(); closingTag = false; }
|
|
79
102
|
else state = 'text';
|
|
80
103
|
break;
|
|
81
104
|
case 'bang-1':
|
|
82
105
|
html += c;
|
|
83
|
-
state = c === '-' ? 'bang-dash' : '
|
|
106
|
+
state = c === '-' ? 'bang-dash' : 'bogus';
|
|
84
107
|
break;
|
|
85
108
|
case 'bang-dash':
|
|
86
109
|
html += c;
|
|
87
110
|
if (c === '-') { state = 'comment'; commentDashes = 0; }
|
|
88
|
-
else state = '
|
|
111
|
+
else state = 'bogus';
|
|
112
|
+
break;
|
|
113
|
+
case 'bogus':
|
|
114
|
+
// A bogus comment (`<!x ...>`, `</ ...>`) runs to the next `>`.
|
|
115
|
+
html += c;
|
|
116
|
+
if (c === '>') state = 'text';
|
|
89
117
|
break;
|
|
90
118
|
case 'comment':
|
|
91
119
|
html += c;
|
|
@@ -95,18 +123,15 @@ export function compile(tr) {
|
|
|
95
123
|
break;
|
|
96
124
|
case 'tag-name':
|
|
97
125
|
html += c;
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
} else if (/\s/.test(c)) state = 'in-tag';
|
|
126
|
+
if (closingTag && currentTag === '' && c !== '>' && !/[a-zA-Z]/.test(c)) state = 'bogus';
|
|
127
|
+
else if (c === '>') state = afterTag();
|
|
128
|
+
else if (HTML_WS.test(c)) state = 'in-tag';
|
|
102
129
|
else currentTag += c.toLowerCase();
|
|
103
130
|
break;
|
|
104
131
|
case 'in-tag':
|
|
105
132
|
html += c;
|
|
106
|
-
if (c === '>')
|
|
107
|
-
|
|
108
|
-
if (state === 'rawtext') rawTail = '';
|
|
109
|
-
} else if (!/\s/.test(c) && c !== '/') {
|
|
133
|
+
if (c === '>') state = afterTag();
|
|
134
|
+
else if (!HTML_WS.test(c) && c !== '/') {
|
|
110
135
|
state = 'attr-name';
|
|
111
136
|
attrName = c;
|
|
112
137
|
attrStart = html.length - 1;
|
|
@@ -114,8 +139,10 @@ export function compile(tr) {
|
|
|
114
139
|
break;
|
|
115
140
|
case 'rawtext':
|
|
116
141
|
html += c;
|
|
117
|
-
|
|
118
|
-
|
|
142
|
+
// Only the element's OWN end tag closes it: a `</style>` inside a
|
|
143
|
+
// `<script>` is script text.
|
|
144
|
+
rawTail = (rawTail + c.toLowerCase()).slice(-(currentTag.length + 3));
|
|
145
|
+
if (rawTail === `</${currentTag}>`) {
|
|
119
146
|
state = 'text';
|
|
120
147
|
rawTail = '';
|
|
121
148
|
currentTag = '';
|
|
@@ -123,19 +150,28 @@ export function compile(tr) {
|
|
|
123
150
|
break;
|
|
124
151
|
case 'attr-name':
|
|
125
152
|
if (c === '=') { state = 'after-eq'; html += c; }
|
|
126
|
-
else if (
|
|
127
|
-
else if (c === '>') { state =
|
|
153
|
+
else if (HTML_WS.test(c)) { state = 'after-name'; html += c; }
|
|
154
|
+
else if (c === '>') { state = afterTag(); attrName = ''; html += c; }
|
|
128
155
|
else { attrName += c; html += c; }
|
|
129
156
|
break;
|
|
157
|
+
case 'after-name':
|
|
158
|
+
// Whitespace between a name and its `=` (`abc = ${x}`), as the HTML
|
|
159
|
+
// tokenizer allows.
|
|
160
|
+
html += c;
|
|
161
|
+
if (c === '=') state = 'after-eq';
|
|
162
|
+
else if (c === '>') { state = afterTag(); attrName = ''; }
|
|
163
|
+
else if (c === '/') { state = 'in-tag'; attrName = ''; }
|
|
164
|
+
else if (!HTML_WS.test(c)) { state = 'attr-name'; attrName = c; attrStart = html.length - 1; }
|
|
165
|
+
break;
|
|
130
166
|
case 'after-eq':
|
|
131
167
|
if (c === '"' || c === "'") { state = 'attr-quoted'; attrQuote = c; html += c; }
|
|
132
|
-
else if (
|
|
133
|
-
else if (c === '>') { state =
|
|
168
|
+
else if (HTML_WS.test(c)) { html += c; }
|
|
169
|
+
else if (c === '>') { state = afterTag(); attrName = ''; html += c; }
|
|
134
170
|
else { state = 'attr-unquoted'; html += c; }
|
|
135
171
|
break;
|
|
136
172
|
case 'attr-unquoted':
|
|
137
|
-
if (
|
|
138
|
-
else if (c === '>') { state =
|
|
173
|
+
if (HTML_WS.test(c)) { state = 'in-tag'; attrName = ''; html += c; }
|
|
174
|
+
else if (c === '>') { state = afterTag(); attrName = ''; html += c; }
|
|
139
175
|
else html += c;
|
|
140
176
|
break;
|
|
141
177
|
case 'attr-quoted':
|
|
@@ -144,8 +180,10 @@ export function compile(tr) {
|
|
|
144
180
|
break;
|
|
145
181
|
case 'skip-attr':
|
|
146
182
|
// Consume mixed-attribute chars without appending to html.
|
|
147
|
-
// The attribute was replaced with a sentinel on the first hole.
|
|
148
|
-
|
|
183
|
+
// The attribute was replaced with a sentinel on the first hole. An
|
|
184
|
+
// UNQUOTED value (`attrQuote` empty, `a=x${y}` or `a=${x}${y}`) ends
|
|
185
|
+
// at whitespace, `>` or `/>`, as the HTML tokenizer ends it.
|
|
186
|
+
if (attrQuote === '' ? (HTML_WS.test(c) || c === '>' || (c === '/' && s[j + 1] === '>')) : c === attrQuote) {
|
|
149
187
|
// Closing quote: finalize the attr-mixed part.
|
|
150
188
|
if (mixedAttr) {
|
|
151
189
|
const idx0 = mixedAttr.firstPartIdx;
|
|
@@ -160,13 +198,17 @@ export function compile(tr) {
|
|
|
160
198
|
// statics[n] = prefix of strings[last+1] up to closing quote
|
|
161
199
|
const statics = [];
|
|
162
200
|
const s0 = strings[group[0]];
|
|
163
|
-
|
|
164
|
-
|
|
201
|
+
if (attrQuote === '') {
|
|
202
|
+
statics.push(s0.slice(s0.lastIndexOf('=') + 1).trimStart());
|
|
203
|
+
} else {
|
|
204
|
+
const qp = s0.lastIndexOf(attrQuote);
|
|
205
|
+
statics.push(qp >= 0 ? s0.slice(qp + 1) : s0);
|
|
206
|
+
}
|
|
165
207
|
for (let k = 1; k < group.length; k++) {
|
|
166
208
|
statics.push(strings[group[k]]);
|
|
167
209
|
}
|
|
168
210
|
const sLast = strings[group[group.length - 1] + 1];
|
|
169
|
-
const eq = sLast.indexOf(attrQuote);
|
|
211
|
+
const eq = attrQuote === '' ? j : sLast.indexOf(attrQuote);
|
|
170
212
|
statics.push(eq >= 0 ? sLast.slice(0, eq) : sLast);
|
|
171
213
|
|
|
172
214
|
const specialized = isBindingPrefix(mixedAttr.name[0]);
|
|
@@ -191,8 +233,11 @@ export function compile(tr) {
|
|
|
191
233
|
}
|
|
192
234
|
mixedAttr = null;
|
|
193
235
|
}
|
|
194
|
-
state = 'in-tag';
|
|
195
236
|
attrName = '';
|
|
237
|
+
// A quoted value's closing quote was consumed with it; an unquoted
|
|
238
|
+
// value's terminator is markup and is kept.
|
|
239
|
+
if (attrQuote === '') html += c;
|
|
240
|
+
state = (attrQuote === '' && c === '>') ? afterTag() : 'in-tag';
|
|
196
241
|
}
|
|
197
242
|
break;
|
|
198
243
|
}
|
|
@@ -209,28 +254,38 @@ export function compile(tr) {
|
|
|
209
254
|
continue;
|
|
210
255
|
}
|
|
211
256
|
if (state === 'rawtext') {
|
|
212
|
-
// Inside <script
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
257
|
+
// Inside <script> / <style> / <textarea> / <title> the parser reads a
|
|
258
|
+
// comment marker as text, so the hole leaves a text sentinel that
|
|
259
|
+
// `splitRawTextSentinels` turns into a marker comment after parsing.
|
|
260
|
+
// It is then an ordinary child part committing text nodes, which is
|
|
261
|
+
// what lit does and what SSR's raw-text hole emits.
|
|
216
262
|
rawTail = '';
|
|
217
|
-
|
|
263
|
+
html += `${RAW_SENTINEL}${partIdx}${RAW_SENTINEL}`;
|
|
264
|
+
parts.push({ kind: 'child', path: [] });
|
|
218
265
|
continue;
|
|
219
266
|
}
|
|
267
|
+
// An unquoted value that goes on after its hole (`a=${x}${y}`,
|
|
268
|
+
// `a=${x}px`) is interpolated, like a quoted one.
|
|
269
|
+
const next = strings[i + 1];
|
|
270
|
+
const unquotedGoesOn = state === 'after-eq'
|
|
271
|
+
&& (next === '' ? i + 1 < strings.length - 1 : !/^([ \t\n\f\r]|>|\/>)/.test(next));
|
|
220
272
|
if (state === 'text') {
|
|
221
273
|
// Child hole: insert a comment marker. Use bracketed markers so we can
|
|
222
274
|
// later walk all comments and find ours without ambiguity.
|
|
223
275
|
html += `<!--${MARKER}${partIdx}-->`;
|
|
224
276
|
parts.push({ kind: 'child', path: [] });
|
|
225
|
-
} else if (state === 'in-tag') {
|
|
226
|
-
// Element-position hole: `<tag ${expr}
|
|
277
|
+
} else if (state === 'in-tag' || state === 'after-name') {
|
|
278
|
+
// Element-position hole: `<tag ${expr}>`, also after a bare attribute
|
|
279
|
+
// (`<input disabled ${expr}>`). Used by the `ref` directive
|
|
227
280
|
// (and any future element-bound directive). Emit a sentinel attribute
|
|
228
281
|
// on the current open tag; at bind time the attribute is stripped
|
|
229
282
|
// and the element is captured into the part.
|
|
230
283
|
const sentinel = `data-${MARKER}${partIdx}`;
|
|
231
284
|
html += `${sentinel}=""`;
|
|
232
285
|
parts.push({ kind: 'element', path: [] });
|
|
233
|
-
|
|
286
|
+
state = 'in-tag';
|
|
287
|
+
attrName = '';
|
|
288
|
+
} else if (state === 'after-eq' && !unquotedGoesOn) {
|
|
234
289
|
const prefix = attrName[0];
|
|
235
290
|
const name = attrName.slice(1);
|
|
236
291
|
if (isBindingPrefix(prefix)) {
|
|
@@ -249,35 +304,43 @@ export function compile(tr) {
|
|
|
249
304
|
}
|
|
250
305
|
state = 'in-tag';
|
|
251
306
|
attrName = '';
|
|
252
|
-
} else if (state === 'attr-quoted' || state === 'attr-unquoted') {
|
|
307
|
+
} else if (state === 'attr-quoted' || state === 'attr-unquoted' || state === 'after-eq') {
|
|
253
308
|
// First hole inside a quoted attribute value: start mixed-attr tracking.
|
|
254
309
|
// Replace the entire attribute with a sentinel (same as regular attr).
|
|
310
|
+
if (state !== 'attr-quoted') attrQuote = '';
|
|
255
311
|
html = html.slice(0, attrStart);
|
|
256
312
|
const sentinel = `data-${MARKER}${partIdx}`;
|
|
257
313
|
html += `${sentinel}=""`;
|
|
258
314
|
mixedAttr = { name: attrName, firstPartIdx: partIdx };
|
|
259
315
|
parts.push({ kind: 'noop', path: [] }); // patched to attr-mixed at close-quote
|
|
260
316
|
state = 'skip-attr';
|
|
261
|
-
} else
|
|
262
|
-
//
|
|
317
|
+
} else {
|
|
318
|
+
// `skip-attr`: a later hole in the same mixed attribute. Any other
|
|
319
|
+
// state is a hole where no binding can sit (a tag name, `</div ${x}>`):
|
|
320
|
+
// it binds nothing, as in lit's production build, and still takes its
|
|
321
|
+
// index so every later part keeps its own.
|
|
263
322
|
parts.push({ kind: 'noop', path: [] });
|
|
264
323
|
}
|
|
265
324
|
}
|
|
266
325
|
}
|
|
267
326
|
|
|
268
327
|
const templateEl = document.createElement('template');
|
|
269
|
-
if (tr.svg) {
|
|
270
|
-
// An svg`` fragment is parsed inside an <svg>
|
|
271
|
-
// land in
|
|
272
|
-
// slot and path walks, so every recorded path is
|
|
273
|
-
// fragment's own root, as it is for an html`` template
|
|
274
|
-
|
|
328
|
+
if (tr.svg || tr.mathml) {
|
|
329
|
+
// An svg`` (or mathml``) fragment is parsed inside an <svg> (or <math>)
|
|
330
|
+
// wrapper so its elements land in that namespace, then the wrapper is
|
|
331
|
+
// unwrapped BEFORE the slot and path walks, so every recorded path is
|
|
332
|
+
// relative to the fragment's own root, as it is for an html`` template
|
|
333
|
+
// (lit does the same).
|
|
334
|
+
const wrapTag = tr.svg ? 'svg' : 'math';
|
|
335
|
+
templateEl.innerHTML = `<${wrapTag}>${html}</${wrapTag}>`;
|
|
275
336
|
const wrapper = /** @type {Element} */ (templateEl.content.firstChild);
|
|
276
337
|
wrapper.replaceWith(...wrapper.childNodes);
|
|
277
338
|
} else {
|
|
278
339
|
templateEl.innerHTML = html;
|
|
279
340
|
}
|
|
280
341
|
|
|
342
|
+
if (html.includes(RAW_SENTINEL)) splitRawTextSentinels(templateEl.content);
|
|
343
|
+
|
|
281
344
|
// Mark every <slot> in the template for framework projection and
|
|
282
345
|
// register a SLOT part for each so the slot-apply step can find them on
|
|
283
346
|
// clones. This runs BEFORE assignPaths so the sentinel attributes the
|
|
@@ -295,6 +358,38 @@ export function compile(tr) {
|
|
|
295
358
|
return cached;
|
|
296
359
|
}
|
|
297
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Replace each raw-text hole's sentinel with a `wjm-<idx>` marker comment, so
|
|
363
|
+
* the path walk records it as a child part. A comment inside `<script>` or
|
|
364
|
+
* `<textarea>` is a DOM child the element's text (`textContent`, `.text`,
|
|
365
|
+
* `.value`) ignores, which is how lit anchors a raw-text binding too.
|
|
366
|
+
*
|
|
367
|
+
* @param {DocumentFragment} root
|
|
368
|
+
*/
|
|
369
|
+
function splitRawTextSentinels(root) {
|
|
370
|
+
/** @type {Text[]} */
|
|
371
|
+
const hits = [];
|
|
372
|
+
/** @param {Node} node */
|
|
373
|
+
const collect = (node) => {
|
|
374
|
+
for (const child of node.childNodes) {
|
|
375
|
+
if (child.nodeType === 3) { if (/** @type {Text} */ (child).data.includes(RAW_SENTINEL)) hits.push(/** @type {Text} */ (child)); }
|
|
376
|
+
else collect(child);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
collect(root);
|
|
380
|
+
for (const text of hits) {
|
|
381
|
+
const pieces = text.data.split(RAW_SENTINEL);
|
|
382
|
+
/** @type {Node[]} */
|
|
383
|
+
const nodes = [];
|
|
384
|
+
// pieces alternate text, index, text, index, ..., text
|
|
385
|
+
for (let k = 0; k < pieces.length; k++) {
|
|
386
|
+
if (k % 2) nodes.push(document.createComment(`${MARKER}${pieces[k]}`));
|
|
387
|
+
else if (pieces[k]) nodes.push(document.createTextNode(pieces[k]));
|
|
388
|
+
}
|
|
389
|
+
text.replaceWith(...nodes);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
298
393
|
/**
|
|
299
394
|
* Walk the compiled template content for <slot> elements (the static ones
|
|
300
395
|
* written into the template, not dynamically-inserted ones). For each:
|
package/src/render-client.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export interface RenderOptions { host?: object; }
|
|
2
|
-
|
|
2
|
+
/** What `render()` returns, lit's `RootPart`: `setConnected(false)` disconnects the async directives the render created, `setConnected(true)` reconnects them. */
|
|
3
|
+
export interface RootPart {
|
|
4
|
+
setConnected(isConnected: boolean): void;
|
|
5
|
+
}
|
|
6
|
+
export function render(value: unknown, container: Element | DocumentFragment | ShadowRoot, options?: RenderOptions): RootPart;
|
|
@@ -67,7 +67,7 @@ export function withHostMarker(opening) {
|
|
|
67
67
|
* `getAttribute` API so it works whether the instance is the server shim or a
|
|
68
68
|
* real `HTMLElement`. Returns the opening tag unchanged when there is nothing
|
|
69
69
|
* to add, so existing SSR output stays byte-identical when no component
|
|
70
|
-
* reflects
|
|
70
|
+
* reflects.
|
|
71
71
|
*
|
|
72
72
|
* @param {string} opening the element's opening tag, ending in `>`
|
|
73
73
|
* @param {any} instance
|
|
@@ -129,7 +129,7 @@ export function applyAttrsToInstance(instance, attrs, Cls) {
|
|
|
129
129
|
// per-component error isolation below, which is deliberate: an author who
|
|
130
130
|
// supplies a converter owns the read, the same rule `_reflectAttribute`
|
|
131
131
|
// states for `toAttribute`. See the comment on `readAttributeValue`.
|
|
132
|
-
instance[propName] = readAttributeValue(def, decodeAttrEntities(sourceValue));
|
|
132
|
+
instance[propName] = readAttributeValue(def, decodeAttrEntities(sourceValue), propName);
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
@@ -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} */
|
|
@@ -323,6 +329,18 @@ function tokenize(strings) {
|
|
|
323
329
|
else chunk += c;
|
|
324
330
|
break;
|
|
325
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
|
+
}
|
|
326
344
|
chunk += c;
|
|
327
345
|
if (c === attrQuote) { state = 'in-tag'; attrName = ''; }
|
|
328
346
|
break;
|
|
@@ -368,6 +386,22 @@ function tokenize(strings) {
|
|
|
368
386
|
}
|
|
369
387
|
|
|
370
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
|
+
}
|
|
371
405
|
// A value hole sits here. Its op says where, and the walker decides what.
|
|
372
406
|
if (state === 'comment' || state === 'comment-start' || state === 'comment-start-dash' || state === 'comment-bang') {
|
|
373
407
|
flush();
|
|
@@ -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.
|