diff --git a/.changeset/empty-text-resume.md b/.changeset/empty-text-resume.md new file mode 100644 index 00000000000..99b7e25f24c --- /dev/null +++ b/.changeset/empty-text-resume.md @@ -0,0 +1,9 @@ +--- +"@marko/runtime-tags": patch +--- + +Fix resume claiming the wrong DOM node for a placeholder that renders empty on the server (eg `
${value}` where `value` starts empty and is assigned later). Placeholders now serialize a single `_text_resume`/`_html_resume` call which writes an `EmptyText` resume marker when the rendered text is empty, letting resume create the text node instead of guessing a neighbor. + +Fix resume of an unescaped placeholder (`$!{value}`) whose markup parses to multiple top-level nodes: the server now brackets the markup with range markers so resume reconstructs the full range (including its last-child accessor), instead of claiming only the final node and orphaning the rest on the first client update. + +This also removes the `` separator comments previously written for empty positions and shrinks both the SSR output and the client resume runtime. diff --git a/.sizes.json b/.sizes.json index 6f4959ec3fe..38e54fc6f8b 100644 --- a/.sizes.json +++ b/.sizes.json @@ -7,8 +7,8 @@ { "name": "*", "total": { - "min": 26526, - "brotli": 9878 + "min": 26586, + "brotli": 9948 } }, { @@ -33,12 +33,12 @@ "brotli": 82 }, "runtime": { - "min": 2650, - "brotli": 1302 + "min": 2612, + "brotli": 1282 }, "total": { - "min": 2731, - "brotli": 1384 + "min": 2693, + "brotli": 1364 } }, { @@ -63,12 +63,12 @@ "brotli": 101 }, "runtime": { - "min": 2855, - "brotli": 1378 + "min": 2817, + "brotli": 1357 }, "total": { - "min": 2970, - "brotli": 1479 + "min": 2932, + "brotli": 1458 } } ] diff --git a/.sizes/comments.ssr/runtime.js b/.sizes/comments.ssr/runtime.js index d6f79ec903d..d697b3dcae8 100644 --- a/.sizes/comments.ssr/runtime.js +++ b/.sizes/comments.ssr/runtime.js @@ -1,4 +1,4 @@ -// size: 2855 (min) 1378 (brotli) +// size: 2817 (min) 1357 (brotli) //#region packages/runtime-tags/dist/dom.mjs let decodeAccessor = (num) => (num + (num < 26 ? 10 : num < 962 ? 334 : 11998)).toString(36), rendering, @@ -206,19 +206,14 @@ function init(runtimeId = "M") { if ((processResumes(render.r, effects), readyIds)); let retained = 0; for (visit of (visits = render.v)) - if ( - ((lastTokenIndex = render.i.length), + ((lastTokenIndex = render.i.length), (visitText = visit.data), (visitType = visitText[lastTokenIndex++]), (visitScope = getScope(nextToken())), - visitType === "*") - ) { - let prev = visit.previousSibling; - visitScope[nextToken()] = - prev && (prev.nodeType < 8 || prev.data) - ? prev - : visit.parentNode.insertBefore(new Text(), visit); - } + (visitScope[nextToken()] = + visitType === "$" + ? visit.previousSibling + : visit.parentNode.insertBefore(new Text(), visit))); return ((visits.length = retained), effects); }), (render.w = () => { diff --git a/.sizes/counter.ssr/runtime.js b/.sizes/counter.ssr/runtime.js index 6b101b8d601..41d3a9af9f0 100644 --- a/.sizes/counter.ssr/runtime.js +++ b/.sizes/counter.ssr/runtime.js @@ -1,4 +1,4 @@ -// size: 2650 (min) 1302 (brotli) +// size: 2612 (min) 1282 (brotli) //#region packages/runtime-tags/dist/dom.mjs let decodeAccessor = (num) => (num + (num < 26 ? 10 : num < 962 ? 334 : 11998)).toString(36), rendering, @@ -203,19 +203,14 @@ function init(runtimeId = "M") { if ((processResumes(render.r, effects), readyIds)); let retained = 0; for (visit of (visits = render.v)) - if ( - ((lastTokenIndex = render.i.length), + ((lastTokenIndex = render.i.length), (visitText = visit.data), (visitType = visitText[lastTokenIndex++]), (visitScope = getScope(nextToken())), - visitType === "*") - ) { - let prev = visit.previousSibling; - visitScope[nextToken()] = - prev && (prev.nodeType < 8 || prev.data) - ? prev - : visit.parentNode.insertBefore(new Text(), visit); - } + (visitScope[nextToken()] = + visitType === "$" + ? visit.previousSibling + : visit.parentNode.insertBefore(new Text(), visit))); return ((visits.length = retained), effects); }), (render.w = () => { diff --git a/.sizes/dom.js b/.sizes/dom.js index 65b9b93e2d0..79def7d366e 100644 --- a/.sizes/dom.js +++ b/.sizes/dom.js @@ -1,4 +1,4 @@ -// size: 26526 (min) 9878 (brotli) +// size: 26586 (min) 9948 (brotli) //#region packages/runtime-tags/dist/dom.mjs let unsafeStyleAttrReg = /[\\;]/g, replaceUnsafeStyleAttr = (c) => (c === ";" ? "\\3B " : "\\\\"), @@ -19,6 +19,7 @@ let unsafeStyleAttrReg = /[\\;]/g, }, decodeAccessor = (num) => (num + (num < 26 ? 10 : num < 962 ? 334 : 11998)).toString(36), branchesEnabled, + dynamicHtmlEnabled, rendering, runId = 2, caughtError = /* @__PURE__ */ new WeakSet(), @@ -98,6 +99,19 @@ let unsafeStyleAttrReg = /[\\;]/g, lazyEnabled, isResuming, cloneCache = {}, + _html = /*@__PURE__*/ withDynamicHtml(function (scope, value, accessor) { + let firstChild = scope[accessor], + parentNode = firstChild.parentNode, + lastChild = scope["H" + accessor] || firstChild, + newContent = parseHTML(_to_text(value), parentNode.namespaceURI); + (insertChildNodes( + parentNode, + firstChild, + (scope[accessor] = newContent.firstChild || newContent.appendChild(new Text())), + (scope["H" + accessor] = newContent.lastChild), + ), + removeChildNodes(firstChild, lastChild)); + }), R = /[\p{L}\p{N}]/gu, inputType = "", controllableScripts = {}, @@ -400,6 +414,9 @@ function normalizeDynamicRenderer(value) { function withBranches(runtime) { return ((branchesEnabled = 1), runtime); } +function withDynamicHtml(runtime) { + return ((dynamicHtmlEnabled = 1), runtime); +} function _hoist_read_error() {} function _assert_hoist(value) {} function forIn(obj, cb) { @@ -953,7 +970,7 @@ function init(runtimeId = "M") { ); ((branch._ ??= visitScope), (branch.K = branch.S = startVisit), - visitType === "'" && (branch.a = startVisit)); + visitType === "(" && (branch.a = startVisit)); } else ((curBranchScopes = push(curBranchScopes, branch)), accessor && @@ -1028,6 +1045,7 @@ function init(runtimeId = "M") { lastToken, lastTokenIndex, visitBranches, + htmlStart, embedAnchor; return ( (serializeContext._ = registeredValues), @@ -1042,22 +1060,23 @@ function init(runtimeId = "M") { } let retained = 0; for (visit of (visits = render.v)) - if ( - ((lastTokenIndex = render.i.length), + ((lastTokenIndex = render.i.length), (visitText = visit.data), (visitType = visitText[lastTokenIndex++]), (visitScope = getScope(nextToken())), - visitType === "*") - ) { - let prev = visit.previousSibling; - visitScope[nextToken()] = - prev && (prev.nodeType < 8 || prev.data) - ? prev - : visit.parentNode.insertBefore(new Text(), visit); - } else - branchesEnabled - ? (visitBranches ||= createVisitBranches())() - : lazyEnabled && render.b && (visits[retained++] = visit); + dynamicHtmlEnabled && visitType > "%" && visitType <= "'" + ? visitType === "&" + ? (htmlStart = visit) + : ((visitScope[nextToken()] = htmlStart), + (visitScope["H" + lastToken] = visit)) + : branchesEnabled && visitType > "'" + ? (visitBranches ||= createVisitBranches())() + : lazyEnabled && render.b && visitType > "%" + ? (visits[retained++] = visit) + : (visitScope[nextToken()] = + visitType === "$" + ? visit.previousSibling + : visit.parentNode.insertBefore(new Text(), visit))); return ( embedRenders && !embedAnchor && @@ -1313,19 +1332,6 @@ function _attrs_script(scope, nodeAccessor) { controllableScripts[scope["F" + nodeAccessor]]?.(scope, nodeAccessor); for (let name in events) _on(el, name, events[name]); } -function _html(scope, value, accessor) { - let firstChild = scope[accessor], - parentNode = firstChild.parentNode, - lastChild = scope["H" + accessor] || firstChild, - newContent = parseHTML(_to_text(value), parentNode.namespaceURI); - (insertChildNodes( - parentNode, - firstChild, - (scope[accessor] = newContent.firstChild || newContent.appendChild(new Text())), - (scope["H" + accessor] = newContent.lastChild), - ), - removeChildNodes(firstChild, lastChild)); -} function normalizeClientRender(value) { let renderer = normalizeDynamicRenderer(value); if (renderer && renderer.a) return renderer; diff --git a/packages/runtime-tags/src/__tests__/fixtures-interop/custom-tag-parameters-from-args/sizes.json b/packages/runtime-tags/src/__tests__/fixtures-interop/custom-tag-parameters-from-args/sizes.json index 7e1db2d9589..ed69d2c7d97 100644 --- a/packages/runtime-tags/src/__tests__/fixtures-interop/custom-tag-parameters-from-args/sizes.json +++ b/packages/runtime-tags/src/__tests__/fixtures-interop/custom-tag-parameters-from-args/sizes.json @@ -2,8 +2,8 @@ "dom": { "template.marko.page.mjs": { "total": { - "min": 55532, - "brotli": 17663 + "min": 55495, + "brotli": 17664 }, "files": { "components/custom-tag.marko": 626, diff --git a/packages/runtime-tags/src/__tests__/fixtures-interop/error-serialize-promise-class-to-tags/sizes.json b/packages/runtime-tags/src/__tests__/fixtures-interop/error-serialize-promise-class-to-tags/sizes.json index 43dd88ef883..61f58247c74 100644 --- a/packages/runtime-tags/src/__tests__/fixtures-interop/error-serialize-promise-class-to-tags/sizes.json +++ b/packages/runtime-tags/src/__tests__/fixtures-interop/error-serialize-promise-class-to-tags/sizes.json @@ -2,8 +2,8 @@ "dom": { "template.marko.page.mjs": { "total": { - "min": 64372, - "brotli": 20114 + "min": 64335, + "brotli": 20078 }, "files": { "components/tags-child.marko": 499, diff --git a/packages/runtime-tags/src/__tests__/fixtures-interop/error-unserializable-class-to-tags/__snapshots__/html.bundle.debug.js b/packages/runtime-tags/src/__tests__/fixtures-interop/error-unserializable-class-to-tags/__snapshots__/html.bundle.debug.js index 3648317f121..d80c354115a 100644 --- a/packages/runtime-tags/src/__tests__/fixtures-interop/error-unserializable-class-to-tags/__snapshots__/html.bundle.debug.js +++ b/packages/runtime-tags/src/__tests__/fixtures-interop/error-unserializable-class-to-tags/__snapshots__/html.bundle.debug.js @@ -5,7 +5,7 @@ var tags_child_default = _template("__tests__/components/tags-child.marko", (inp _scope_reason(); const $scope0_id = _scope_id(); let count = 0; - _html(`${_el_resume($scope0_id, "#button/0")}`); + _html(`${_el_resume($scope0_id, "#button/0")}`); _script($scope0_id, "__tests__/components/tags-child.marko_0"); writeScope($scope0_id, { input_data: input.data, diff --git a/packages/runtime-tags/src/__tests__/fixtures-interop/interop-attr-tags-tags-to-class/__snapshots__/html.bundle.debug.js b/packages/runtime-tags/src/__tests__/fixtures-interop/interop-attr-tags-tags-to-class/__snapshots__/html.bundle.debug.js index 0bbadbdd1df..af4ea8df97a 100644 --- a/packages/runtime-tags/src/__tests__/fixtures-interop/interop-attr-tags-tags-to-class/__snapshots__/html.bundle.debug.js +++ b/packages/runtime-tags/src/__tests__/fixtures-interop/interop-attr-tags-tags-to-class/__snapshots__/html.bundle.debug.js @@ -42,7 +42,7 @@ var template_default = _template("__tests__/template.marko", (input) => { _scope_reason(); const $scope0_id = _scope_id(); let count = 0; - _html(`${_el_resume($scope0_id, "#button/0")}`); + _html(`${_el_resume($scope0_id, "#button/0")}`); _dynamic_tag($scope0_id, "#text/2", _marko_template, { onSelect: _resume(function() { count++; diff --git a/packages/runtime-tags/src/__tests__/fixtures-interop/interop-attr-tags-tags-to-class/__snapshots__/writes.debug.html b/packages/runtime-tags/src/__tests__/fixtures-interop/interop-attr-tags-tags-to-class/__snapshots__/writes.debug.html index 0f75e4d9a5b..419d5eebee5 100644 --- a/packages/runtime-tags/src/__tests__/fixtures-interop/interop-attr-tags-tags-to-class/__snapshots__/writes.debug.html +++ b/packages/runtime-tags/src/__tests__/fixtures-interop/interop-attr-tags-tags-to-class/__snapshots__/writes.debug.html @@ -1,5 +1,5 @@ + id=tags>0