@takazudo/zfb 4.4.0 → 4.5.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.
Files changed (38) hide show
  1. package/README.md +14 -0
  2. package/dist/css.d.ts +45 -0
  3. package/dist/css.js +137 -0
  4. package/dist/css.js.map +1 -0
  5. package/dist/zudo-react/dom-bindings.js +11 -5
  6. package/dist/zudo-react/dom-bindings.js.map +1 -1
  7. package/dist/zudo-react/error-boundary-client.d.ts +3 -0
  8. package/dist/zudo-react/error-boundary-client.js +232 -0
  9. package/dist/zudo-react/error-boundary-client.js.map +1 -0
  10. package/dist/zudo-react/error-boundary-type.d.ts +8 -0
  11. package/dist/zudo-react/error-boundary-type.js +25 -0
  12. package/dist/zudo-react/error-boundary-type.js.map +1 -0
  13. package/dist/zudo-react/error-boundary.d.ts +8 -0
  14. package/dist/zudo-react/error-boundary.js +13 -0
  15. package/dist/zudo-react/error-boundary.js.map +1 -0
  16. package/dist/zudo-react/form-recovery.d.ts +2 -0
  17. package/dist/zudo-react/form-recovery.js +116 -0
  18. package/dist/zudo-react/form-recovery.js.map +1 -0
  19. package/dist/zudo-react/forms.d.ts +38 -4
  20. package/dist/zudo-react/forms.js +143 -172
  21. package/dist/zudo-react/forms.js.map +1 -1
  22. package/dist/zudo-react/hydrate.d.ts +68 -0
  23. package/dist/zudo-react/hydrate.js +177 -89
  24. package/dist/zudo-react/hydrate.js.map +1 -1
  25. package/dist/zudo-react/index.d.ts +1 -0
  26. package/dist/zudo-react/index.js +1 -0
  27. package/dist/zudo-react/index.js.map +1 -1
  28. package/dist/zudo-react/render-html.js +55 -10
  29. package/dist/zudo-react/render-html.js.map +1 -1
  30. package/dist/zudo-react/scope.d.ts +21 -13
  31. package/dist/zudo-react/scope.js +141 -113
  32. package/dist/zudo-react/scope.js.map +1 -1
  33. package/dist/zudo-react/style.d.ts +0 -1
  34. package/dist/zudo-react/style.js +11 -503
  35. package/dist/zudo-react/style.js.map +1 -1
  36. package/dist/zudo-react/vocabulary.js +12 -49
  37. package/dist/zudo-react/vocabulary.js.map +1 -1
  38. package/package.json +11 -7
@@ -1,10 +1,12 @@
1
- import { activateForms, prepareForms, reconcileForms, FormError } from "./forms.js";
1
+ import { boundaryRenderer, integrity, recoverable } from "./error-boundary-type.js";
2
+ import { islandRootType } from "./island-root-type.js";
3
+ import { activateForms, prepareForms, reconcileForms, formHost, FormError, } from "./forms.js";
2
4
  import { Fragment, isDescription } from "./description.js";
3
- import { createScope, withScope } from "./scope.js";
5
+ import { RuntimeScope, withScope } from "./scope.js";
4
6
  import { isReactive, read, styleText, setAttribute, bind, listener } from "./dom-bindings.js";
5
7
  import { createRoot, diagnostic, report, rootPath, storedRoot, } from "./root.js";
6
- import { subscribe } from "./reactive.js";
7
- import { batch } from "./scheduler.js";
8
+ import { subscribe, untracked } from "./reactive.js";
9
+ import { batch, enqueue } from "./scheduler.js";
8
10
  import { rawHtmlReserved } from "./raw-html.js";
9
11
  import { Show, For, showProps, forProps, keyed, keyPayload, view } from "./structure.js";
10
12
  import { attributeError, commonAttrs, dialectSuggestion, displayOnlyControl, emptyIframeChildren, formProps, htmlAttrs, htmlTags, isDialectProp, reactiveModelSuggestion, svgAttrs, svgTags, voidTags, } from "./vocabulary.js";
