Repository: marko-js/marko

Issue:
input.content not rendering in the browser
### Marko Version: 6.0.116

### Details

I'm trying to create a component that conditionally renders the parent tag, passing through its content as follows.
```
<const/{ content, ...others }=input>
<${input.href ? "a" : "div"} ...others class=["tile", input.class]>
  <${input.content}/>
</>
```

Such a component still works for server-rendered content, but is broken for browser-rendered content.

This used to work in 6.0.115 but seems broken in 6.0.116.

Minimal reproduction https://github.com/vwong/marko6-broken-client-render


Discussion:
Thanks for the report and reproduction. Working on a patch!



Patch:
diff --git a/.changeset/quick-carrots-start.md b/.changeset/quick-carrots-start.md
new file mode 100644
index 0000000000..30be45749e
--- /dev/null
+++ b/.changeset/quick-carrots-start.md
@@ -0,0 +1,5 @@
+---
+"@marko/runtime-tags": patch
+---
+
+Fix issue where the ...rest alias optimization was applied incorrectly when there exists unrelated property aliases.
diff --git a/packages/runtime-tags/src/translator/util/binding-prop-tree.ts b/packages/runtime-tags/src/translator/util/binding-prop-tree.ts
index fb911d2494..abdfd44d58 100644
--- a/packages/runtime-tags/src/translator/util/binding-prop-tree.ts
+++ b/packages/runtime-tags/src/translator/util/binding-prop-tree.ts
@@ -1,6 +1,6 @@
 import { generateUid } from "./generate-uid";
