@webjsdev/core 0.7.47 → 0.7.48
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/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 +1 -1
- package/index.d.ts +1 -1
- package/index.js +1 -1
- 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/form-action.js +204 -203
- package/src/render-client.js +94 -96
- package/src/render-server.js +60 -109
- package/src/router-client.js +617 -12
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);
|
|
@@ -430,7 +407,7 @@ async function renderTemplate(tr, ctx, formScopeAtStart = 'none') {
|
|
|
430
407
|
// fallback to HTML now and carry it as data-webjs-fallback, which the
|
|
431
408
|
// injectDSD streaming pre-pass reads as the boundary placeholder.
|
|
432
409
|
if (currentTag === 'webjs-suspense' && name === 'fallback') {
|
|
433
|
-
const fbHtml = await render(val, ctx
|
|
410
|
+
const fbHtml = await render(val, ctx);
|
|
434
411
|
out += `data-webjs-fallback="${escapeAttr(fbHtml)}"`;
|
|
435
412
|
state = 'in-tag';
|
|
436
413
|
attrName = '';
|
|
@@ -516,11 +493,6 @@ async function renderTemplate(tr, ctx, formScopeAtStart = 'none') {
|
|
|
516
493
|
// author gets the duplicate message rather than a confusing
|
|
517
494
|
// complaint about the `name` the FIRST hole just injected.
|
|
518
495
|
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
496
|
const attrs = parseStartTagAttrs(out.slice(tagStart));
|
|
525
497
|
if (attrs.has('name')) assertSubmitterHasNoName(attrs.get('name') || '', currentTag, false);
|
|
526
498
|
if (attrs.has('value')) assertSubmitterHasNoValue(currentTag);
|
|
@@ -1889,29 +1861,28 @@ export function renderToStream(value, opts = { ssr: true }) {
|
|
|
1889
1861
|
* @param {unknown} value
|
|
1890
1862
|
* @param {SuspenseCtx} [ctx]
|
|
1891
1863
|
* @param {ReadableStreamDefaultController<string>} controller
|
|
1892
|
-
* @param {'none'|'unbound'|'bound'|'unknown'} [formScope] enclosing form scope, as seen from THIS scan
|
|
1893
1864
|
*/
|
|
1894
|
-
async function streamRender(value, ctx, controller
|
|
1865
|
+
async function streamRender(value, ctx, controller) {
|
|
1895
1866
|
if (value == null || value === false || value === true) return;
|
|
1896
1867
|
if (value && typeof /** @type any */ (value).then === 'function') {
|
|
1897
1868
|
value = await value;
|
|
1898
|
-
return streamRender(value, ctx, controller
|
|
1869
|
+
return streamRender(value, ctx, controller);
|
|
1899
1870
|
}
|
|
1900
1871
|
if (isUnsafeHTML(value)) {
|
|
1901
1872
|
controller.enqueue(String(/** @type any */ (value).value ?? ''));
|
|
1902
1873
|
return;
|
|
1903
1874
|
}
|
|
1904
1875
|
if (isLive(value)) {
|
|
1905
|
-
return streamRender(/** @type any */ (value).value, ctx, controller
|
|
1876
|
+
return streamRender(/** @type any */ (value).value, ctx, controller);
|
|
1906
1877
|
}
|
|
1907
1878
|
if (isWatch(value)) {
|
|
1908
|
-
return streamRender(/** @type any */ (value).signal.get(), ctx, controller
|
|
1879
|
+
return streamRender(/** @type any */ (value).signal.get(), ctx, controller);
|
|
1909
1880
|
}
|
|
1910
1881
|
if (isKeyed(value)) {
|
|
1911
|
-
return streamRender(/** @type any */ (value).value, ctx, controller
|
|
1882
|
+
return streamRender(/** @type any */ (value).value, ctx, controller);
|
|
1912
1883
|
}
|
|
1913
1884
|
if (isGuard(value)) {
|
|
1914
|
-
return streamRender(/** @type any */ (value).fn(), ctx, controller
|
|
1885
|
+
return streamRender(/** @type any */ (value).fn(), ctx, controller);
|
|
1915
1886
|
}
|
|
1916
1887
|
if (isTemplateContent(value)) {
|
|
1917
1888
|
const tpl = /** @type any */ (value).template;
|
|
@@ -1922,19 +1893,19 @@ async function streamRender(value, ctx, controller, formScope = 'none') {
|
|
|
1922
1893
|
return;
|
|
1923
1894
|
}
|
|
1924
1895
|
if (isCache(value)) {
|
|
1925
|
-
return streamRender(/** @type any */ (value).value, ctx, controller
|
|
1896
|
+
return streamRender(/** @type any */ (value).value, ctx, controller);
|
|
1926
1897
|
}
|
|
1927
1898
|
if (isUntil(value)) {
|
|
1928
1899
|
const args = /** @type any */ (value).args;
|
|
1929
1900
|
for (const a of args) {
|
|
1930
1901
|
if (!a || typeof (/** @type any */ (a).then) !== 'function') {
|
|
1931
|
-
return streamRender(a, ctx, controller
|
|
1902
|
+
return streamRender(a, ctx, controller);
|
|
1932
1903
|
}
|
|
1933
1904
|
}
|
|
1934
1905
|
if (args.length > 0) {
|
|
1935
1906
|
try {
|
|
1936
1907
|
const winner = await Promise.race(args.map((p) => Promise.resolve(p).catch(() => undefined)));
|
|
1937
|
-
return streamRender(winner, ctx, controller
|
|
1908
|
+
return streamRender(winner, ctx, controller);
|
|
1938
1909
|
} catch {
|
|
1939
1910
|
return;
|
|
1940
1911
|
}
|
|
@@ -1945,13 +1916,13 @@ async function streamRender(value, ctx, controller, formScope = 'none') {
|
|
|
1945
1916
|
return;
|
|
1946
1917
|
}
|
|
1947
1918
|
if (Array.isArray(value)) {
|
|
1948
|
-
for (const v of value) await streamRender(v, ctx, controller
|
|
1919
|
+
for (const v of value) await streamRender(v, ctx, controller);
|
|
1949
1920
|
return;
|
|
1950
1921
|
}
|
|
1951
1922
|
if (isRepeat(value)) {
|
|
1952
1923
|
const r = /** @type any */ (value);
|
|
1953
1924
|
for (let i = 0; i < r.items.length; i++) {
|
|
1954
|
-
await streamRender(r.templateFn(r.items[i], i), ctx, controller
|
|
1925
|
+
await streamRender(r.templateFn(r.items[i], i), ctx, controller);
|
|
1955
1926
|
}
|
|
1956
1927
|
return;
|
|
1957
1928
|
}
|
|
@@ -1960,16 +1931,16 @@ async function streamRender(value, ctx, controller, formScope = 'none') {
|
|
|
1960
1931
|
if (ctx) {
|
|
1961
1932
|
const id = `s${ctx.nextId++}`;
|
|
1962
1933
|
controller.enqueue(`<webjs-boundary id="${id}">`);
|
|
1963
|
-
await streamRender(s.fallback, ctx, controller
|
|
1934
|
+
await streamRender(s.fallback, ctx, controller);
|
|
1964
1935
|
controller.enqueue(`</webjs-boundary>`);
|
|
1965
|
-
ctx.pending.push({ id, promise: Promise.resolve(s.children)
|
|
1936
|
+
ctx.pending.push({ id, promise: Promise.resolve(s.children) });
|
|
1966
1937
|
} else {
|
|
1967
|
-
await streamRender(s.fallback, ctx, controller
|
|
1938
|
+
await streamRender(s.fallback, ctx, controller);
|
|
1968
1939
|
}
|
|
1969
1940
|
return;
|
|
1970
1941
|
}
|
|
1971
1942
|
if (isTemplate(value)) {
|
|
1972
|
-
await streamTemplate(/** @type any */ (value), ctx, controller
|
|
1943
|
+
await streamTemplate(/** @type any */ (value), ctx, controller);
|
|
1973
1944
|
return;
|
|
1974
1945
|
}
|
|
1975
1946
|
controller.enqueue(escapeText(String(value)));
|
|
@@ -1982,9 +1953,8 @@ async function streamRender(value, ctx, controller, formScope = 'none') {
|
|
|
1982
1953
|
* @param {import('./html.js').TemplateResult} tr
|
|
1983
1954
|
* @param {SuspenseCtx} [ctx]
|
|
1984
1955
|
* @param {ReadableStreamDefaultController<string>} controller
|
|
1985
|
-
* @param {'none'|'unbound'|'bound'|'unknown'} [formScopeAtStart]
|
|
1986
1956
|
*/
|
|
1987
|
-
async function streamTemplate(tr, ctx, controller
|
|
1957
|
+
async function streamTemplate(tr, ctx, controller) {
|
|
1988
1958
|
const { strings, values } = tr;
|
|
1989
1959
|
let state = 'text';
|
|
1990
1960
|
let attrName = '';
|
|
@@ -2010,7 +1980,6 @@ async function streamTemplate(tr, ctx, controller, formScopeAtStart = 'none') {
|
|
|
2010
1980
|
let pendingSubmitterTag = null; // tag of a bound submitter, until it closes
|
|
2011
1981
|
// See the buffered machine: seeded from the caller so a nested template
|
|
2012
1982
|
// rendered into a bound form's children knows it is inside one.
|
|
2013
|
-
let formScope = formScopeAtStart;
|
|
2014
1983
|
let isCloseTag = false;
|
|
2015
1984
|
|
|
2016
1985
|
// See the buffered machine for why this runs at the `>` rather than at the
|
|
@@ -2032,17 +2001,12 @@ async function streamTemplate(tr, ctx, controller, formScopeAtStart = 'none') {
|
|
|
2032
2001
|
const bound = bindFormActionStartTag(buf.slice(tagStart), pendingActionId);
|
|
2033
2002
|
buf = buf.slice(0, tagStart) + bound.tag + bound.hidden;
|
|
2034
2003
|
pendingActionId = null;
|
|
2035
|
-
formScope = 'bound';
|
|
2036
2004
|
}
|
|
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
2005
|
if (submitterTag != null) {
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
assertSubmitterStartTag(buf.slice(tagStart), currentTag, { bound: false, propAttrs: submitterProps });
|
|
2006
|
+
// #1307, the SAME injection as the buffered machine, through the same
|
|
2007
|
+
// helper, so this second state machine cannot drift from it.
|
|
2008
|
+
buf = buf.slice(0, tagStart)
|
|
2009
|
+
+ bindSubmitterStartTag(buf.slice(tagStart), submitterTag, { duplicateAction, propAttrs: submitterProps });
|
|
2046
2010
|
}
|
|
2047
2011
|
};
|
|
2048
2012
|
// #1155: a `.method` / `.enctype` / `.encoding` prop on a form is dropped
|
|
@@ -2078,14 +2042,6 @@ async function streamTemplate(tr, ctx, controller, formScopeAtStart = 'none') {
|
|
|
2078
2042
|
// tag ending on a bare or unquoted attribute must keep escaping its body.
|
|
2079
2043
|
const handleTagEnd = (allowRawtext) => {
|
|
2080
2044
|
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
2045
|
isCloseTag = false;
|
|
2090
2046
|
state = allowRawtext && isRawtextTag(currentTag) ? 'rawtext' : 'text';
|
|
2091
2047
|
if (state === 'rawtext') rawTail = '';
|
|
@@ -2189,7 +2145,7 @@ async function streamTemplate(tr, ctx, controller, formScopeAtStart = 'none') {
|
|
|
2189
2145
|
} else if (state === 'text') {
|
|
2190
2146
|
// Flush the buffered static content before streaming the value.
|
|
2191
2147
|
if (buf) { controller.enqueue(buf); buf = ''; }
|
|
2192
|
-
await streamRender(val, ctx, controller
|
|
2148
|
+
await streamRender(val, ctx, controller);
|
|
2193
2149
|
} else if (state === 'after-eq') {
|
|
2194
2150
|
const prefix = attrName[0];
|
|
2195
2151
|
const name = attrName.slice(1);
|
|
@@ -2246,11 +2202,6 @@ async function streamTemplate(tr, ctx, controller, formScopeAtStart = 'none') {
|
|
|
2246
2202
|
buf = buf.slice(0, attrStart).replace(/\s+$/, '');
|
|
2247
2203
|
} else {
|
|
2248
2204
|
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
2205
|
const attrs = parseStartTagAttrs(buf.slice(tagStart));
|
|
2255
2206
|
if (attrs.has('name')) assertSubmitterHasNoName(attrs.get('name') || '', currentTag, false);
|
|
2256
2207
|
if (attrs.has('value')) assertSubmitterHasNoValue(currentTag);
|
|
@@ -2306,10 +2257,10 @@ async function streamSuspenseBoundaries(ctx, controller, dev) {
|
|
|
2306
2257
|
while (ctx.pending.length) {
|
|
2307
2258
|
const batch = ctx.pending.splice(0);
|
|
2308
2259
|
await Promise.all(
|
|
2309
|
-
batch.map(async ({ id, promise
|
|
2260
|
+
batch.map(async ({ id, promise }) => {
|
|
2310
2261
|
try {
|
|
2311
2262
|
const resolved = await promise;
|
|
2312
|
-
const html = await render(resolved, ctx
|
|
2263
|
+
const html = await render(resolved, ctx);
|
|
2313
2264
|
const full = await injectDSD(html, ctx, [], dev);
|
|
2314
2265
|
controller.enqueue(
|
|
2315
2266
|
`<template data-webjs-resolve="${id}">${full}</template>` +
|