Skip to content

Commit

Permalink
feat: add await tag ssr
Browse files Browse the repository at this point in the history
  • Loading branch information
DylanPiercey committed Jan 9, 2025
1 parent 5cc3648 commit 491413d
Show file tree
Hide file tree
Showing 11 changed files with 349 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/few-shirts-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@marko/runtime-tags": patch
---

Add await tag ssr.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const _template_ = "<div><button>Inc</button></div>";
export const _walks_ = /* next(1), get, out(1) */"D l";
import { resolveAfter } from "../../utils/resolve";
import * as _$ from "@marko/runtime-tags/debug/dom";
const _value$await_content3 = /* @__PURE__ */_$.value("value", (_scope, value) => _$.data(_scope["#text/0"], value));
const _params_4$await_content = /* @__PURE__ */_$.value("_params_4", (_scope, _params_4) => _value$await_content3(_scope, _params_4[0]));
const _count$await_content3 = /* @__PURE__ */_$.closure("count", (_scope, count) => _$.data(_scope["#text/1"], count));
const _await_content3 = _$.register("__tests__/template.marko_3_renderer", /* @__PURE__ */_$.createRenderer("Got: <!> <!>", /* over(1), replace, over(2), replace */"b%c%", void 0, () => [_count$await_content3], () => _params_4$await_content));
const _value$await_content2 = /* @__PURE__ */_$.value("value", (_scope, value) => _$.data(_scope["#text/0"], value));
const _params_3$await_content = /* @__PURE__ */_$.value("_params_3", (_scope, _params_3) => _value$await_content2(_scope, _params_3[0]));
const _count$await_content2 = /* @__PURE__ */_$.closure("count", (_scope, count) => _$.data(_scope["#text/1"], count));
const _await_content2 = _$.register("__tests__/template.marko_2_renderer", /* @__PURE__ */_$.createRenderer("Got: <!> <!>", /* over(1), replace, over(2), replace */"b%c%", void 0, () => [_count$await_content2], () => _params_3$await_content));
const _value$await_content = /* @__PURE__ */_$.value("value", (_scope, value) => _$.data(_scope["#text/0"], value));
const _params_2$await_content = /* @__PURE__ */_$.value("_params_2", (_scope, _params_2) => _value$await_content(_scope, _params_2[0]));
const _count$await_content = /* @__PURE__ */_$.closure("count", (_scope, count) => _$.data(_scope["#text/1"], count));
const _await_content = _$.register("__tests__/template.marko_1_renderer", /* @__PURE__ */_$.createRenderer("Got: <!> <!>", /* over(1), replace, over(2), replace */"b%c%", void 0, () => [_count$await_content], () => _params_2$await_content));
const _count_effect = _$.effect("__tests__/template.marko_0_count", (_scope, {
count
}) => _$.on(_scope["#button/0"], "click", function () {
_count(_scope, count + 1), count;
}));
const _count = /* @__PURE__ */_$.state("count", (_scope, count) => _count_effect(_scope), () => _$.intersections([_count$await_content, _count$await_content2, _count$await_content3]));
export function _setup_(_scope) {
_count(_scope, 0);
}
export default /* @__PURE__ */_$.createTemplate("__tests__/template.marko", _template_, _walks_, _setup_);
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { resolveAfter } from "../../utils/resolve";
import * as _$ from "@marko/runtime-tags/debug/html";
const _renderer = /* @__PURE__ */_$.createRenderer((input, _tagVar) => {
const _scope0_id = _$.nextScopeId();
const count = 0;
_$.write("<div>");
_$.fork(Promise.resolve("a"), value => {
const _scope1_id = _$.nextScopeId();
_$.write(`Got: ${_$.escapeXML(value)} <!>${_$.escapeXML(count)}${_$.markResumeNode(_scope1_id, "#text/1")}`);
_$.writeScope(_scope1_id, {
"_": _$.ensureScopeWithId(_scope0_id)
});
});
_$.fork(resolveAfter("b", 2), value => {
const _scope2_id = _$.nextScopeId();
_$.write(`Got: ${_$.escapeXML(value)} <!>${_$.escapeXML(count)}${_$.markResumeNode(_scope2_id, "#text/1")}`);
_$.writeScope(_scope2_id, {
"_": _$.ensureScopeWithId(_scope0_id)
});
});
_$.fork(resolveAfter("c", 1), value => {
const _scope3_id = _$.nextScopeId();
_$.write(`Got: ${_$.escapeXML(value)} <!>${_$.escapeXML(count)}${_$.markResumeNode(_scope3_id, "#text/1")}`);
_$.writeScope(_scope3_id, {
"_": _$.ensureScopeWithId(_scope0_id)
});
});
_$.write(`<button>Inc</button>${_$.markResumeNode(_scope0_id, "#button/0")}</div>`);
_$.writeEffect(_scope0_id, "__tests__/template.marko_0_count");
_$.writeScope(_scope0_id, {
"count": count
});
});
export default /* @__PURE__ */_$.createTemplate("__tests__/template.marko", _renderer);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Render "End"
```html
<div>
Got: a 0Got: b 0Got: c 0
<button>
Inc
</button>
</div>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Write
<div>Got: a <!>0<!--M_*1 #text/1--><script>WALKER_RUNTIME("M")("_");M._.r=[_=>(_.b={0:_.a={count:0},1:{_:_.a}})]</script>


# Write
<script>M._.r.push(_=>(_.c={2:{_:_.a}}))</script>


# Write
Got: b <!>0<!--M_*3 #text/1-->Got: c <!>0<!--M_*2 #text/1--><button>Inc</button><!--M_*0 #button/0--></div><script>M._.r.push(_=>(_.d={3:{_:_.a}}),0,"__tests__/template.marko_0_count",0);M._.w()</script>


# Render "End"
```html
<html>
<head />
<body>
<div>
Got: a
<!---->
0
<!--M_*1 #text/1-->
<script>
WALKER_RUNTIME("M")("_");M._.r=[_=&gt;(_.b={0:_.a={count:0},1:{_:_.a}})]
</script>
<script>
M._.r.push(_=&gt;(_.c={2:{_:_.a}}))
</script>
Got: b
<!---->
0
<!--M_*3 #text/1-->
Got: c
<!---->
0
<!--M_*2 #text/1-->
<button>
Inc
</button>
<!--M_*0 #button/0-->
</div>
<script>
M._.r.push(_=&gt;(_.d={3:{_:_.a}}),0,"__tests__/template.marko_0_count",0);M._.w()
</script>
</body>
</html>
```

# Mutations
```
inserted #document/html0
inserted #document/html0/head0
inserted #document/html0/body1
inserted #document/html0/body1/div0
inserted #document/html0/body1/div0/#text0
inserted #document/html0/body1/div0/#comment1
inserted #document/html0/body1/div0/#text2
inserted #document/html0/body1/div0/#comment3
inserted #document/html0/body1/div0/script4
inserted #document/html0/body1/div0/script4/#text0
inserted #document/html0/body1/div0/script5
inserted #document/html0/body1/div0/script5/#text0
inserted #document/html0/body1/div0/#text6
inserted #document/html0/body1/div0/#comment7
inserted #document/html0/body1/div0/#text8
inserted #document/html0/body1/div0/#comment9
inserted #document/html0/body1/div0/#text10
inserted #document/html0/body1/div0/#comment11
inserted #document/html0/body1/div0/#text12
inserted #document/html0/body1/div0/#comment13
inserted #document/html0/body1/div0/button14
inserted #document/html0/body1/div0/button14/#text0
inserted #document/html0/body1/div0/#comment15
inserted #document/html0/body1/script1
inserted #document/html0/body1/script1/#text0
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { resolveAfter } from "../../utils/resolve";