-import { forEach } from "./optional";
-import { type Binding, BindingType } from "./references";
+import { forEach, type Opt } from "./optional";
+import { type Binding, BindingType, propsUtil } from "./references";
 
 export type BindingPropTree = {
   binding: Binding;
@@ -22,8 +22,7 @@ export function getBindingPropTree(binding: Binding) {
       }
     } else if (binding.aliases.size === 1) {
       const [restAlias] = binding.aliases;
-
-      if (restAlias.excludeProperties !== undefined) {
+      if (hasSupersetExcludeProperties(binding, restAlias.excludeProperties)) {
         props.rest = getBindingPropTree(restAlias);
         props.props = {};
 
@@ -47,3 +46,20 @@ export function getBindingPropTree(binding: Binding) {
 
   return props;
 }
+
+function hasSupersetExcludeProperties(
+  binding: Binding,
+  excludeProperties: Opt<string>,
+) {
+  if (excludeProperties === undefined) {
+    return false;
+  }
+
+  for (const prop of binding.propertyAliases.keys()) {
+    if (!propsUtil.has(excludeProperties, prop)) {
+      return false;
+    }
+  }
+
+  return true;
+}
diff --git a/packages/runtime-tags/src/translator/util/references.ts b/packages/runtime-tags/src/translator/util/references.ts
index d7a518942c..017702d0be 100644
--- a/packages/runtime-tags/src/translator/util/references.ts
+++ b/packages/runtime-tags/src/translator/util/references.ts
@@ -1352,7 +1352,10 @@ export const bindingUtil = new Sorted(function compareBindings(
           : a.id - b.id);
 });
 
-const propsUtil = new Sorted(function compareProps(a: string, b: string) {
+export const propsUtil = new Sorted(function compareProps(
+  a: string,
+  b: string,
+) {
   return a < b ? -1 : a > b ? 1 : 0;
 });
 


Test patch:
diff --git a/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/.name-cache.json b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/.name-cache.json
new file mode 100644
index 0000000000..bd8eff5262
--- /dev/null
+++ b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/.name-cache.json
@@ -0,0 +1,9 @@
+{
+  "vars": {
+    "props": {
+      "$_": "t",
+      "$init": "r",
+      "$$child_content__setup": "m"
+    }
+  }
+}
diff --git a/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/csr-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/csr-sanitized.expected.md
new file mode 100644
index 0000000000..c8b7192cdc
--- /dev/null
+++ b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/csr-sanitized.expected.md
@@ -0,0 +1,8 @@
+# Render
+```html
+<div
+  class="foo"
+>
+  1
+</div>
+```
diff --git a/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/csr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/csr.expected.md
new file mode 100644
index 0000000000..5a67bb9755
--- /dev/null
+++ b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/csr.expected.md
@@ -0,0 +1,13 @@
+# Render
+```html
+<div
+  class="foo"
+>
+  1
+</div>
+```
+
+# Mutations
+```
+INSERT div
+```
\ No newline at end of file
diff --git a/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/dom.expected/tags/child.js b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/dom.expected/tags/child.js
new file mode 100644
index 0000000000..504841a284
--- /dev/null
+++ b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/dom.expected/tags/child.js
@@ -0,0 +1,23 @@
+export const $template = "<div><!></div>";
+export const $walks = /* get, next(1), replace, out(1) */" D%l";
+export const $setup = () => {};
+import * as _ from "@marko/runtime-tags/debug/dom";
+const $input_class = /* @__PURE__ */_._const("input_class", $scope => _._attr_class($scope["#div/0"], [$scope.input_class, "foo"]));
+const $rest__script = _._script("__tests__/tags/child.marko_0_rest", $scope => _._attrs_script($scope, "#div/0"));
+const $rest = /* @__PURE__ */_._const("rest", $scope => {
+  _._attrs_partial($scope, "#div/0", $scope.rest, {
+    class: 1
+  });
+  $rest__script($scope);
+});
+const $dynamicTag = /* @__PURE__ */_._dynamic_tag("#text/1");
+const $content = /* @__PURE__ */_._const("content", $scope => $dynamicTag($scope, $scope.content));
+export const $input = /* @__PURE__ */_._const("input", $scope => {
+  (({
+    content,
+    ...rest
+  }) => $rest($scope, rest))($scope.input);
+  $input_class($scope, $scope.input.class);
+  $content($scope, $scope.input.content);
+});
+export default /* @__PURE__ */_._template("__tests__/tags/child.marko", $template, $walks, $setup, $input);
\ No newline at end of file
diff --git a/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/dom.expected/template.hydrate.js b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/dom.expected/template.hydrate.js
new file mode 100644
index 0000000000..deaff8376e
--- /dev/null
+++ b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/dom.expected/template.hydrate.js
@@ -0,0 +1,6 @@
+// size: 129 (min) 96 (brotli)
+_._script("a0", ($scope) => _._attrs_script($scope, "a"));
+const $child_content__setup = _._closure_get(1, ($scope) =>
+  _._text($scope.a, $scope._.b),
+);
+(_._content_resume("b0", " ", " b", $child_content__setup), init());
diff --git a/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/dom.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/dom.expected/template.js
new file mode 100644
index 0000000000..222f721ad5
--- /dev/null
+++ b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/dom.expected/template.js
@@ -0,0 +1,16 @@
+export const $template = _child_template;
+export const $walks = /* <child> */`/${_child_walks}&`;
+import * as _ from "@marko/runtime-tags/debug/dom";
+import { $setup as _child, $input as _child_input, $template as _child_template, $walks as _child_walks } from "./tags/child.marko";
+const $child_content__value = /* @__PURE__ */_._closure_get("value", $scope => _._text($scope["#text/0"], $scope._.value));
+const $child_content__setup = $child_content__value;
+const $child_content = _._content_resume("__tests__/template.marko_1_content", " ", /* get, over(1) */" b", $child_content__setup);
+const $value = /* @__PURE__ */_._const("value");
+export function $setup($scope) {
+  _child($scope["#childScope/0"]);
+  _child_input($scope["#childScope/0"], {
+    content: $child_content($scope)
+  });
+  $value($scope, 1);
+}
+export default /* @__PURE__ */_._template("__tests__/template.marko", $template, $walks, $setup);
\ No newline at end of file
diff --git a/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/html.expected/tags/child.js b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/html.expected/tags/child.js
new file mode 100644
index 0000000000..d4620ab835
--- /dev/null
+++ b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/html.expected/tags/child.js
@@ -0,0 +1,20 @@
+import * as _ from "@marko/runtime-tags/debug/html";
+export default _._template("__tests__/tags/child.marko", input => {
+  const $scope0_reason = _._scope_reason();
+  const $scope0_id = _._scope_id();
+  const {
+    content,
+    ...rest
+  } = input;
+  _._html(`<div${_._attr_class([input.class, "foo"])}${_._attrs_partial(rest, {
+    class: 1
+  }, "#div/0", $scope0_id, "div")}>`);
+  _._dynamic_tag($scope0_id, "#text/1", content, {}, 0, 0, _._serialize_guard($scope0_reason, /* input.content */0));
+  _._html(`</div>${_._el_resume($scope0_id, "#div/0")}`);
+  _._script($scope0_id, "__tests__/tags/child.marko_0_rest");
+  _._scope($scope0_id, {
+    rest
+  }, "__tests__/tags/child.marko", 0, {
+    rest: "1:22"
+  });
+});
\ No newline at end of file
diff --git a/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/html.expected/template.js b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/html.expected/template.js
new file mode 100644
index 0000000000..db72be87ab
--- /dev/null
+++ b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/html.expected/template.js
@@ -0,0 +1,21 @@
+import * as _ from "@marko/runtime-tags/debug/html";
+import _child from "./tags/child.marko";
+export default _._template("__tests__/template.marko", input => {
+  const $scope0_id = _._scope_id();
+  const value = 1;
+  _child({
+    content: _._content_resume("__tests__/template.marko_1_content", () => {
+      const $scope1_id = _._scope_id();
+      _._html(_._escape(value));
+      _._scope($scope1_id, {
+        _: _._scope_with_id($scope0_id)
+      }, "__tests__/template.marko", "2:2");
+      _._resume_branch($scope1_id);
+    }, $scope0_id)
+  });
+  _._scope($scope0_id, {
+    value
+  }, "__tests__/template.marko", 0, {
+    value: "1:8"
+  });
+});
\ No newline at end of file
diff --git a/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/resume-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/resume-sanitized.expected.md
new file mode 100644
index 0000000000..c8b7192cdc
--- /dev/null
+++ b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/resume-sanitized.expected.md
@@ -0,0 +1,8 @@
+# Render
+```html
+<div
+  class="foo"
+>
+  1
+</div>
+```
diff --git a/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/resume.expected.md b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/resume.expected.md
new file mode 100644
index 0000000000..76de43e74e
--- /dev/null
+++ b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/resume.expected.md
@@ -0,0 +1,30 @@
+# Render
+```html
+<html>
+  <head />
+  <body>
+    <div
+      class="foo"
+    >
+      1
+    </div>
+    <!--M_*2 #div/0-->
+    <script>
+      WALKER_RUNTIME("M")("_");
+      M._.r = [_ =&gt; (_.b = [0, _.a = {
+          value: 1
+        },
+        {
+          rest:
+          {}
+        },
+        {
+          _: _.a
+        }]),
+        "__tests__/tags/child.marko_0_rest 2"
+      ];
+      M._.w()
+    </script>
+  </body>
+</html>
+```
diff --git a/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/ssr-sanitized.expected.md b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/ssr-sanitized.expected.md
new file mode 100644
index 0000000000..7b2a574fd9
--- /dev/null
+++ b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/ssr-sanitized.expected.md
@@ -0,0 +1,8 @@
+# Render End
+```html
+<div
+  class="foo"
+>
+  1
+</div>
+```
diff --git a/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/ssr.expected.md b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/ssr.expected.md
new file mode 100644
index 0000000000..1118ae3fac
--- /dev/null
+++ b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/__snapshots__/ssr.expected.md
@@ -0,0 +1,47 @@
+# Write
+```html
+  <div class=foo>1</div><!--M_*2 #div/0--><script>WALKER_RUNTIME("M")("_");M._.r=[_=>(_.b=[0,_.a={value:1},{rest:{}},{_:_.a}]),"__tests__/tags/child.marko_0_rest 2"];M._.w()</script>
+```
+
+# Render End
+```html
+<html>
+  <head />
+  <body>
+    <div
+      class="foo"
+    >
+      1
+    </div>
+    <!--M_*2 #div/0-->
+    <script>
+      WALKER_RUNTIME("M")("_");
+      M._.r = [_ =&gt; (_.b = [0, _.a = {
+          value: 1
+        },
+        {
+          rest:
+          {}
+        },
+        {
+          _: _.a
+        }]),
+        "__tests__/tags/child.marko_0_rest 2"
+      ];
+      M._.w()
+    </script>
+  </body>
+</html>
+```
+
+# Mutations
+```
+INSERT html
+INSERT html/head
+INSERT html/body
+INSERT html/body/div
+INSERT html/body/div/#text
+INSERT html/body/#comment
+INSERT html/body/script
+INSERT html/body/script/#text
+```
\ No newline at end of file
diff --git a/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/tags/child.marko b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/tags/child.marko
new file mode 100644
index 0000000000..573d610aee
--- /dev/null
+++ b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/tags/child.marko
@@ -0,0 +1,4 @@
+<const/{ content, ...rest }=input>
+<div ...rest class=[input.class, "foo"]>
+  <${content}/>
+</div>
diff --git a/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/template.marko b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/template.marko
new file mode 100644
index 0000000000..2362ddb289
--- /dev/null
+++ b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/template.marko
@@ -0,0 +1,4 @@
+<const/value=1>
+<child>
+  ${value}
+</child>
\ No newline at end of file
diff --git a/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/test.ts b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/test.ts
new file mode 100644
index 0000000000..8aeb492a1b
--- /dev/null
+++ b/packages/runtime-tags/src/__tests__/fixtures/destructure-and-reference-unprovided-input/test.ts
@@ -0,0 +1,1 @@
+export const steps = [{}];