Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/branch-closure-repush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@marko/runtime-tags": patch
---

Stop serializing a closure value for an `<if>`/`<for>` body when the branch condition only changes together with the value: the owner signal pushes the fresh value before the branch is created, so the serialized copy was never read. Content passed through such a branch no longer needs a resume registration, letting otherwise static templates tree-shake out of the client bundle.
Branch sources excused this way still count toward a separate instantiation-flavored reason, so closures inside content that those branches can instantiate after resume keep their serialization.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
type: bug
impact: med
effort: high
site: packages/runtime-tags/src/translator/visitors/tag/dynamic-tag.ts › analyze
---

# Closures inside dynamic-tag content rendered directly in a `<for>` body are not serialized

When attribute-tag content is rendered by a bare `<${content}/>` in a `<for>` body (no wrapping `<if>`), a closure inside that content over unrelated outer state is never serialized, so an item added client-side after resume renders the closure value as empty. Wrapping the dynamic tag in `<if=content>` serializes correctly. Repro: `template.marko` with `<let/count=5/><let/n=1/><button onClick() { n++ }>add</button><sections><for|i| from=1 to=n><@section>${count}</@section></for></sections>` and `tags/sections.marko` of `<for|{ content }| of=input.section><div><${content}/></div></for>` — after hydrate, clicking renders `<div/>` where CSR renders `<div>5</div>`, and SSR writes neither `count` nor a content registration. Present on `main` (pre-dates the branch-closure serialization work); likely the direct dynamic-tag-content path skips the downstream reason chain that the `<if>` closure shape flows through.

Check: add the two files above as a fixture with `steps: [{}, click]` and `equivalent: false`; `render-ssr.md` and `render-csr.md` diverge on the appended item's text.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// tags/custom-tag.marko
var custom_tag_default = _template("__tests__/tags/custom-tag.marko", (input) => {
const $scope0_reason = _scope_reason(), $sg__input_test = _serialize_guard($scope0_reason, 3), $si__input_test = _serialize_if($scope0_reason, 3);
const $scope0_reason = _scope_reason(), $sg__input_test = _serialize_guard($scope0_reason, 3);
const $scope0_id = _scope_id();
_html(`<div${_attr_class(input.class)}></div>${_el_resume($scope0_id, "#div/0", _serialize_guard($scope0_reason, 2))}`);
_if(() => {
Expand All @@ -9,17 +9,11 @@ var custom_tag_default = _template("__tests__/tags/custom-tag.marko", (input) =>
_html(`<div${_attr_class(input.test.class)} id=test>`);
_dynamic_tag($scope1_id, "#text/1", input.test.content, {}, 0, 0, _serialize_guard($scope0_reason, 5));
_html(`</div>${_el_resume($scope1_id, "#div/0", _serialize_guard($scope0_reason, 4))}`);
$si__input_test && writeScope($scope1_id, { _: _serialize_if($scope0_reason, 1) && _scope_with_id($scope0_id) }, "__tests__/tags/custom-tag.marko", "3:2");
_serialize_if($scope0_reason, 3) && writeScope($scope1_id, { _: _serialize_if($scope0_reason, 1) && _scope_with_id($scope0_id) }, "__tests__/tags/custom-tag.marko", "3:2");
return 0;
}
}, $scope0_id, "#text/1", $sg__input_test, $sg__input_test, $sg__input_test, 0, 1);
_serialize_if($scope0_reason, 0) && writeScope($scope0_id, {
input_test_class: $si__input_test && input.test?.class,
input_test_content: $si__input_test && input.test?.content
}, "__tests__/tags/custom-tag.marko", 0, {
input_test_class: ["input.test.class"],
input_test_content: ["input.test.content"]
});
_serialize_if($scope0_reason, 0) && writeScope($scope0_id, {}, "__tests__/tags/custom-tag.marko", 0);
});