<let/count = 0/>
<div>
<await|value|=Promise.resolve("a")>
Got: ${value} ${count}
</await>

<await|value|=resolveAfter("b", 2)>
Got: ${value} ${count}
</await>

<await|value|=resolveAfter("c", 1)>
Got: ${value} ${count}
</await>

<button onClick() {
count++
}>
Inc
</button>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const skip_csr = true;

export const steps = [{}, click, click, click];

function click(container: Element) {
container.querySelector("button")!.click();
}
159 changes: 159 additions & 0 deletions packages/runtime-tags/src/translator/core/await.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { types as t } from "@marko/compiler";
import {
assertNoArgs,
assertNoAttributeTags,
assertNoVar,
type Tag,
} from "@marko/compiler/babel-utils";

import { assertNoSpreadAttrs } from "../util/assert";
import evaluate from "../util/evaluate";
import { isStatefulReferences } from "../util/is-stateful";
import { BindingType, trackParamsReferences } from "../util/references";
import { callRuntime } from "../util/runtime";
import runtimeInfo from "../util/runtime-info";
import {
checkStatefulClosures,
getOrCreateSection,
getSectionForBody,
setSectionParentIsOwner,
} from "../util/sections";
import {
setForceResumeScope,
setSubscriberBuilder,
writeHTMLResumeStatements,
} from "../util/signals";
import { toFirstExpressionOrBlock } from "../util/to-first-expression-or-block";
import { translateByTarget } from "../util/visitors";
import * as writer from "../util/writer";

