@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.
@@ -5,8 +5,8 @@ import {
5
5
  assertNotFunctionActionAttr, assertNotFunctionReflectedActionProp,
6
6
  assertIdentifiableAction, bindFormActionStartTag, isBoundFormAction, resolveFormActionId,
7
7
  assertConvergentBoundForm, assertSubmitterHasNoName, assertSubmitterHasNoValue,
8
- assertSubmitterFormIsBound, assertSubmitterHasNoFormAttribute,
9
- assertSingleSubmitterAction, assertSubmitterStartTag, parseStartTagAttrs,
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>, formScope?: 'none'|'unbound'|'bound'|'unknown'}[], nextId: number }} SuspenseCtx
34
+ * @typedef {{ pending: {id: string, promise: Promise<unknown>}[], nextId: number }} SuspenseCtx
35
35
  *
36
- * `formScope` (#1207) is how a caller that resolves a Suspense boundary ITSELF
37
- * tells this render what the boundary's surroundings were. The page pipeline in
38
- * `@webjsdev/server` drains `ctx.pending` and re-renders each resolved child
39
- * through here, which is a fresh scan with no view of the shell it belongs to,
40
- * so without the recorded scope a `<button formaction=${fn}>` inside a bound
41
- * form's Suspense boundary reads as form-less and is refused. That refusal is
42
- * caught and replaced by the empty string in production, so the whole boundary
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, formScope?: 'none'|'unbound'|'bound'|'unknown' }} [opts]
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
- // A caller that knows the scope passes it. Absent, this is a top-level render
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, formScope = 'none') {
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, formScope);
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, formScope);
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, formScope);
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, formScope);
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, formScope);
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, formScope);
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, formScope);
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, formScope);
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, formScope)));
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, formScope)));
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, formScope);
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), formScope });
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, formScope);
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, formScopeAtStart = 'none') {
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
- assertSubmitterStartTag(out.slice(tagStart), submitterTag, { bound: true, duplicateAction, propAttrs: submitterProps });
239
- } else if (formScope === 'bound' && !isCloseTag && (currentTag === 'button' || currentTag === 'input')) {
240
- // Part B (#1207): an ordinary submitter inside a bound form, whose own
241
- // `formmethod` / `formenctype` can defeat the binding without carrying an
242
- // action of its own. Neither renderer used to look at those. Gated on
243
- // 'bound' because that is the only scope in which the sweep has anything
244
- // to protect: under 'unknown' the enclosing form is not this scan's to
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. `</form>` closes the form
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, formScope);
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, formScope);
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, formScope = 'none') {
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, formScope);
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, formScope);
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, formScope);
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, formScope);
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, formScope);
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, formScope);
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, formScope);
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, formScope);
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, formScope);
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, formScope);
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, formScope);
1934
+ await streamRender(s.fallback, ctx, controller);
1964
1935
  controller.enqueue(`</webjs-boundary>`);
1965
- ctx.pending.push({ id, promise: Promise.resolve(s.children), formScope });
1936
+ ctx.pending.push({ id, promise: Promise.resolve(s.children) });
1966
1937
  } else {
1967
- await streamRender(s.fallback, ctx, controller, formScope);
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, formScope);
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, formScopeAtStart = 'none') {
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
- assertSubmitterStartTag(buf.slice(tagStart), submitterTag, { bound: true, duplicateAction, propAttrs: submitterProps });
2042
- } else if (formScope === 'bound' && !isCloseTag && (currentTag === 'button' || currentTag === 'input')) {
2043
- // Part B (#1207), the SAME sweep as the buffered machine, through the
2044
- // same helper, so this second state machine cannot drift from it.
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, formScope);
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, formScope }) => {
2260
+ batch.map(async ({ id, promise }) => {
2310
2261
  try {
2311
2262
  const resolved = await promise;
2312
- const html = await render(resolved, ctx, formScope);
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>` +