Skip to content

Commit d8a5a09

Browse files
DylanPierceyclaude
authored andcommitted
feat: add <context> core tag
Provide a value to a tag's body content and consume it anywhere below in the render tree via a statically resolved from= reference (tag name or relative template path). Server-only (immutable) contexts compile to zero client code -- enforced by an executable bundle assertion -- while mutable providers fan changes out to consumers fine-grained, resume subscriptions without re-rendering, resolve from client-created branches (including providers nested in keyed loop iterations), and support a writable := round trip.
1 parent 7f786f2 commit d8a5a09

247 files changed

Lines changed: 4828 additions & 162 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/context-core-tag.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
"marko": minor
3+
---
4+
5+
Add the `<context>` core tag: a provider/consumer mechanism resolved
6+
through the render tree (dynamic extent), not lexical authoring structure,
7+
so a provider works correctly even when consumers are reached through
8+
passed content.
9+
10+
```marko
11+
<context=settings>
12+
<header/> // sees settings
13+
<main/> // sees settings
14+
</context>
15+
```
16+
17+
```marko
18+
<context/settings from="<settings-provider>"/>
19+
```
20+
21+
A provider whose value is server-only (literals, module/`static` values,
22+
`$global`-derived expressions) contributes zero bytes to the client
23+
bundle: no provider import, no registration, no subscription machinery.
24+
25+
Mutable providers (values derived from `let`/`input` state) fan changes
26+
out to consumers fine-grained: each consumer re-runs individually when
27+
the provided value changes, resumed pages reattach subscriptions without
28+
re-rendering, and consumers created client-side (eg inside an `<if>`
29+
toggled after resume) resolve the provider through its serialized branch
30+
entry. The bind shorthand makes a provider writable:
31+
32+
```marko
33+
<let/cart=[]/>
34+
<context:=cart>
35+
<checkout-panel/> // may assign to its consumed `cart` variable
36+
</context>
37+
```
38+
39+
A writable consumer's assignment invokes the provider's change handler,
40+
so the write lands in provider state and fans back out to every
41+
consumer.
42+
43+
A template may consume its own context (`from=` resolving to the file
44+
itself): the consume resolves the nearest instance in the render tree,
45+
which enables recursive components and consume-inside-provide patterns.

agent-feedback/unclear.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,9 @@ Things that were hard to understand, and what would have clarified them. Format
77
`packages/runtime-tags/src/translator/util/runtime.ts:21` | 2026-07-02 | impact:low | effort:low
88

99
`pureDOMFunctions` includes `_template`, `_await_promise`, `_await_content`, `_load_template`, and `_load_setup`, yet those factories have observable side effects at call time: `_template` calls `_resume(id, renderer)` (`packages/runtime-tags/src/dom/template.ts:42`) and the await/load factories call `_enable_catch()`/`enableBranches()` latches. The annotations are sound only because of a non-obvious invariant: registration is needed exactly when the value can be referenced by a serialized register id, which requires the value to be reachable in the client module graph anyway, and the enable latches are re-triggered by whichever construct survives tree-shaking. Two independent reviews flagged these as possibly-unsound; a comment on `pureDOMFunctions` stating the invariant would prevent repeated re-derivation.
10+
11+
## Nested same-template `<context>` providers need an intervening resumable branch
12+
13+
`packages/runtime-tags/src/dom/context.ts` (`_context_branch`) / `html/context.ts` | 2026-07-09 | impact:low | effort:high
14+
15+
Same-template provider instances nested through content that never resumes (no state/closures between them) collide on one branch scope and hit the debug "share the same branch" error, because the caller-side `kBranchSerializeReason` set for a mutable provider's section does not thread through the content/dynamic-tag runtime guard (`_serialize_guard($scope_reason, holeIndex)`) the way `<if>`/`<for>` read it directly. A static parent-side `addSerializeReason(section.parent, true, sectionAccessor.binding)` in `finalizeReferences` was tried and does not reach the child's hole reason. Real recursive components carry per-instance state (which resumes the branch and resolves this naturally -- see the `context-self` fixture), so the gap is narrow; closing it is the cross-template serialize-reason edge the persisted-pages reach analysis owns.

cspell.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,8 @@
382382
"vmax",
383383
"cqmin",
384384
"cqmax",
385-
"whib"
385+
"whib",
386+
"reparenting"
386387
],
387388
"ignoreRegExpList": [],
388389
"files": ["*"],

packages/runtime-class/test/taglib-lookup/fixtures/getTagsSorted/expected.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"col",
4141
"colgroup",
4242
"const",
43+
"context",
4344
"data",
4445
"datalist",
4546
"dd",

packages/runtime-tags/src/__tests__/fixtures-interop/interop-basic-class-to-tags/sizes.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"dom": {
33
"template.marko.page.mjs": {
44
"total": {
5-
"min": 65847,
6-
"brotli": 20208
5+
"min": 65835,
6+
"brotli": 20207
77
},
88
"files": {
99
"components/tags-counter.marko": 681,

packages/runtime-tags/src/__tests__/fixtures-interop/interop-class-to-tags-import/sizes.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"dom": {
33
"template.marko.page.mjs": {
44
"total": {
5-
"min": 65847,
6-
"brotli": 20208
5+
"min": 65835,
6+
"brotli": 20207
77
},
88
"files": {
99
"components/tags-counter.marko": 681,

packages/runtime-tags/src/__tests__/fixtures-interop/interop-dynamic-tag-class-to-tags/sizes.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"dom": {
33
"template.marko.page.mjs": {
44
"total": {
5-
"min": 65745,
6-
"brotli": 20176
5+
"min": 65733,
6+
"brotli": 20160
77
},
88
"files": {
99
"components/tags-child.marko": 509,

packages/runtime-tags/src/__tests__/fixtures-interop/interop-dynamic-tag-conditional-class-to-tags/sizes.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"dom": {
33
"template.marko.page.mjs": {
44
"total": {
5-
"min": 65786,
6-
"brotli": 20187
5+
"min": 65774,
6+
"brotli": 20200
77
},
88
"files": {
99
"components/tags-child.marko": 509,

packages/runtime-tags/src/__tests__/fixtures-interop/interop-nested-attr-tags-class-to-tags/sizes.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"dom": {
33
"template.marko.page.mjs": {
44
"total": {
5-
"min": 66152,
6-
"brotli": 20390
5+
"min": 66140,
6+
"brotli": 20358
77
},
88
"files": {
99
"components/tags-layout.marko": 838,

packages/runtime-tags/src/__tests__/fixtures-interop/interop-nested-class-to-tags/sizes.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
"dom": {
33
"template.marko.page.mjs": {
44
"total": {
5-
"min": 65824,
6-
"brotli": 20220
5+
"min": 65812,
6+
"brotli": 20211
77
},
88
"files": {
99
"components/tags-layout.marko": 696,

0 commit comments

Comments
 (0)