export default {
analyze(tag: t.NodePath<t.MarkoTag>) {
assertNoVar(tag);
assertNoArgs(tag);
assertNoSpreadAttrs(tag);
assertNoAttributeTags(tag);
const { node } = tag;
const [valueAttr] = node.attributes;

if (!valueAttr) {
throw tag
.get("name")
.buildCodeFrameError("The `await` tag requires a value.");
}

if (
node.attributes.length > 1 ||
!t.isMarkoAttribute(valueAttr) ||
valueAttr.name !== "value"
) {
throw tag
.get("name")
.buildCodeFrameError(
"The `await` tag only supports the `value` attribute.",
);
}

if (!node.body.body.length) {
throw tag
.get("name")
.buildCodeFrameError("The `await` tag requires body content.");
}

if (
node.body.params.length &&
(node.body.params.length > 1 || t.isSpreadElement(node.body.params[0]))
) {
throw tag
.get("name")
.buildCodeFrameError(
"The `await` tag only supports a single parameter.",
);
}

getOrCreateSection(tag);
trackParamsReferences(
tag.get("body"),
BindingType.derived,
undefined,
evaluate(valueAttr.value),
);
},
translate: translateByTarget({
html: {
enter(tag) {
const tagBody = tag.get("body");
const bodySection = getSectionForBody(tagBody);

if (!bodySection) {
tag.remove();
return;
}

setSectionParentIsOwner(bodySection, true);
writer.flushBefore(tag);
},
exit(tag) {
const { node } = tag;
const [valueAttr] = node.attributes;
const tagBody = tag.get("body");
const bodySection = getSectionForBody(tagBody)!;

if (
isStatefulReferences(valueAttr.extra?.referencedBindings) ||
checkStatefulClosures(bodySection, true)
) {
setForceResumeScope(bodySection);
}

writer.flushInto(tag);
// TODO: this is a hack to get around the fact that we don't have a way to
// know if a scope requires dynamic subscriptions
setSubscriberBuilder(tag, (() => {}) as any);
writeHTMLResumeStatements(tagBody);

tag
.replaceWith(
t.expressionStatement(
callRuntime(
"fork",
valueAttr.value,
t.arrowFunctionExpression(
node.body.params,
toFirstExpressionOrBlock(node.body.body),
),
),
),
)[0]
.skip();
},
},
dom: {
enter(tag) {
const tagBody = tag.get("body");
const bodySection = getSectionForBody(tagBody);

if (!bodySection) {
tag.remove();
return;
}

setSectionParentIsOwner(bodySection, true);
// TODO: this is a hack to get around the fact that we don't have a way to
// know if a scope requires dynamic subscriptions
setSubscriberBuilder(tag, (signal) => signal);
},
exit(tag) {
tag.remove();
},
},
}),
attributes: {},
autocomplete: [
{
description: "Use to consume asynchronous an data.",
descriptionMoreURL: "https://markojs.com/docs/core-tags/#await",
},
],
types: runtimeInfo.name + "/tag-types/await.d.marko",
} as Tag;
2 changes: 2 additions & 0 deletions packages/runtime-tags/src/translator/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import runtimeInfo from "../util/runtime-info";
import AttrsTag from "./attrs";
import AwaitTag from "./await";
import ClientTag from "./client";
import ConstTag from "./const";
import DebugTag from "./debug";
Expand All @@ -25,6 +26,7 @@ import StyleTag from "./style";
export default {
taglibId: runtimeInfo.taglibId,
"<attrs>": AttrsTag,
"<await>": AwaitTag,
"<client>": ClientTag,
"<const>": ConstTag,
"<debug>": DebugTag,
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-tags/src/translator/util/references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export function trackParamsReferences(
body.scope,
section,
paramsBinding,
undefined,
upstreamExpression,
i + "",
);
}
Expand Down
8 changes: 8 additions & 0 deletions packages/runtime-tags/tag-types/await.d.marko
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** File for types only, not actual implementation **/

export interface Input<T> {
value: T;
content: Marko.Body<[Awaited<T>]>
}

return=input.value

0 comments on commit 491413d

Please sign in to comment.