@webjsdev/core 0.7.47 → 0.7.49
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 +3 -2
- package/dist/webjs-core-browser.js +5 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +21 -21
- package/dist/webjs-core-testing.js.map +2 -2
- package/dist/webjs-core.js +7 -7
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +13 -5
- package/index.d.ts +2 -2
- package/index.js +2 -2
- package/package.json +1 -1
- package/src/action-seed-client.d.ts +23 -1
- package/src/action-seed-client.js +359 -17
- package/src/component.d.ts +7 -3
- package/src/component.js +94 -2
- package/src/form-action.js +204 -203
- package/src/render-client.js +94 -96
- package/src/render-server.js +100 -118
- package/src/router-client.js +644 -21
- package/src/websocket-client.js +5 -2
package/src/render-server.js
CHANGED
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
assertNotFunctionActionAttr, assertNotFunctionReflectedActionProp,
|
|
6
6
|
assertIdentifiableAction, bindFormActionStartTag, isBoundFormAction, resolveFormActionId,
|
|
7
7
|
assertConvergentBoundForm, assertSubmitterHasNoName, assertSubmitterHasNoValue,
|
|
8
|
-
|
|
9
|
-
assertSingleSubmitterAction,
|
|
8
|
+
assertSubmitterHasNoFormAttribute,
|
|
9
|
+
assertSingleSubmitterAction, bindSubmitterStartTag, parseStartTagAttrs,
|
|
10
10
|
isSubmitterReflectedProp, FORM_ACTION_FIELD,
|
|
11
11
|
} from './form-action.js';
|
|
12
12
|
import { lookup, lookupModuleUrl, allTags } from './registry.js';
|
|
@@ -31,19 +31,18 @@ import { cspNonce } from './csp-nonce.js';
|
|
|
31
31
|
* Suspense still works but we fall back to emitting only the fallback
|
|
32
32
|
* (the promise is dropped: appropriate for static pre-render).
|
|
33
33
|
*
|
|
34
|
-
* @typedef {{ pending: {id: string, promise: Promise<unknown
|
|
34
|
+
* @typedef {{ pending: {id: string, promise: Promise<unknown>}[], nextId: number }} SuspenseCtx
|
|
35
35
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* would silently disappear from a page that still returned 200.
|
|
36
|
+
* A boundary's enclosing form scope used to be recorded here and threaded back
|
|
37
|
+
* in (#1207), because the page pipeline in `@webjsdev/server` drains
|
|
38
|
+
* `ctx.pending` and re-renders each resolved child through a FRESH scan with no
|
|
39
|
+
* view of the shell it belongs to, so a `<button formaction=${fn}>` inside a
|
|
40
|
+
* bound form's Suspense boundary read as form-less and was refused. Gone with
|
|
41
|
+
* #1307: a bound submitter carries its own `formmethod` and enctype, so no
|
|
42
|
+
* renderer needs to know what encloses it and there is nothing left to thread.
|
|
44
43
|
*
|
|
45
44
|
* @param {unknown} value
|
|
46
|
-
* @param {{ ssr?: boolean, suspenseCtx?: SuspenseCtx, dev?: boolean
|
|
45
|
+
* @param {{ ssr?: boolean, suspenseCtx?: SuspenseCtx, dev?: boolean }} [opts]
|
|
47
46
|
* @returns {Promise<string>}
|
|
48
47
|
*/
|
|
49
48
|
export async function renderToString(value, opts = { ssr: true }) {
|
|
@@ -54,23 +53,20 @@ export async function renderToString(value, opts = { ssr: true }) {
|
|
|
54
53
|
// sharing it see the same flag. Undefined stays undefined (NODE_ENV fallback).
|
|
55
54
|
const dev = opts && opts.dev !== undefined ? opts.dev : ctx && ctx.dev;
|
|
56
55
|
if (ctx && ctx.dev === undefined && dev !== undefined) ctx.dev = dev;
|
|
57
|
-
|
|
58
|
-
// and 'none' is the honest answer: there is conclusively no enclosing form.
|
|
59
|
-
const html = await render(value, ctx, (opts && opts.formScope) || 'none');
|
|
56
|
+
const html = await render(value, ctx);
|
|
60
57
|
return opts && opts.ssr === false ? html : await injectDSD(html, ctx, [], dev);
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
/**
|
|
64
61
|
* @param {unknown} value
|
|
65
62
|
* @param {SuspenseCtx} [ctx]
|
|
66
|
-
* @param {'none'|'unbound'|'bound'|'unknown'} [formScope] enclosing form scope, as seen from THIS scan
|
|
67
63
|
* @returns {Promise<string>}
|
|
68
64
|
*/
|
|
69
|
-
async function render(value, ctx
|
|
65
|
+
async function render(value, ctx) {
|
|
70
66
|
if (value == null || value === false || value === true) return '';
|
|
71
67
|
if (value && typeof /** @type any */ (value).then === 'function') {
|
|
72
68
|
value = await value;
|
|
73
|
-
return render(value, ctx
|
|
69
|
+
return render(value, ctx);
|
|
74
70
|
}
|
|
75
71
|
// unsafeHTML: inject raw HTML string without escaping.
|
|
76
72
|
if (isUnsafeHTML(value)) {
|
|
@@ -78,21 +74,21 @@ async function render(value, ctx, formScope = 'none') {
|
|
|
78
74
|
}
|
|
79
75
|
// live() on the server just unwraps and renders the inner value.
|
|
80
76
|
if (isLive(value)) {
|
|
81
|
-
return render(/** @type any */ (value).value, ctx
|
|
77
|
+
return render(/** @type any */ (value).value, ctx);
|
|
82
78
|
}
|
|
83
79
|
// watch() on the server reads the signal once and inlines the
|
|
84
80
|
// result. Subscription is a client-only concern; the SSR HTML
|
|
85
81
|
// freezes a snapshot of the current value.
|
|
86
82
|
if (isWatch(value)) {
|
|
87
|
-
return render(/** @type any */ (value).signal.get(), ctx
|
|
83
|
+
return render(/** @type any */ (value).signal.get(), ctx);
|
|
88
84
|
}
|
|
89
85
|
// keyed() on the server: render the wrapped template; key is client-only.
|
|
90
86
|
if (isKeyed(value)) {
|
|
91
|
-
return render(/** @type any */ (value).value, ctx
|
|
87
|
+
return render(/** @type any */ (value).value, ctx);
|
|
92
88
|
}
|
|
93
89
|
// guard() on the server: always invoke the value function (no cache on SSR).
|
|
94
90
|
if (isGuard(value)) {
|
|
95
|
-
return render(/** @type any */ (value).fn(), ctx
|
|
91
|
+
return render(/** @type any */ (value).fn(), ctx);
|
|
96
92
|
}
|
|
97
93
|
// templateContent() on the server: emit the template's innerHTML verbatim.
|
|
98
94
|
if (isTemplateContent(value)) {
|
|
@@ -105,7 +101,7 @@ async function render(value, ctx, formScope = 'none') {
|
|
|
105
101
|
}
|
|
106
102
|
// cache() on the server: pass-through to the inner value.
|
|
107
103
|
if (isCache(value)) {
|
|
108
|
-
return render(/** @type any */ (value).value, ctx
|
|
104
|
+
return render(/** @type any */ (value).value, ctx);
|
|
109
105
|
}
|
|
110
106
|
// until() on the server: render the first synchronous candidate, or
|
|
111
107
|
// await the first Promise to settle when all candidates are Promises.
|
|
@@ -115,13 +111,13 @@ async function render(value, ctx, formScope = 'none') {
|
|
|
115
111
|
const args = /** @type any */ (value).args;
|
|
116
112
|
for (const a of args) {
|
|
117
113
|
if (!a || typeof (/** @type any */ (a).then) !== 'function') {
|
|
118
|
-
return render(a, ctx
|
|
114
|
+
return render(a, ctx);
|
|
119
115
|
}
|
|
120
116
|
}
|
|
121
117
|
if (args.length > 0) {
|
|
122
118
|
try {
|
|
123
119
|
const winner = await Promise.race(args.map((p) => Promise.resolve(p).catch(() => undefined)));
|
|
124
|
-
return render(winner, ctx
|
|
120
|
+
return render(winner, ctx);
|
|
125
121
|
} catch {
|
|
126
122
|
return '';
|
|
127
123
|
}
|
|
@@ -134,35 +130,34 @@ async function render(value, ctx, formScope = 'none') {
|
|
|
134
130
|
return '';
|
|
135
131
|
}
|
|
136
132
|
if (Array.isArray(value)) {
|
|
137
|
-
const parts = await Promise.all(value.map((v) => render(v, ctx
|
|
133
|
+
const parts = await Promise.all(value.map((v) => render(v, ctx)));
|
|
138
134
|
return parts.join('');
|
|
139
135
|
}
|
|
140
136
|
if (isRepeat(value)) {
|
|
141
137
|
const r = /** @type any */ (value);
|
|
142
|
-
const parts = await Promise.all(r.items.map((it, i) => render(r.templateFn(it, i), ctx
|
|
138
|
+
const parts = await Promise.all(r.items.map((it, i) => render(r.templateFn(it, i), ctx)));
|
|
143
139
|
return parts.join('');
|
|
144
140
|
}
|
|
145
141
|
if (isSuspense(value)) {
|
|
146
142
|
const s = /** @type any */ (value);
|
|
147
|
-
const fallback = await render(s.fallback, ctx
|
|
143
|
+
const fallback = await render(s.fallback, ctx);
|
|
148
144
|
if (ctx) {
|
|
149
145
|
const id = `s${ctx.nextId++}`;
|
|
150
|
-
ctx.pending.push({ id, promise: Promise.resolve(s.children)
|
|
146
|
+
ctx.pending.push({ id, promise: Promise.resolve(s.children) });
|
|
151
147
|
return `<webjs-boundary id="${id}">${fallback}</webjs-boundary>`;
|
|
152
148
|
}
|
|
153
149
|
return fallback;
|
|
154
150
|
}
|
|
155
|
-
if (isTemplate(value)) return renderTemplate(/** @type any */ (value), ctx
|
|
151
|
+
if (isTemplate(value)) return renderTemplate(/** @type any */ (value), ctx);
|
|
156
152
|
return escapeText(String(value));
|
|
157
153
|
}
|
|
158
154
|
|
|
159
155
|
/**
|
|
160
156
|
* @param {import('./html.js').TemplateResult} tr
|
|
161
157
|
* @param {SuspenseCtx} [ctx]
|
|
162
|
-
* @param {'none'|'unbound'|'bound'|'unknown'} [formScopeAtStart]
|
|
163
158
|
* @returns {Promise<string>}
|
|
164
159
|
*/
|
|
165
|
-
async function renderTemplate(tr, ctx
|
|
160
|
+
async function renderTemplate(tr, ctx) {
|
|
166
161
|
const { strings, values } = tr;
|
|
167
162
|
let out = '';
|
|
168
163
|
let state = 'text';
|
|
@@ -202,9 +197,6 @@ async function renderTemplate(tr, ctx, formScopeAtStart = 'none') {
|
|
|
202
197
|
// and for the same reason. A top-level scan that simply contains no form
|
|
203
198
|
// stays 'none' and is still refused, because there the answer IS known.
|
|
204
199
|
//
|
|
205
|
-
// Seeded from the caller so a nested template rendered into a form's children
|
|
206
|
-
// inherits the scope, since each template is its own scan.
|
|
207
|
-
let formScope = formScopeAtStart;
|
|
208
200
|
let isCloseTag = false;
|
|
209
201
|
|
|
210
202
|
// A bound `action=${fn}` is committed at its hole, but the edits it implies
|
|
@@ -229,21 +221,15 @@ async function renderTemplate(tr, ctx, formScopeAtStart = 'none') {
|
|
|
229
221
|
const bound = bindFormActionStartTag(out.slice(tagStart), pendingActionId);
|
|
230
222
|
out = out.slice(0, tagStart) + bound.tag + bound.hidden;
|
|
231
223
|
pendingActionId = null;
|
|
232
|
-
formScope = 'bound';
|
|
233
224
|
}
|
|
234
|
-
// A form that opened and bound NOTHING still opens a scope: a submitter
|
|
235
|
-
// inside it must be refused, which is a different answer from 'none'.
|
|
236
|
-
if (!isCloseTag && currentTag === 'form' && formScope !== 'bound') formScope = 'unbound';
|
|
237
225
|
if (submitterTag != null) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
// judge, and under 'none' / 'unbound' there is no binding to defeat.
|
|
246
|
-
assertSubmitterStartTag(out.slice(tagStart), currentTag, { bound: false, propAttrs: submitterProps });
|
|
226
|
+
// #1307: a bound submitter carries its WHOLE submission, so `formmethod`
|
|
227
|
+
// and the enctype are injected onto the button here rather than inherited
|
|
228
|
+
// from a form this scan may not even be able to see. That is what removed
|
|
229
|
+
// the enclosing-form question, and with it the four-state scope tracking
|
|
230
|
+
// that could never answer it for a button inside a component.
|
|
231
|
+
out = out.slice(0, tagStart)
|
|
232
|
+
+ bindSubmitterStartTag(out.slice(tagStart), submitterTag, { duplicateAction, propAttrs: submitterProps });
|
|
247
233
|
}
|
|
248
234
|
};
|
|
249
235
|
// #1155: a `.method` / `.enctype` / `.encoding` prop on a form is dropped
|
|
@@ -275,8 +261,7 @@ async function renderTemplate(tr, ctx, formScopeAtStart = 'none') {
|
|
|
275
261
|
};
|
|
276
262
|
|
|
277
263
|
// Every `>` in a tag state funnels through here, so the bound-form bookkeeping
|
|
278
|
-
// stays in one place rather than at five call sites.
|
|
279
|
-
// scope: a submitter after it is outside.
|
|
264
|
+
// stays in one place rather than at five call sites.
|
|
280
265
|
//
|
|
281
266
|
// `allowRawtext` is NOT a preference. Only two of those five call sites ever
|
|
282
267
|
// entered rawtext: the `tag-name` and `in-tag` exits. The three attribute
|
|
@@ -289,14 +274,6 @@ async function renderTemplate(tr, ctx, formScopeAtStart = 'none') {
|
|
|
289
274
|
// question from #1207; this preserves it exactly.
|
|
290
275
|
const handleTagEnd = (allowRawtext) => {
|
|
291
276
|
closeBoundFormTag();
|
|
292
|
-
if (isCloseTag && currentTag === 'form') {
|
|
293
|
-
// Back to the scope this scan STARTED in, not a flat 'none'. Under an
|
|
294
|
-
// 'unknown' seed (the component pass) the scan never learned whether the
|
|
295
|
-
// host page has a bound form, and closing a form of its own teaches it
|
|
296
|
-
// nothing about that, so asserting 'none' here would downgrade a
|
|
297
|
-
// cannot-tell into a conclusive refusal.
|
|
298
|
-
formScope = formScopeAtStart === 'unknown' ? 'unknown' : 'none';
|
|
299
|
-
}
|
|
300
277
|
isCloseTag = false;
|
|
301
278
|
state = allowRawtext && isRawtextTag(currentTag) ? 'rawtext' : 'text';
|
|
302
279
|
if (state === 'rawtext') rawTail = '';
|
|
@@ -401,7 +378,7 @@ async function renderTemplate(tr, ctx, formScopeAtStart = 'none') {
|
|
|
401
378
|
out += String(val ?? '');
|
|
402
379
|
rawTail = '';
|
|
403
380
|
} else if (state === 'text') {
|
|
404
|
-
out += await render(val, ctx
|
|
381
|
+
out += await render(val, ctx);
|
|
405
382
|
} else if (state === 'after-eq') {
|
|
406
383
|
const prefix = attrName[0];
|
|
407
384
|
const name = attrName.slice(1);
|
|
@@ -423,14 +400,17 @@ async function renderTemplate(tr, ctx, formScopeAtStart = 'none') {
|
|
|
423
400
|
// page-level `.prop` on a native element could have set the
|
|
424
401
|
// property to begin with.
|
|
425
402
|
out = out.slice(0, attrStart);
|
|
426
|
-
// `<webjs-suspense .fallback=${html`...`}>` (#471).
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
403
|
+
// `<webjs-suspense .fallback=${html`...`}>` (#471). This element is
|
|
404
|
+
// defined only in the browser, so the injectDSD walk skips it
|
|
405
|
+
// (`lookup(tag)` finds no class) and no server-side instance runs
|
|
406
|
+
// consumePropAttrs. A normal data-webjs-prop-* binding would then
|
|
407
|
+
// land at connectedCallback, too late for the streaming placeholder.
|
|
408
|
+
// So render the fallback to HTML now and carry it as
|
|
409
|
+
// data-webjs-fallback, which the injectDSD streaming pre-pass reads
|
|
410
|
+
// as the boundary placeholder. (The value itself would serialize
|
|
411
|
+
// fine: a TemplateResult is a plain {strings, values} object.)
|
|
432
412
|
if (currentTag === 'webjs-suspense' && name === 'fallback') {
|
|
433
|
-
const fbHtml = await render(val, ctx
|
|
413
|
+
const fbHtml = await render(val, ctx);
|
|
434
414
|
out += `data-webjs-fallback="${escapeAttr(fbHtml)}"`;
|
|
435
415
|
state = 'in-tag';
|
|
436
416
|
attrName = '';
|
|
@@ -516,11 +496,6 @@ async function renderTemplate(tr, ctx, formScopeAtStart = 'none') {
|
|
|
516
496
|
// author gets the duplicate message rather than a confusing
|
|
517
497
|
// complaint about the `name` the FIRST hole just injected.
|
|
518
498
|
assertSingleSubmitterAction(pendingSubmitterTag != null, currentTag);
|
|
519
|
-
// Only 'unknown' defers: a component renders its own template in a
|
|
520
|
-
// separate pass that cannot see the enclosing form, so the question
|
|
521
|
-
// is not this scan's to answer. 'none' and 'unbound' are known
|
|
522
|
-
// answers and are still refused.
|
|
523
|
-
if (formScope !== 'unknown') assertSubmitterFormIsBound(formScope === 'bound', currentTag);
|
|
524
499
|
const attrs = parseStartTagAttrs(out.slice(tagStart));
|
|
525
500
|
if (attrs.has('name')) assertSubmitterHasNoName(attrs.get('name') || '', currentTag, false);
|
|
526
501
|
if (attrs.has('value')) assertSubmitterHasNoValue(currentTag);
|
|
@@ -1755,9 +1730,37 @@ function applyAttrsToInstance(instance, attrs, Cls) {
|
|
|
1755
1730
|
// `raw` is the entity-encoded attribute text (parseAttrs returns the
|
|
1756
1731
|
// literal characters between the quotes), so decode the HTML entities
|
|
1757
1732
|
// before JSON.parse. A JSON attribute carries `"` for every `"`;
|
|
1758
|
-
// parsing it raw throws and
|
|
1759
|
-
//
|
|
1760
|
-
|
|
1733
|
+
// parsing it raw throws, and the prop then falls to the failure value
|
|
1734
|
+
// below rather than to the object the author wrote.
|
|
1735
|
+
//
|
|
1736
|
+
// An unparseable attribute falls back to `null`, NOT to the raw string
|
|
1737
|
+
// (#1253), matching this branch's counterpart in
|
|
1738
|
+
// `attributeChangedCallback`. They have to agree, or the same
|
|
1739
|
+
// `<my-el cfg="oops">` SSRs holding a string and re-renders holding
|
|
1740
|
+
// something else the moment the element upgrades. `null` is the right
|
|
1741
|
+
// value to agree on: a string is never a valid value for a property the
|
|
1742
|
+
// author declared `Object` or `Array`. This function iterates the
|
|
1743
|
+
// attributes PRESENT on the source tag, so an absent one is not read here
|
|
1744
|
+
// at all and its property keeps whatever the constructor gave it; the
|
|
1745
|
+
// agreement being asserted is about a present, unparseable attribute.
|
|
1746
|
+
//
|
|
1747
|
+
// That agreement is about this FALLBACK, not a guarantee that the two
|
|
1748
|
+
// readers see the same attributes in the first place. This function
|
|
1749
|
+
// walks the parsed source tag while the client goes through
|
|
1750
|
+
// `observedAttributes` and the browser's own name-lowercasing, and
|
|
1751
|
+
// hand-written markup can land in the gaps between those routes (the
|
|
1752
|
+
// entity decoding just below is one, since it reverses far less than a
|
|
1753
|
+
// browser does). Those gaps predate #1253 and it neither causes nor
|
|
1754
|
+
// closes any of them; no attempt is made here to enumerate them,
|
|
1755
|
+
// because every attempt so far has been incomplete.
|
|
1756
|
+
//
|
|
1757
|
+
// Scoped to the DEFAULT converter, which is all this branch is. The
|
|
1758
|
+
// client reader tries `converter.fromAttribute` FIRST and only falls
|
|
1759
|
+
// through to here when there is none, while this function has no
|
|
1760
|
+
// converter arm at all, so a prop declaring one is read differently by
|
|
1761
|
+
// the two sides regardless of what this line does. That gap predates
|
|
1762
|
+
// #1253 and is left alone rather than widened.
|
|
1763
|
+
try { instance[propName] = JSON.parse(unescapeAttr(raw)); } catch { instance[propName] = null; }
|
|
1761
1764
|
} else instance[propName] = raw;
|
|
1762
1765
|
}
|
|
1763
1766
|
}
|
|
@@ -1889,29 +1892,28 @@ export function renderToStream(value, opts = { ssr: true }) {
|
|
|
1889
1892
|
* @param {unknown} value
|
|
1890
1893
|
* @param {SuspenseCtx} [ctx]
|
|
1891
1894
|
* @param {ReadableStreamDefaultController<string>} controller
|
|
1892
|
-
* @param {'none'|'unbound'|'bound'|'unknown'} [formScope] enclosing form scope, as seen from THIS scan
|
|
1893
1895
|
*/
|
|
1894
|
-
async function streamRender(value, ctx, controller
|
|
1896
|
+
async function streamRender(value, ctx, controller) {
|
|
1895
1897
|
if (value == null || value === false || value === true) return;
|
|
1896
1898
|
if (value && typeof /** @type any */ (value).then === 'function') {
|
|
1897
1899
|
value = await value;
|
|
1898
|
-
return streamRender(value, ctx, controller
|
|
1900
|
+
return streamRender(value, ctx, controller);
|
|
1899
1901
|
}
|
|
1900
1902
|
if (isUnsafeHTML(value)) {
|
|
1901
1903
|
controller.enqueue(String(/** @type any */ (value).value ?? ''));
|
|
1902
1904
|
return;
|
|
1903
1905
|
}
|
|
1904
1906
|
if (isLive(value)) {
|
|
1905
|
-
return streamRender(/** @type any */ (value).value, ctx, controller
|
|
1907
|
+
return streamRender(/** @type any */ (value).value, ctx, controller);
|
|
1906
1908
|
}
|
|
1907
1909
|
if (isWatch(value)) {
|
|
1908
|
-
return streamRender(/** @type any */ (value).signal.get(), ctx, controller
|
|
1910
|
+
return streamRender(/** @type any */ (value).signal.get(), ctx, controller);
|
|
1909
1911
|
}
|
|
1910
1912
|
if (isKeyed(value)) {
|
|
1911
|
-
return streamRender(/** @type any */ (value).value, ctx, controller
|
|
1913
|
+
return streamRender(/** @type any */ (value).value, ctx, controller);
|
|
1912
1914
|
}
|
|
1913
1915
|
if (isGuard(value)) {
|
|
1914
|
-
return streamRender(/** @type any */ (value).fn(), ctx, controller
|
|
1916
|
+
return streamRender(/** @type any */ (value).fn(), ctx, controller);
|
|
1915
1917
|
}
|
|
1916
1918
|
if (isTemplateContent(value)) {
|
|
1917
1919
|
const tpl = /** @type any */ (value).template;
|
|
@@ -1922,19 +1924,19 @@ async function streamRender(value, ctx, controller, formScope = 'none') {
|
|
|
1922
1924
|
return;
|
|
1923
1925
|
}
|
|
1924
1926
|
if (isCache(value)) {
|
|
1925
|
-
return streamRender(/** @type any */ (value).value, ctx, controller
|
|
1927
|
+
return streamRender(/** @type any */ (value).value, ctx, controller);
|
|
1926
1928
|
}
|
|
1927
1929
|
if (isUntil(value)) {
|
|
1928
1930
|
const args = /** @type any */ (value).args;
|
|
1929
1931
|
for (const a of args) {
|
|
1930
1932
|
if (!a || typeof (/** @type any */ (a).then) !== 'function') {
|
|
1931
|
-
return streamRender(a, ctx, controller
|
|
1933
|
+
return streamRender(a, ctx, controller);
|
|
1932
1934
|
}
|
|
1933
1935
|
}
|
|
1934
1936
|
if (args.length > 0) {
|
|
1935
1937
|
try {
|
|
1936
1938
|
const winner = await Promise.race(args.map((p) => Promise.resolve(p).catch(() => undefined)));
|
|
1937
|
-
return streamRender(winner, ctx, controller
|
|
1939
|
+
return streamRender(winner, ctx, controller);
|
|
1938
1940
|
} catch {
|
|
1939
1941
|
return;
|
|
1940
1942
|
}
|
|
@@ -1945,13 +1947,13 @@ async function streamRender(value, ctx, controller, formScope = 'none') {
|
|
|
1945
1947
|
return;
|
|
1946
1948
|
}
|
|
1947
1949
|
if (Array.isArray(value)) {
|
|
1948
|
-
for (const v of value) await streamRender(v, ctx, controller
|
|
1950
|
+
for (const v of value) await streamRender(v, ctx, controller);
|
|
1949
1951
|
return;
|
|
1950
1952
|
}
|
|
1951
1953
|
if (isRepeat(value)) {
|
|
1952
1954
|
const r = /** @type any */ (value);
|
|
1953
1955
|
for (let i = 0; i < r.items.length; i++) {
|
|
1954
|
-
await streamRender(r.templateFn(r.items[i], i), ctx, controller
|
|
1956
|
+
await streamRender(r.templateFn(r.items[i], i), ctx, controller);
|
|
1955
1957
|
}
|
|
1956
1958
|
return;
|
|
1957
1959
|
}
|
|
@@ -1960,16 +1962,16 @@ async function streamRender(value, ctx, controller, formScope = 'none') {
|
|
|
1960
1962
|
if (ctx) {
|
|
1961
1963
|
const id = `s${ctx.nextId++}`;
|
|
1962
1964
|
controller.enqueue(`<webjs-boundary id="${id}">`);
|
|
1963
|
-
await streamRender(s.fallback, ctx, controller
|
|
1965
|
+
await streamRender(s.fallback, ctx, controller);
|
|
1964
1966
|
controller.enqueue(`</webjs-boundary>`);
|
|
1965
|
-
ctx.pending.push({ id, promise: Promise.resolve(s.children)
|
|
1967
|
+
ctx.pending.push({ id, promise: Promise.resolve(s.children) });
|
|
1966
1968
|
} else {
|
|
1967
|
-
await streamRender(s.fallback, ctx, controller
|
|
1969
|
+
await streamRender(s.fallback, ctx, controller);
|
|
1968
1970
|
}
|
|
1969
1971
|
return;
|
|
1970
1972
|
}
|
|
1971
1973
|
if (isTemplate(value)) {
|
|
1972
|
-
await streamTemplate(/** @type any */ (value), ctx, controller
|
|
1974
|
+
await streamTemplate(/** @type any */ (value), ctx, controller);
|
|
1973
1975
|
return;
|
|
1974
1976
|
}
|
|
1975
1977
|
controller.enqueue(escapeText(String(value)));
|
|
@@ -1982,9 +1984,8 @@ async function streamRender(value, ctx, controller, formScope = 'none') {
|
|
|
1982
1984
|
* @param {import('./html.js').TemplateResult} tr
|
|
1983
1985
|
* @param {SuspenseCtx} [ctx]
|
|
1984
1986
|
* @param {ReadableStreamDefaultController<string>} controller
|
|
1985
|
-
* @param {'none'|'unbound'|'bound'|'unknown'} [formScopeAtStart]
|
|
1986
1987
|
*/
|
|
1987
|
-
async function streamTemplate(tr, ctx, controller
|
|
1988
|
+
async function streamTemplate(tr, ctx, controller) {
|
|
1988
1989
|
const { strings, values } = tr;
|
|
1989
1990
|
let state = 'text';
|
|
1990
1991
|
let attrName = '';
|
|
@@ -2010,7 +2011,6 @@ async function streamTemplate(tr, ctx, controller, formScopeAtStart = 'none') {
|
|
|
2010
2011
|
let pendingSubmitterTag = null; // tag of a bound submitter, until it closes
|
|
2011
2012
|
// See the buffered machine: seeded from the caller so a nested template
|
|
2012
2013
|
// rendered into a bound form's children knows it is inside one.
|
|
2013
|
-
let formScope = formScopeAtStart;
|
|
2014
2014
|
let isCloseTag = false;
|
|
2015
2015
|
|
|
2016
2016
|
// See the buffered machine for why this runs at the `>` rather than at the
|
|
@@ -2032,17 +2032,12 @@ async function streamTemplate(tr, ctx, controller, formScopeAtStart = 'none') {
|
|
|
2032
2032
|
const bound = bindFormActionStartTag(buf.slice(tagStart), pendingActionId);
|
|
2033
2033
|
buf = buf.slice(0, tagStart) + bound.tag + bound.hidden;
|
|
2034
2034
|
pendingActionId = null;
|
|
2035
|
-
formScope = 'bound';
|
|
2036
2035
|
}
|
|
2037
|
-
// A form that opened and bound NOTHING still opens a scope: a submitter
|
|
2038
|
-
// inside it must be refused, which is a different answer from 'none'.
|
|
2039
|
-
if (!isCloseTag && currentTag === 'form' && formScope !== 'bound') formScope = 'unbound';
|
|
2040
2036
|
if (submitterTag != null) {
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
assertSubmitterStartTag(buf.slice(tagStart), currentTag, { bound: false, propAttrs: submitterProps });
|
|
2037
|
+
// #1307, the SAME injection as the buffered machine, through the same
|
|
2038
|
+
// helper, so this second state machine cannot drift from it.
|
|
2039
|
+
buf = buf.slice(0, tagStart)
|
|
2040
|
+
+ bindSubmitterStartTag(buf.slice(tagStart), submitterTag, { duplicateAction, propAttrs: submitterProps });
|
|
2046
2041
|
}
|
|
2047
2042
|
};
|
|
2048
2043
|
// #1155: a `.method` / `.enctype` / `.encoding` prop on a form is dropped
|
|
@@ -2078,14 +2073,6 @@ async function streamTemplate(tr, ctx, controller, formScopeAtStart = 'none') {
|
|
|
2078
2073
|
// tag ending on a bare or unquoted attribute must keep escaping its body.
|
|
2079
2074
|
const handleTagEnd = (allowRawtext) => {
|
|
2080
2075
|
closeBoundFormTag();
|
|
2081
|
-
if (isCloseTag && currentTag === 'form') {
|
|
2082
|
-
// Back to the scope this scan STARTED in, not a flat 'none'. Under an
|
|
2083
|
-
// 'unknown' seed (the component pass) the scan never learned whether the
|
|
2084
|
-
// host page has a bound form, and closing a form of its own teaches it
|
|
2085
|
-
// nothing about that, so asserting 'none' here would downgrade a
|
|
2086
|
-
// cannot-tell into a conclusive refusal.
|
|
2087
|
-
formScope = formScopeAtStart === 'unknown' ? 'unknown' : 'none';
|
|
2088
|
-
}
|
|
2089
2076
|
isCloseTag = false;
|
|
2090
2077
|
state = allowRawtext && isRawtextTag(currentTag) ? 'rawtext' : 'text';
|
|
2091
2078
|
if (state === 'rawtext') rawTail = '';
|
|
@@ -2189,7 +2176,7 @@ async function streamTemplate(tr, ctx, controller, formScopeAtStart = 'none') {
|
|
|
2189
2176
|
} else if (state === 'text') {
|
|
2190
2177
|
// Flush the buffered static content before streaming the value.
|
|
2191
2178
|
if (buf) { controller.enqueue(buf); buf = ''; }
|
|
2192
|
-
await streamRender(val, ctx, controller
|
|
2179
|
+
await streamRender(val, ctx, controller);
|
|
2193
2180
|
} else if (state === 'after-eq') {
|
|
2194
2181
|
const prefix = attrName[0];
|
|
2195
2182
|
const name = attrName.slice(1);
|
|
@@ -2246,11 +2233,6 @@ async function streamTemplate(tr, ctx, controller, formScopeAtStart = 'none') {
|
|
|
2246
2233
|
buf = buf.slice(0, attrStart).replace(/\s+$/, '');
|
|
2247
2234
|
} else {
|
|
2248
2235
|
assertSingleSubmitterAction(pendingSubmitterTag != null, currentTag);
|
|
2249
|
-
// Only 'unknown' defers: a component renders its own template in a
|
|
2250
|
-
// separate pass that cannot see the enclosing form, so the question
|
|
2251
|
-
// is not this scan's to answer. 'none' and 'unbound' are known
|
|
2252
|
-
// answers and are still refused.
|
|
2253
|
-
if (formScope !== 'unknown') assertSubmitterFormIsBound(formScope === 'bound', currentTag);
|
|
2254
2236
|
const attrs = parseStartTagAttrs(buf.slice(tagStart));
|
|
2255
2237
|
if (attrs.has('name')) assertSubmitterHasNoName(attrs.get('name') || '', currentTag, false);
|
|
2256
2238
|
if (attrs.has('value')) assertSubmitterHasNoValue(currentTag);
|
|
@@ -2306,10 +2288,10 @@ async function streamSuspenseBoundaries(ctx, controller, dev) {
|
|
|
2306
2288
|
while (ctx.pending.length) {
|
|
2307
2289
|
const batch = ctx.pending.splice(0);
|
|
2308
2290
|
await Promise.all(
|
|
2309
|
-
batch.map(async ({ id, promise
|
|
2291
|
+
batch.map(async ({ id, promise }) => {
|
|
2310
2292
|
try {
|
|
2311
2293
|
const resolved = await promise;
|
|
2312
|
-
const html = await render(resolved, ctx
|
|
2294
|
+
const html = await render(resolved, ctx);
|
|
2313
2295
|
const full = await injectDSD(html, ctx, [], dev);
|
|
2314
2296
|
controller.enqueue(
|
|
2315
2297
|
`<template data-webjs-resolve="${id}">${full}</template>` +
|