@@ -32,14 +34,21 @@ function owned(context, name, namespace, parentTag, path, factory, initial, kind
32
34
  const fragment = context.document.createDocumentFragment();
33
35
  const operationsStart = context.operations.length;
34
36
  const formsStart = context.forms.length;
37
+ const rangesStart = context.dynamicRanges.length;
35
38
  const previous = context.scope;
36
39
  context.scope = scope;
37
40
  const cleanups = [];
38
41
  try {
39
42
  withScope(scope, () => {
40
- scope.onCleanup(() => {
41
- for (const cleanup of [...cleanups].reverse())
42
- cleanup();
43
+ scope.cleanups.push(() => {
44
+ for (const cleanup of cleanups.splice(0).reverse()) {
45
+ try {
46
+ cleanup();
47
+ }
48
+ catch (error) {
49
+ scope.report("ZR_CLEANUP_ERROR", "cleanup", String(error));
50
+ }
51
+ }
43
52
  });
44
53
  const output = factory();
45
54
  if (kind)
@@ -49,16 +58,23 @@ function owned(context, name, namespace, parentTag, path, factory, initial, kind
49
58
  });
50
59
  const operations = context.operations.splice(operationsStart);
51
60
  const forms = initial ? context.forms.slice(formsStart) : context.forms.splice(formsStart);
52
- const instance = { scope, fragment, operations, forms, cleanups };
53
- if (initial)
54
- for (const form of forms)
55
- if (!context.formOwners.has(form))
56
- context.formOwners.set(form, instance);
61
+ const instance = {
62
+ scope,
63
+ fragment,
64
+ operations,
65
+ forms,
66
+ formOwners: context.formOwners,
67
+ cleanups,
68
+ };
69
+ for (const form of forms)
70
+ if (!context.formOwners.has(form))
71
+ context.formOwners.set(form, instance);
57
72
  return instance;
58
73
  }
59
74
  catch (error) {
60
75
  context.operations.length = operationsStart;
61
76
  context.forms.length = formsStart;
77
+ context.dynamicRanges.length = rangesStart;
62
78
  scope.dispose();
63
79
  throw error;
64
80
  }
@@ -69,12 +85,29 @@ function owned(context, name, namespace, parentTag, path, factory, initial, kind
69
85
  function installOwned(instance, map, context, mode) {
70
86
  if (mode === "mount" && instance.forms.length) {
71
87
  const bindings = prepareForms(instance.forms, map, "mount", context.container);
72
- reconcileForms(bindings, "mount");
73
- activateForms(bindings, instance.scope, instance.cleanups, context.options, context.container);
88
+ reconcileForms(bindings, "mount", context.container);
89
+ activateOwnedForms(bindings, instance.forms, map, instance.formOwners, context, instance.scope, instance.cleanups);
74
90
  }
75
91
  for (const operation of instance.operations)
76
92
  operation(map, instance.cleanups);
77
93
  }
94
+ function activateOwnedForms(bindings, plans, map, owners, context, scope, cleanups) {
95
+ const byNode = new Map();
96
+ for (const plan of plans)
97
+ byNode.set(map.get(plan.element), owners.get(plan));
98
+ const groups = new Map();
99
+ for (const binding of bindings) {
100
+ const owner = byNode.get(binding.node);
101
+ const group = groups.get(owner) ?? [];
102
+ group.push(binding);
103
+ groups.set(owner, group);
104
+ }
105
+ // Root-owned bindings initialize first, as in the root adoption path.
106
+ activateForms(groups.get(undefined) ?? [], scope, cleanups, context.options, context.container);
107
+ groups.delete(undefined);
108
+ for (const [owner, group] of groups)
109
+ activateForms(group, owner.scope, owner.cleanups, context.options, context.container);
110
+ }
78
111
  function selfMap(fragment) {
79
112
  const map = new Map();
80
113
  const visit = (node) => {
@@ -87,22 +120,23 @@ function selfMap(fragment) {
87
120
  return map;
88
121
  }
89
122
  class Failure extends Error {
90
- code;
91
- phase;
92
- expected;
93
- actual;
94
- path;
95
123
  constructor(code, phase, expected, actual, path) {
96
124
  super(`${code}: ${expected}; got ${actual}`);
97
- this.code = code;
98
- this.phase = phase;
99
- this.expected = expected;
100
- this.actual = actual;
101
- this.path = path;
125
+ Object.assign(this, { code, phase, expected, actual, path });
102
126
  }
103
127
  }
104
- function fail(code, phase, expected, actual, path) {
105
- throw new Failure(code, phase, expected, actual, path);
128
+ function fail(code, phase, expected, actual, path, fatal = false) {
129
+ const error = new Failure(code, phase, expected, actual, path);
130
+ throw fatal ||
131
+ [
132
+ "ZR_IDENTITY",
133
+ "ZR_CANCELLED",
134
+ "ZR_DUPLICATE_ROOT",
135
+ "ZR_HYDRATION_MISMATCH",
136
+ "ZR_NESTED_ISLAND",
137
+ ].includes(code)
138
+ ? integrity(error)
139
+ : error;
106
140
  }
107
141
  function append(parent, node) {
108
142
  if (node.nodeType === Node.TEXT_NODE && parent.lastChild?.nodeType === Node.TEXT_NODE) {
@@ -112,7 +146,7 @@ function append(parent, node) {
112
146
  parent.appendChild(node);
113
147
  }
114
148
  function marker(context, parent, kind, content) {
115
- const id = context.next++;
149
+ const id = `${context.prefix ?? ""}${context.next++}`;
116
150
  const open = context.document.createComment(`zr:1:${id}:${kind}`);
117
151
  parent.appendChild(open);
118
152
  content();
@@ -122,7 +156,8 @@ function marker(context, parent, kind, content) {
122
156
  context.opaque.add(open);
123
157
  if (kind === "t")
124
158
  context.textSlots.add(open);
125
- if (context.collectRootRanges && (kind === "t" || kind === "l" || kind.startsWith("s:")))
159
+ if (context.collectRootRanges &&
160
+ (kind === "e" || kind === "t" || kind === "l" || kind.startsWith("s:")))
126
161
  context.dynamicRanges.push([open, close]);
127
162
  return [open, close];
128
163
  }
@@ -158,7 +193,7 @@ function range(start, end) {
158
193
  if (node === end)
159
194
  return nodes;
160
195
  }
161
- throw new Error("ZR_REGION_RANGE");
196
+ throw integrity(new Error("ZR_REGION_RANGE"));
162
197
  }
163
198
  function removeRange(start, end) {
164
199
  for (const node of range(start, end))
@@ -201,11 +236,12 @@ function scalar(value, path) {
201
236
  fail("ZR_CHILD", "setup", "scalar", typeof value, path);
202
237
  }
203
238
  function validateRawHtml(value, tag, path) {
204
- if (typeof value !== "string" ||
205
- rawHtmlReserved(value, tag) ||
239
+ if (typeof value !== "string")
240
+ fail("ZR_RAW_HTML", "setup", "valid rawHtml", typeof value, path);
241
+ if (rawHtmlReserved(value, tag) ||
206
242
  (tag === "script" && /<\/script/i.test(value)) ||
207
243
  (tag === "style" && /<\/style/i.test(value)))
208
- fail("ZR_RAW_HTML", "setup", "valid rawHtml", typeof value, path);
244
+ fail("ZR_RAW_HTML", "setup", "valid rawHtml", typeof value, path, true);
209
245
  }
210
246
  function children(value, parent, context, namespace, tag, path) {
211
247
  if (value == null || typeof value === "boolean")
@@ -235,6 +271,8 @@ function element(desc, parent, context, namespace, parentTag, path) {
235
271
  const childNamespace = tag === "foreignObject" ? HTML : ownNamespace;
236
272
  const element = context.document.createElementNS(ownNamespace, tag);
237
273
  const props = desc.props;
274
+ if ("data-zfb-island" in props || "data-zfb-island-skip-ssr" in props)
275
+ fail("ZR_NESTED_ISLAND", "preflight", "no nested root", tag, path);
238
276
  if (tag === "iframe") {
239
277
  if (Object.hasOwn(props, "rawHtml"))
240
278
  fail("ZR_RAW_HTML", "preflight", "iframe without rawHtml", "rawHtml", path);
@@ -291,14 +329,15 @@ function element(desc, parent, context, namespace, parentTag, path) {
291
329
  fail("ZR_LISTENER", "setup", "native listener", name, path);
292
330
  context.operations.push((map, cleanups) => {
293
331
  const node = map.get(element);
332
+ const eventError = (error) => report(context.options, diagnostic(context.options, "ZR_EVENT_ERROR", "update", `${rootPath(context.container)}${path}`, "handler success", String(error)));
294
333
  const handler = (event) => {
295
334
  try {
296
335
  const result = original(event);
297
336
  if (result && typeof result === "object" && "then" in result)
298
- Promise.resolve(result).catch((error) => report(context.options, diagnostic(context.options, "ZR_EVENT_ERROR", "update", `${rootPath(context.container)}${path}`, "handler success", String(error))));
337
+ Promise.resolve(result).catch(eventError);
299
338
  }
300
339
  catch (error) {
301
- report(context.options, diagnostic(context.options, "ZR_EVENT_ERROR", "update", `${rootPath(context.container)}${path}`, "handler success", String(error)));
340
+ eventError(error);
302
341
  }
303
342
  };
304
343
  node.addEventListener(parsed.name, handler, parsed.capture);
@@ -343,8 +382,7 @@ function element(desc, parent, context, namespace, parentTag, path) {
343
382
  (!custom &&
344
383
  !commonAttrs.has(name) &&
345
384
  !(ownNamespace === SVG ? svgAttrs.has(name) : htmlAttrs.has(name)) &&
346
- !/^data-[\w.-]+$/.test(name) &&
347
- !/^aria-[\w.-]+$/.test(name) &&
385
+ !/^(data|aria)-[\w.-]+$/.test(name) &&
348
386
  !/^on[a-z]+$/.test(name)))
349
387
  fail("ZR_ATTRIBUTE", "setup", "supported attribute", name, path);
350
388
  const initial = read(original);
@@ -404,7 +442,7 @@ function element(desc, parent, context, namespace, parentTag, path) {
404
442
  const template = context.document.createElement("template");
405
443
  template.innerHTML = value;
406
444
  if (start.parentNode !== node || end.parentNode !== node)
407
- throw new Error("ZR_REGION_RANGE");
445
+ throw integrity(new Error("ZR_REGION_RANGE"));
408
446
  for (const child of range(start, end).slice(1, -1))
409
447
  node.removeChild(child);
410
448
  node.insertBefore(template.content, end);
@@ -488,6 +526,31 @@ function render(value, parent, context, namespace, parentTag, path) {
488
526
  }
489
527
  if (!isDescription(value))
490
528
  fail("ZR_CHILD", "setup", "description", typeof value, path);
529
+ if (value.type === islandRootType)
530
+ fail("ZR_NESTED_ISLAND", "preflight", "no nested root", "island", path);
531
+ const boundary = boundaryRenderer(value.type);
532
+ if (boundary) {
533
+ if (restricted.has(parentTag))
534
+ fail("ZR_UNSUPPORTED_POSITION", "preflight", "flow region", parentTag, path);
535
+ boundary(value, parent, context, namespace, parentTag, path, {
536
+ owned,
537
+ installOwned,
538
+ selfMap,
539
+ marker,
540
+ range,
541
+ fail,
542
+ openMarker,
543
+ matchingClose,
544
+ recoverable,
545
+ prepareForms,
546
+ read,
547
+ subscribe,
548
+ untracked,
549
+ enqueue,
550
+ formHost,
551
+ });
552
+ return;
553
+ }
491
554
  if (value.type === Fragment) {
492
555
  marker(context, parent, "f", () => children(value.props.children, parent, context, namespace, parentTag, path));
493
556
  return;
@@ -520,17 +583,21 @@ function render(value, parent, context, namespace, parentTag, path) {
520
583
  if (current)
521
584
  installOwned(current, map, context, "hydrate");
522
585
  const subscription = subscribe(() => {
523
- const next = props.when.value;
524
- if (next === shown)
525
- return;
526
- const factory = next ? props.children : props.fallback;
527
586
  let replacement = null;
587
+ let rendering = true;
528
588
  try {
589
+ const next = props.when.value;
590
+ if (typeof next !== "boolean")
591
+ throw new TypeError(`ZR_CHILD: Show.when at ${path}`);
592
+ if (next === shown)
593
+ return;
594
+ const factory = next ? props.children : props.fallback;
529
595
  if (factory)
530
596
  replacement = owned(context, "Show", namespace, parentTag, path, factory, false, undefined, owner);
597
+ rendering = false;
531
598
  const replacementMap = replacement ? selfMap(replacement.fragment) : null;
532
599
  if (replacement && replacement.forms.length)
533
- prepareForms(replacement.forms, replacementMap, "mount", context.container);
600
+ prepareForms(replacement.forms, replacementMap, "mount", context.container, current ? [current.scope] : [], start.parentNode);
534
601
  if (current) {
535
602
  current.scope.dispose();
536
603
  let node = start.nextSibling;
@@ -550,6 +617,10 @@ function render(value, parent, context, namespace, parentTag, path) {
550
617
  }
551
618
  catch (error) {
552
619
  replacement?.scope.dispose();
620
+ if (rendering) {
621
+ if (owner.recoverRender(error))
622
+ return;
623
+ }
553
624
  throw error;
554
625
  }
555
626
  }, {
@@ -603,14 +674,24 @@ function render(value, parent, context, namespace, parentTag, path) {
603
674
  installOwned(entry.owned, map, context, "hydrate");
604
675
  const end = map.get(ends[1]);
605
676
  const subscription = subscribe(() => {
606
- const next = props.each.value;
607
- if (!Array.isArray(next))
608
- throw new TypeError(`ZR_CHILD: For.each at ${path}`);
677
+ let next;
609
678
  let nextKeys;
679
+ try {
680
+ next = props.each.value;
681
+ if (!Array.isArray(next))
682
+ throw new TypeError(`ZR_CHILD: For.each at ${path}`);
683
+ }
684
+ catch (error) {
685
+ if (owner.recoverRender(error))
686
+ return;
687
+ throw error;
688
+ }
610
689
  try {
611
690
  nextKeys = keyed(next, props.by, path);
612
691
  }
613
692
  catch (error) {
693
+ if (owner.recoverRender(error))
694
+ return;
614
695
  const detail = String(error);
615
696
  const code = detail.includes("ZR_DUPLICATE_KEY")
616
697
  ? "ZR_DUPLICATE_KEY"
@@ -622,6 +703,7 @@ function render(value, parent, context, namespace, parentTag, path) {
622
703
  }
623
704
  const old = new Map(current.map((entry) => [entry.key, entry]));
624
705
  const created = [];
706
+ let rendering = true;
625
707
  try {
626
708
  const ordered = nextKeys.map((key, index) => {
627
709
  const retained = old.get(key);
@@ -642,9 +724,12 @@ function render(value, parent, context, namespace, parentTag, path) {
642
724
  created.push(entry);
643
725
  return entry;
644
726
  });
727
+ rendering = false;
645
728
  for (const entry of created)
646
729
  if (entry.owned.forms.length)
647
- prepareForms(entry.owned.forms, selfMap(entry.owned.fragment), "mount", context.container);
730
+ prepareForms(entry.owned.forms, selfMap(entry.owned.fragment), "mount", context.container, current
731
+ .filter((item) => !nextKeys.includes(item.key))
732
+ .map((item) => item.owned.scope), end.parentNode);
648
733
  for (const entry of current)
649
734
  if (!nextKeys.includes(entry.key)) {
650
735
  entry.owned.scope.dispose();
@@ -682,6 +767,10 @@ function render(value, parent, context, namespace, parentTag, path) {
682
767
  catch (error) {
683
768
  for (const entry of created)
684
769
  entry.owned.scope.dispose();
770
+ if (rendering) {
771
+ if (owner.recoverRender(error))
772
+ return;
773
+ }
685
774
  throw error;
686
775
  }
687
776
  }, {
@@ -765,22 +854,31 @@ function significant(nodes, pre) {
765
854
  ? nodes
766
855
  : nodes.filter((node) => node.nodeType !== Node.TEXT_NODE || normalize(node.textContent ?? "", false) !== "");
767
856
  }
857
+ const openingMarker = /^zr:1:((?:0|[1-9]\d*)(?:\.(?:0|[1-9]\d*))*):(c|f|t|h|l|s:[01]|i:[ns](?:[0-9a-f]{2})*|e)$/;
858
+ function openMarker(node) {
859
+ return node.nodeType === Node.COMMENT_NODE ? openingMarker.exec(node.textContent ?? "") : null;
860
+ }
768
861
  function matchingClose(nodes, start) {
769
- const open = nodes[start].textContent ?? "";
770
- const id = /^zr:1:(\d+):/.exec(open)?.[1];
771
- if (id === undefined)
862
+ const first = openMarker(nodes[start]);
863
+ if (!first)
772
864
  return -1;
773
- let depth = 0;
865
+ const stack = [first[1]];
774
866
  for (let i = start + 1; i < nodes.length; i++) {
775
- const value = nodes[i].textContent;
776
- if (value?.startsWith("zr:1:"))
777
- depth++;
778
- else if (value?.startsWith("/zr:1:")) {
779
- if (depth)
780
- depth--;
781
- else
782
- return value === `/zr:1:${id}` ? i : -1;
867
+ const node = nodes[i];
868
+ if (node.nodeType !== Node.COMMENT_NODE)
869
+ continue;
870
+ const value = node.textContent ?? "";
871
+ const open = openMarker(node);
872
+ if (open)
873
+ stack.push(open[1]);
874
+ else if (value.startsWith("/zr:1:")) {
875
+ if (value !== `/zr:1:${stack.pop()}`)
876
+ return -1;
877
+ if (!stack.length)
878
+ return i;
783
879
  }
880
+ else if (value.includes("zr:1:"))
881
+ return -1;
784
882
  }
785
883
  return -1;
786
884
  }
@@ -793,6 +891,12 @@ function compareChildren(expected, actual, map, context, path, pre = false) {
793
891
  const wanted = wants[i];
794
892
  const found = has[j];
795
893
  const childPath = `${path}/${wanted.nodeName.toLowerCase()}[${i}]`;
894
+ const boundary = context.errorRanges.get(wanted);
895
+ if (boundary) {
896
+ compare(wanted, found, map, context, childPath, pre);
897
+ [i, j] = boundary(wants, has, i, j, map, childPath);
898
+ continue;
899
+ }
796
900
  if (context.opaque.has(wanted)) {
797
901
  compare(wanted, found, map, context, childPath, pre);
798
902
  const wantedEnd = matchingClose(wants, i);
@@ -861,7 +965,7 @@ function execute(node, container, options, mode) {
861
965
  (node.type.displayName ?? node.type.name) !==
862
966
  options.identity.component)
863
967
  fail("ZR_IDENTITY", "preflight", options.identity.component, "different child component", rootPath(container));
864
- scope = createScope({
968
+ scope = new RuntimeScope({
865
969
  component: options.identity.component,
866
970
  reporter: (value) => report(options, value),
867
971
  protocol,
@@ -877,52 +981,36 @@ function execute(node, container, options, mode) {
877
981
  next: 0,
878
982
  operations: [],
879
983
  forms: [],
880
- formOwners: new Map(),
881
- opaque: new Set(),
882
- textSlots: new Set(),
883
- displayOnlyTextareas: new Set(),
984
+ formOwners: new WeakMap(),
985
+ opaque: new WeakSet(),
986
+ textSlots: new WeakSet(),
987
+ displayOnlyTextareas: new WeakSet(),
988
+ errorRanges: new WeakMap(),
989
+ replacements: [],
884
990
  dynamicRanges: [],
885
991
  collectRootRanges: true,
886
992
  };
887
993
  const fragment = container.ownerDocument.createDocumentFragment();
888
994
  render(node, fragment, context, HTML, "", rootPath(container));
889
995
  context.collectRootRanges = false;
890
- const built = [...fragment.childNodes];
891
- const map = new Map();
996
+ const map = mode === "hydrate" ? new Map() : selfMap(fragment);
892
997
  if (mode === "hydrate")
893
998
  compareChildren(fragment, container, map, context, rootPath(container));
894
- else {
895
- for (const expected of built) {
896
- const visit = (item) => {
897
- map.set(item, item);
898
- for (const child of item.childNodes)
899
- visit(child);
900
- };
901
- visit(expected);
902
- }
903
- }
904
999
  const formBindings = prepareForms(context.forms, map, mode, container);
905
1000
  if (options.signal?.aborted || !container.isConnected)
906
1001
  fail("ZR_CANCELLED", "commit", "connected root", "aborted or disconnected", rootPath(container));
907
- reconcileForms(formBindings, mode);
1002
+ reconcileForms(formBindings, mode, context.container);
908
1003
  if (mode === "mount")
909
1004
  container.replaceChildren(fragment);
1005
+ else
1006
+ for (const replace of context.replacements)
1007
+ replace();
1008
+ context.replacements.length = 0;
910
1009
  committed = true;
911
1010
  for (const operation of context.operations)
912
1011
  operation(map, cleanups);
913
- const formOwnerByNode = new Map();
914
- for (const [plan, owner] of context.formOwners)
915
- formOwnerByNode.set(map.get(plan.element), owner);
916
- const rootForms = formBindings.filter((binding) => !formOwnerByNode.has(binding.node));
917
- activateForms(rootForms, scope, cleanups, options, container);
918
- const ownedForms = new Map();
919
- for (const binding of formBindings) {
920
- const owner = formOwnerByNode.get(binding.node);
921
- if (owner)
922
- ownedForms.set(owner, [...(ownedForms.get(owner) ?? []), binding]);
923
- }
924
- for (const [owner, bindings] of ownedForms)
925
- activateForms(bindings, owner.scope, owner.cleanups, options, container);
1012
+ activateOwnedForms(formBindings, context.forms, map, context.formOwners, context, scope, cleanups);
1013
+ context.forms.length = 0;
926
1014
  const owned = rootOwned(container, context, map);
927
1015
  context.dynamicRanges.length = 0;
928
1016
  const handle = createRoot(container, options, scope, cleanups, owned);