// template.marko
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// tags/custom-tag.marko
var custom_tag_default = _template("b", (input) => {
const $scope0_reason = _scope_reason(), $sg__input_test = _serialize_guard($scope0_reason, 3), $si__input_test = _serialize_if($scope0_reason, 3);
const $scope0_reason = _scope_reason(), $sg__input_test = _serialize_guard($scope0_reason, 3);
const $scope0_id = _scope_id();
_html(`<div${_attr_class(input.class)}></div>${_el_resume($scope0_id, "a", _serialize_guard($scope0_reason, 2))}`);
_if(() => {
Expand All @@ -9,14 +9,11 @@ var custom_tag_default = _template("b", (input) => {
_html(`<div${_attr_class(input.test.class)} id=test>`);
_dynamic_tag($scope1_id, "b", input.test.content, {}, 0, 0, _serialize_guard($scope0_reason, 5));
_html(`</div>${_el_resume($scope1_id, "a", _serialize_guard($scope0_reason, 4))}`);
$si__input_test && writeScope($scope1_id, { _: _serialize_if($scope0_reason, 1) && _scope_with_id($scope0_id) });
_serialize_if($scope0_reason, 3) && writeScope($scope1_id, { _: _serialize_if($scope0_reason, 1) && _scope_with_id($scope0_id) });
return 0;
}
}, $scope0_id, "b", $sg__input_test, $sg__input_test, $sg__input_test, 0, 1);
_serialize_if($scope0_reason, 0) && writeScope($scope0_id, {
g: $si__input_test && input.test?.class,
h: $si__input_test && input.test?.content
});
_serialize_if($scope0_reason, 0) && writeScope($scope0_id, {});
});

// template.marko
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// tags/custom-tag.marko
var custom_tag_default = _template("__tests__/tags/custom-tag.marko", (input) => {
const $scope0_reason = _scope_reason(), $sg__input_test = _serialize_guard($scope0_reason, 3), $si__input_test = _serialize_if($scope0_reason, 3);
const $scope0_reason = _scope_reason(), $sg__input_test = _serialize_guard($scope0_reason, 3);
const $scope0_id = _scope_id();
_html(`<div${_attr_style(input.style)}></div>${_el_resume($scope0_id, "#div/0", _serialize_guard($scope0_reason, 2))}`);
_if(() => {
Expand All @@ -9,17 +9,11 @@ var custom_tag_default = _template("__tests__/tags/custom-tag.marko", (input) =>
_html(`<div${_attr_style(input.test.style)} id=test>`);
_dynamic_tag($scope1_id, "#text/1", input.test.content, {}, 0, 0, _serialize_guard($scope0_reason, 5));
_html(`</div>${_el_resume($scope1_id, "#div/0", _serialize_guard($scope0_reason, 4))}`);
$si__input_test && writeScope($scope1_id, { _: _serialize_if($scope0_reason, 1) && _scope_with_id($scope0_id) }, "__tests__/tags/custom-tag.marko", "3:2");
_serialize_if($scope0_reason, 3) && writeScope($scope1_id, { _: _serialize_if($scope0_reason, 1) && _scope_with_id($scope0_id) }, "__tests__/tags/custom-tag.marko", "3:2");
return 0;
}
}, $scope0_id, "#text/1", $sg__input_test, $sg__input_test, $sg__input_test, 0, 1);
_serialize_if($scope0_reason, 0) && writeScope($scope0_id, {
input_test_style: $si__input_test && input.test?.style,
input_test_content: $si__input_test && input.test?.content
}, "__tests__/tags/custom-tag.marko", 0, {
input_test_style: ["input.test.style"],
input_test_content: ["input.test.content"]
});
_serialize_if($scope0_reason, 0) && writeScope($scope0_id, {}, "__tests__/tags/custom-tag.marko", 0);
});

// template.marko
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// tags/custom-tag.marko
var custom_tag_default = _template("b", (input) => {
const $scope0_reason = _scope_reason(), $sg__input_test = _serialize_guard($scope0_reason, 3), $si__input_test = _serialize_if($scope0_reason, 3);
const $scope0_reason = _scope_reason(), $sg__input_test = _serialize_guard($scope0_reason, 3);
const $scope0_id = _scope_id();
_html(`<div${_attr_style(input.style)}></div>${_el_resume($scope0_id, "a", _serialize_guard($scope0_reason, 2))}`);
_if(() => {
Expand All @@ -9,14 +9,11 @@ var custom_tag_default = _template("b", (input) => {
_html(`<div${_attr_style(input.test.style)} id=test>`);
_dynamic_tag($scope1_id, "b", input.test.content, {}, 0, 0, _serialize_guard($scope0_reason, 5));
_html(`</div>${_el_resume($scope1_id, "a", _serialize_guard($scope0_reason, 4))}`);
$si__input_test && writeScope($scope1_id, { _: _serialize_if($scope0_reason, 1) && _scope_with_id($scope0_id) });
_serialize_if($scope0_reason, 3) && writeScope($scope1_id, { _: _serialize_if($scope0_reason, 1) && _scope_with_id($scope0_id) });
return 0;
}
}, $scope0_id, "b", $sg__input_test, $sg__input_test, $sg__input_test, 0, 1);
_serialize_if($scope0_reason, 0) && writeScope($scope0_id, {
g: $si__input_test && input.test?.style,
h: $si__input_test && input.test?.content
});
_serialize_if($scope0_reason, 0) && writeScope($scope0_id, {});
});

// template.marko
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// tags/pager.marko
const $template$2 = "<nav></nav>";
const $walks$2 = " b";
const $setup$2 = () => {};
const $if_content__dynamicTag = /*@__PURE__*/ _dynamic_tag("#text/0");
const $if_content__input_start_content = /*@__PURE__*/ _if_closure("#nav/0", 0, ($scope) => $if_content__dynamicTag($scope, $scope._.input_start_content));
const $if_content__setup = $if_content__input_start_content;
const $if = /*@__PURE__*/ _if("#nav/0", "<span><!></span>", "D%", $if_content__setup);
const $input_start = ($scope, input_start) => {
$input_start_content($scope, input_start?.content);
$if($scope, input_start ? 0 : 1);
};
const $input$1 = ($scope, input) => $input_start($scope, input.start);
const $input_start_content = /*@__PURE__*/ _const("input_start_content", $if_content__input_start_content);
var pager_default = /*@__PURE__*/ _template("__tests__/tags/pager.marko", $template$2, " b", 0, $input$1);

// tags/page.marko
const $template$1 = $template$2;
const $walks$1 = /*@__PURE__*/ ((_w0) => `/${_w0}&`)(" b");
const $setup$1 = () => {};
const items$1 = [{ title: "First" }, { title: "Second" }];
const $start_content__next_title = /*@__PURE__*/ _closure_get("next_title", ($scope) => _text($scope["#text/0"], $scope._.next_title));
const $start_content__setup = $start_content__next_title;
const $start_content = /*@__PURE__*/ _content("__tests__/tags/page.marko_1*content", " ", " ", $start_content__setup);
const $next = /*@__PURE__*/ _const("next", ($scope) => {
let $start;
if ($scope.next) {
$start = attrTag({ content: $start_content($scope) });
}
$input_start($scope["#childScope/0"], $start);
$next_title($scope, $scope.next?.title);
});
const $next_title__closure = /*@__PURE__*/ _closure($start_content__next_title);
const $next_title = /*@__PURE__*/ _const("next_title", $next_title__closure);
const $input_index = ($scope, input_index) => $next($scope, items$1[input_index]);
const $input = ($scope, input) => $input_index($scope, input.index);
var page_default = /*@__PURE__*/ _template("__tests__/tags/page.marko", $template$1, $walks$1, $setup$1, $input);

// template.marko
const $template = /*@__PURE__*/ ((_w0) => `<button>next</button>${_w0}`)($template$1);
const $walks = /*@__PURE__*/ ((_w0) => ` b/${_w0}&`)($walks$1);
const items = [{ title: "First" }, { title: "Second" }];
const $index = /*@__PURE__*/ _let("index/2", ($scope) => $input_index($scope["#childScope/1"], $scope.index));
const $setup__script = _script("__tests__/template.marko_0", ($scope) => _on($scope["#button/0"], "click", function() {
$index($scope, ($scope.index + 1) % 3);
}));
function $setup($scope) {
/* @__PURE__ */ $setup$1($scope["#childScope/1"]);
$index($scope, 0);
$setup__script($scope);
}
var template_default = /*@__PURE__*/ _template("__tests__/template.marko", $template, $walks, $setup);
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// tags/pager.marko
const $if_content__dynamicTag = /*@__PURE__*/ _dynamic_tag(0);
const $if_content__input_start_content = /*@__PURE__*/ _if_closure(0, 0, ($scope) => $if_content__dynamicTag($scope, $scope._.e));
const $if = /*@__PURE__*/ _if(0, "<span><!></span>", "D%", $if_content__input_start_content);
const $input_start = ($scope, input_start) => {
$input_start_content($scope, input_start?.content);
$if($scope, input_start ? 0 : 1);
};
const $input_start_content = /*@__PURE__*/ _const(4, $if_content__input_start_content);

// tags/page.marko
const items = [{ title: "First" }, { title: "Second" }];
const $start_content__next_title = /*@__PURE__*/ _closure_get(6, ($scope) => _text($scope.a, $scope._.f));
const $start_content = /*@__PURE__*/ _content("b0", " ", " ", $start_content__next_title);
const $next = /*@__PURE__*/ _const(4, ($scope) => {
let $start;
if ($scope.e) $start = attrTag({ content: $start_content($scope) });
$input_start($scope.a, $start);
$next_title($scope, $scope.e?.title);
});
const $next_title = /*@__PURE__*/ _const(5, /* @__PURE__ */ _closure($start_content__next_title));
const $input_index = ($scope, input_index) => $next($scope, items[input_index]);

// template.marko
const $index = /*@__PURE__*/ _let(2, ($scope) => $input_index($scope.b, $scope.c));
const $setup__script = _script("a0", ($scope) => _on($scope.a, "click", function() {
$index($scope, ($scope.c + 1) % 3);
}));
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// tags/pager.marko
var pager_default = _template("__tests__/tags/pager.marko", (input) => {
const $scope0_reason = _scope_reason(), $sg__input_start = _serialize_guard($scope0_reason, 0), $si__input_start = _serialize_if($scope0_reason, 0);
const $scope0_id = _scope_id();
_html("<nav>");
_if(() => {
if (input.start) {
const $scope1_id = _scope_id();
_html("<span>");
_dynamic_tag($scope1_id, "#text/0", input.start.content, {}, 0, 0, _serialize_guard($scope0_reason, 1));
_html("</span>");
$si__input_start && writeScope($scope1_id, { _: _serialize_if($scope0_reason, 1) && _scope_with_id($scope0_id) }, "__tests__/tags/pager.marko", "2:4");
return 0;
}
}, $scope0_id, "#nav/0", $sg__input_start, $sg__input_start, $sg__input_start, "</nav>", 1);
$si__input_start && writeScope($scope0_id, {}, "__tests__/tags/pager.marko", 0);
});

// tags/page.marko
const items$1 = [{ title: "First" }, { title: "Second" }];
var page_default = _template("__tests__/tags/page.marko", (input) => {
const $scope0_reason = _scope_reason(), $sg__input_index = _serialize_guard($scope0_reason, 0), $si__input_index = _serialize_if($scope0_reason, 0);
const $scope0_id = _scope_id();
const $next_title__closures = new Set();
const next = items$1[input.index];
_set_serialize_reason({
0: $sg__input_index,
1: $sg__input_index
});
let $start;
if (next) {
$start = attrTag({ content: _content("__tests__/tags/page.marko_1*content", () => {
const $scope1_reason = _scope_reason();
const $scope1_id = _scope_id();
_html(`${_escape(next.title)}${_el_resume($scope1_id, "#text/0", $sg__input_index)}`);
$si__input_index && _subscribe($next_title__closures, writeScope($scope1_id, { _: _scope_with_id($scope0_id) }, "__tests__/tags/page.marko", "6:6"));
_resume_branch($scope1_id);
}, $scope0_id) });
}
const $childScope = _peek_scope_id();
pager_default({ start: $start });
$si__input_index && writeScope($scope0_id, {
next_title: next?.title,
"ClosureScopes:next_title": $next_title__closures,
"#childScope/0": _existing_scope($childScope)
}, "__tests__/tags/page.marko", 0, { next_title: ["next.title", "3:8"] });
});

// template.marko
const items = [{ title: "First" }, { title: "Second" }];
var template_default = _template("__tests__/template.marko", (input) => {
_scope_reason();
const $scope0_id = _scope_id();
let index = 0;
_html(`<button>next</button>${_el_resume($scope0_id, "#button/0")}`);
_set_serialize_reason(1);
const $childScope = _peek_scope_id();
page_default({ index });
_script($scope0_id, "__tests__/template.marko_0");
writeScope($scope0_id, {
index,
"#childScope/1": _existing_scope($childScope)
}, "__tests__/template.marko", 0, { index: "2:6" });
_resume_branch($scope0_id);
}, 1);
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// tags/pager.marko
var pager_default = _template("c", (input) => {
const $scope0_reason = _scope_reason(), $sg__input_start = _serialize_guard($scope0_reason, 0), $si__input_start = _serialize_if($scope0_reason, 0);
const $scope0_id = _scope_id();
_html("<nav>");
_if(() => {
if (input.start) {
const $scope1_id = _scope_id();
_html("<span>");
_dynamic_tag($scope1_id, "a", input.start.content, {}, 0, 0, _serialize_guard($scope0_reason, 1));
_html("</span>");
$si__input_start && writeScope($scope1_id, { _: _serialize_if($scope0_reason, 1) && _scope_with_id($scope0_id) });
return 0;
}
}, $scope0_id, "a", $sg__input_start, $sg__input_start, $sg__input_start, "</nav>", 1);
$si__input_start && writeScope($scope0_id, {});
});

// tags/page.marko
const items = [{ title: "First" }, { title: "Second" }];
var page_default = _template("b", (input) => {
const $scope0_reason = _scope_reason(), $sg__input_index = _serialize_guard($scope0_reason, 0), $si__input_index = _serialize_if($scope0_reason, 0);
const $scope0_id = _scope_id();
const $next_title__closures = /* @__PURE__ */ new Set();
const next = items[input.index];
_set_serialize_reason({
0: $sg__input_index,
1: $sg__input_index
});
let $start;
if (next) $start = attrTag({ content: _content("b0", () => {
_scope_reason();
const $scope1_id = _scope_id();
_html(`${_escape(next.title)}${_el_resume($scope1_id, "a", $sg__input_index)}`);
$si__input_index && _subscribe($next_title__closures, writeScope($scope1_id, { _: _scope_with_id($scope0_id) }));
_resume_branch($scope1_id);
}, $scope0_id) });
const $childScope = _peek_scope_id();
pager_default({ start: $start });
$si__input_index && writeScope($scope0_id, {
f: next?.title,
g: $next_title__closures,
a: _existing_scope($childScope)
});
});

// template.marko
var template_default = _template("a", (input) => {
_scope_reason();
const $scope0_id = _scope_id();
let index = 0;
_html(`<button>next</button>${_el_resume($scope0_id, "a")}`);
_set_serialize_reason(1);
const $childScope = _peek_scope_id();
page_default({ index });
_script($scope0_id, "a0");
writeScope($scope0_id, {
c: index,
b: _existing_scope($childScope)
});
_resume_branch($scope0_id);
}, 1);
Loading