From db1ba3673a103ebb813b522032734ff5a96f01a0 Mon Sep 17 00:00:00 2001 From: Luke LaValva Date: Mon, 4 Mar 2024 08:41:18 -0800 Subject: [PATCH] fix: hydration in for loops (#2134) * fix: hydration in for loops --------- Co-authored-by: Michael Rawlings --- .sizes.json | 48 +-- packages/runtime-tags/src/dom/control-flow.ts | 5 +- .../__snapshots__/html.expected/template.js | 13 +- .../resume-sanitized.expected.md | 195 +++++++++ .../__snapshots__/resume.expected.md | 369 ++++++++++++++++++ .../__snapshots__/ssr-sanitized.expected.md | 39 ++ .../__snapshots__/ssr.expected.md | 134 +++++++ .../fixtures/basic-nested-scope-for/test.ts | 2 - packages/translator-tags/src/core/for.ts | 14 +- 9 files changed, 783 insertions(+), 36 deletions(-) create mode 100644 packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/resume-sanitized.expected.md create mode 100644 packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/resume.expected.md create mode 100644 packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/ssr-sanitized.expected.md create mode 100644 packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/ssr.expected.md diff --git a/.sizes.json b/.sizes.json index c9664d79e..c1c3c345b 100644 --- a/.sizes.json +++ b/.sizes.json @@ -7,9 +7,9 @@ { "name": "*", "total": { - "min": 12644, - "gzip": 5391, - "brotli": 4935 + "min": 12664, + "gzip": 5394, + "brotli": 4922 } }, { @@ -21,12 +21,12 @@ }, "runtime": { "min": 3821, - "gzip": 1808, + "gzip": 1800, "brotli": 1614 }, "total": { "min": 4172, - "gzip": 2083, + "gzip": 2075, "brotli": 1852 } }, @@ -34,17 +34,17 @@ "name": "counter 💧", "user": { "min": 204, - "gzip": 180, + "gzip": 182, "brotli": 157 }, "runtime": { "min": 2683, - "gzip": 1362, + "gzip": 1370, "brotli": 1222 }, "total": { "min": 2887, - "gzip": 1542, + "gzip": 1552, "brotli": 1379 } }, @@ -52,36 +52,36 @@ "name": "comments", "user": { "min": 1182, - "gzip": 698, - "brotli": 639 + "gzip": 703, + "brotli": 637 }, "runtime": { - "min": 7343, - "gzip": 3386, - "brotli": 3083 + "min": 7363, + "gzip": 3405, + "brotli": 3091 }, "total": { - "min": 8525, - "gzip": 4084, - "brotli": 3722 + "min": 8545, + "gzip": 4108, + "brotli": 3728 } }, { "name": "comments 💧", "user": { "min": 949, - "gzip": 586, - "brotli": 543 + "gzip": 588, + "brotli": 544 }, "runtime": { - "min": 7867, - "gzip": 3596, - "brotli": 3269 + "min": 7887, + "gzip": 3612, + "brotli": 3284 }, "total": { - "min": 8816, - "gzip": 4182, - "brotli": 3812 + "min": 8836, + "gzip": 4200, + "brotli": 3828 } } ] diff --git a/packages/runtime-tags/src/dom/control-flow.ts b/packages/runtime-tags/src/dom/control-flow.ts index 57e31ae0d..3fd68e69f 100644 --- a/packages/runtime-tags/src/dom/control-flow.ts +++ b/packages/runtime-tags/src/dom/control-flow.ts @@ -315,7 +315,10 @@ export function inLoopScope( ) { const loopScopeAccessor = loopNodeAccessor + AccessorChar.LoopScopeArray; return (scope: Scope, clean?: boolean | 1) => { - const loopScopes = scope[loopScopeAccessor] ?? []; + const loopScopes = + scope[loopScopeAccessor] ?? + scope[loopNodeAccessor + AccessorChar.LoopScopeMap]?.values() ?? + []; for (const scope of loopScopes) { signal(scope, clean); } diff --git a/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/html.expected/template.js b/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/html.expected/template.js index db8d41ccd..d8ab80148 100644 --- a/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/html.expected/template.js +++ b/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/html.expected/template.js @@ -1,18 +1,25 @@ -import { attr as _attr, escapeXML as _escapeXML, markResumeNode as _markResumeNode, write as _write, serializedScope as _serializedScope, writeEffect as _writeEffect, writeScope as _writeScope, nextScopeId as _nextScopeId, maybeFlush as _maybeFlush, createRenderer as _createRenderer, createTemplate as _createTemplate } from "@marko/runtime-tags/debug/html"; +import { attr as _attr, escapeXML as _escapeXML, markResumeNode as _markResumeNode, markResumeControlSingleNodeEnd as _markResumeControlSingleNodeEnd, write as _write, serializedScope as _serializedScope, writeEffect as _writeEffect, writeScope as _writeScope, nextScopeId as _nextScopeId, maybeFlush as _maybeFlush, createRenderer as _createRenderer, createTemplate as _createTemplate } from "@marko/runtime-tags/debug/html"; const _renderer = /* @__PURE__ */_createRenderer((input, _tagVar) => { const _scope0_id = _nextScopeId(); const selected = 0; + const _forScopeIds = [], + _scope1_ = new Map(); + let _i2 = 0; for (const num of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) { const _scope1_id = _nextScopeId(); + let _i = _i2++; + _forScopeIds.push(_scope1_id); _write(`${_escapeXML(num)}${_markResumeNode(_scope1_id, "#text/1")}${_markResumeNode(_scope1_id, "#button/0")}`); _writeEffect(_scope1_id, "packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/template.marko_1_num"); - _writeScope(_scope1_id, { + _writeScope(_scope1_id, (_s => (_scope1_.set(_i, _s), _s))({ "num": num, "_": _serializedScope(_scope0_id) - }); + })); _maybeFlush(); } + _write(`${_markResumeControlSingleNodeEnd(_scope0_id, "#text/0", _forScopeIds)}`); _writeScope(_scope0_id, { + "#text/0(": _scope1_.size ? _scope1_ : undefined, "selected": selected }); }); diff --git a/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/resume-sanitized.expected.md b/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/resume-sanitized.expected.md new file mode 100644 index 000000000..2434b359c --- /dev/null +++ b/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/resume-sanitized.expected.md @@ -0,0 +1,195 @@ +# Render {} +```html + + + + + + + + + + + + +``` + + +# Render +c => click(c, 2) + +```html + + + + + + + + + + + + +``` + + +# Render +c => click(c, 3) + +```html + + + + + + + + + + + + +``` + + +# Render +c => click(c, 5) + +```html + + + + + + + + + + + + +``` \ No newline at end of file diff --git a/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/resume.expected.md b/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/resume.expected.md new file mode 100644 index 000000000..281862701 --- /dev/null +++ b/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/resume.expected.md @@ -0,0 +1,369 @@ +# Render {} +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +# Mutations +``` + +``` + + +# Render +c => click(c, 2) + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +# Mutations +``` +#document/html0/body1/button2: attr(data-selected) null => "" +#document/html0/body1/button2: attr(data-multiple) null => "" +#document/html0/body1/button6: attr(data-multiple) null => "" +#document/html0/body1/button10: attr(data-multiple) null => "" +#document/html0/body1/button14: attr(data-multiple) null => "" +#document/html0/body1/button18: attr(data-multiple) null => "" +#document/html0/body1/button22: attr(data-multiple) null => "" +``` + + +# Render +c => click(c, 3) + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +# Mutations +``` +#document/html0/body1/button2: attr(data-selected) "" => null +#document/html0/body1/button2: attr(data-multiple) "" => null +#document/html0/body1/button4: attr(data-selected) null => "" +#document/html0/body1/button4: attr(data-multiple) null => "" +#document/html0/body1/button6: attr(data-multiple) "" => null +#document/html0/body1/button10: attr(data-multiple) "" => "" +#document/html0/body1/button14: attr(data-multiple) "" => null +#document/html0/body1/button16: attr(data-multiple) null => "" +#document/html0/body1/button18: attr(data-multiple) "" => null +#document/html0/body1/button22: attr(data-multiple) "" => "" +``` + + +# Render +c => click(c, 5) + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +# Mutations +``` +#document/html0/body1/button4: attr(data-selected) "" => null +#document/html0/body1/button4: attr(data-multiple) "" => null +#document/html0/body1/button8: attr(data-selected) null => "" +#document/html0/body1/button8: attr(data-multiple) null => "" +#document/html0/body1/button10: attr(data-multiple) "" => null +#document/html0/body1/button16: attr(data-multiple) "" => null +#document/html0/body1/button18: attr(data-multiple) null => "" +#document/html0/body1/button22: attr(data-multiple) "" => null +``` \ No newline at end of file diff --git a/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/ssr-sanitized.expected.md b/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/ssr-sanitized.expected.md new file mode 100644 index 000000000..bbf01da2c --- /dev/null +++ b/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/ssr-sanitized.expected.md @@ -0,0 +1,39 @@ +# Render "End" +```html + + + + + + + + + + + + +``` \ No newline at end of file diff --git a/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/ssr.expected.md b/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/ssr.expected.md new file mode 100644 index 000000000..43edc5a86 --- /dev/null +++ b/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/__snapshots__/ssr.expected.md @@ -0,0 +1,134 @@ +# Write + + + +# Render "End" +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +# Mutations +``` +inserted #document/html0 +inserted #document/html0/head0 +inserted #document/html0/body1 +inserted #document/html0/body1/button0 +inserted #document/html0/body1/button0/#text0 +inserted #document/html0/body1/button0/#comment1 +inserted #document/html0/body1/#comment1 +inserted #document/html0/body1/button2 +inserted #document/html0/body1/button2/#text0 +inserted #document/html0/body1/button2/#comment1 +inserted #document/html0/body1/#comment3 +inserted #document/html0/body1/button4 +inserted #document/html0/body1/button4/#text0 +inserted #document/html0/body1/button4/#comment1 +inserted #document/html0/body1/#comment5 +inserted #document/html0/body1/button6 +inserted #document/html0/body1/button6/#text0 +inserted #document/html0/body1/button6/#comment1 +inserted #document/html0/body1/#comment7 +inserted #document/html0/body1/button8 +inserted #document/html0/body1/button8/#text0 +inserted #document/html0/body1/button8/#comment1 +inserted #document/html0/body1/#comment9 +inserted #document/html0/body1/button10 +inserted #document/html0/body1/button10/#text0 +inserted #document/html0/body1/button10/#comment1 +inserted #document/html0/body1/#comment11 +inserted #document/html0/body1/button12 +inserted #document/html0/body1/button12/#text0 +inserted #document/html0/body1/button12/#comment1 +inserted #document/html0/body1/#comment13 +inserted #document/html0/body1/button14 +inserted #document/html0/body1/button14/#text0 +inserted #document/html0/body1/button14/#comment1 +inserted #document/html0/body1/#comment15 +inserted #document/html0/body1/button16 +inserted #document/html0/body1/button16/#text0 +inserted #document/html0/body1/button16/#comment1 +inserted #document/html0/body1/#comment17 +inserted #document/html0/body1/button18 +inserted #document/html0/body1/button18/#text0 +inserted #document/html0/body1/button18/#comment1 +inserted #document/html0/body1/#comment19 +inserted #document/html0/body1/button20 +inserted #document/html0/body1/button20/#text0 +inserted #document/html0/body1/button20/#comment1 +inserted #document/html0/body1/#comment21 +inserted #document/html0/body1/button22 +inserted #document/html0/body1/button22/#text0 +inserted #document/html0/body1/button22/#comment1 +inserted #document/html0/body1/#comment23 +inserted #document/html0/body1/#comment24 +inserted #document/html0/body1/script25 +inserted #document/html0/body1/script25/#text0 +``` \ No newline at end of file diff --git a/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/test.ts b/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/test.ts index d2e0a5a59..ea79a7a2f 100644 --- a/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/test.ts +++ b/packages/translator-tags/src/__tests__/fixtures/basic-nested-scope-for/test.ts @@ -12,5 +12,3 @@ function click(container: Element, number: number) { const button = buttons.find((b) => b.textContent === "" + number)!; button.click(); } - -export const skip_ssr = true; diff --git a/packages/translator-tags/src/core/for.ts b/packages/translator-tags/src/core/for.ts index a7cbe1cf2..62bfcbaf9 100644 --- a/packages/translator-tags/src/core/for.ts +++ b/packages/translator-tags/src/core/for.ts @@ -5,7 +5,7 @@ import { getTagDef, } from "@marko/babel-utils"; import { types as t } from "@marko/compiler"; -import { WalkCode } from "@marko/runtime-tags/common/types"; +import { AccessorChar, WalkCode } from "@marko/runtime-tags/common/types"; import { isOutputHTML } from "../util/marko-config"; import analyzeAttributeTags from "../util/nested-attribute-tags"; import { mergeReferences } from "../util/references"; @@ -282,7 +282,7 @@ const translateHTML = { let byParams: t.Expression[]; let keyExpression: t.Expression | undefined = t.identifier("NOO"); - if (isStateful) { + if (isStateful || bodySection.closures) { setRegisterScopeBuilder(tag, (scope: t.Expression) => { const tempScopeIdentifier = currentProgramPath.scope.generateUidIdentifier("s"); @@ -363,7 +363,7 @@ const translateHTML = { valParam = tempValParam; } - if (indexParam || isStateful) { + if (indexParam || isStateful || bodySection.closures) { indexParam ??= currentProgramPath.scope.generateUidIdentifier("i"); const indexName = tag.scope.generateUidIdentifierBasedOnNode( indexParam, @@ -421,7 +421,7 @@ const translateHTML = { const stepName = tag.scope.generateUidIdentifier("step"); const fromName = tag.scope.generateUidIdentifier("from"); - if (indexParam || isStateful) { + if (indexParam || isStateful || bodySection.closures) { indexParam ??= currentProgramPath.scope.generateUidIdentifier("i"); keyExpression = indexParam as t.Identifier; block.body.unshift( @@ -466,7 +466,7 @@ const translateHTML = { ); } - if (isStateful) { + if (isStateful || bodySection.closures) { const forScopeIdsIdentifier = tag.scope.generateUidIdentifier("forScopeIds"); const forScopesIdentifier = getScopeIdentifier(bodySection); @@ -511,7 +511,9 @@ const translateHTML = { )}`; } getSerializedScopeProperties(tagSection).set( - t.stringLiteral(getScopeAccessorLiteral(reserve!).value + "("), + t.stringLiteral( + getScopeAccessorLiteral(reserve!).value + AccessorChar.LoopScopeMap, + ), t.conditionalExpression( t.memberExpression(forScopesIdentifier, t.identifier("size")), forScopesIdentifier,