Description
Marko v6 has no public context API. Every other major frontend framework provides one — React has createContext/useContext, Svelte has setContext/getContext, Vue has provide/inject, and Marko v5 had @marko-tags/context. Marko v6 currently has nothing equivalent.
The infrastructure already exists internally. The #ClosestBranch/#ParentBranch branch chain was clearly designed for this purpose — it is a tree of rendering branches that perfectly models the parent-to-child context relationship. The only missing pieces are:
- Export
addStatement and scopeIdentifier from the translator bundle so third-party translator packages can inject context reads/writes into the signal graph at compile time
- Runtime
setContext/getContext functions that walk the branch chain
- Built-in
<context-provider>/<context-consumer> tags using both of the above
These three parts are independently useful and independently mergeable.
Why
Wanting to build a Marko v6 adapter for [dnd-kit](https://github.com/clauderic/dnd-kit), a headless drag-and-drop library. The React, Svelte, and Vue adapters all provide an implicit context pattern where <create-draggable>, <create-droppable>, and <create-sortable> components automatically find their nearest <drag-drop-provider> ancestor without the user threading props through every intermediate component:
<drag-drop-provider onDragEnd(e) { items = move(items, e) }>
<for|item, index| of=items by="id">
<create-sortable id=item.id index=index>
<li>${item.text}</li>
</create-sortable>
</for>
</drag-drop-provider>
I started to impl this using a custom translator that writes the manager onto scope["#ClosestBranch"] during the render phase and walks the branch chain in consumer effects. The implementation is nearly there, The only blocker is that addStatement and scopeIdentifier are bundled into dist/translator/index.js but not exported making them inaccessible to third-party translators.
addStatement is used 47 times inside the bundle and never exported. The dist/translator/util/ and dist/translator/visitors/ directories only contain .d.ts type stubs & no .js implementations exist at those paths.
Without exported internals, the only alternative is a runtime workaround using DOM traversal and queueMicrotask to deliver context after mount. This works but defers entity registration by one microtask, adds hidden DOM elements, and cannot work during SSR. It appears less performant than the compile-time approach.
This gap also affects every other library author who needs implicit context like form libraries passing validation state to nested inputs, design systems passing theme tokens, router libraries passing route context to nested links. Context is a fundamental pattern for component library authorship and Marko v6 is currently without it.
Possible Implementation & Open Questions
Part 1 — Export translator internals (two lines, non-breaking):
In src/translator/util/signals.ts:
In src/translator/visitors/program/index.ts:
export { scopeIdentifier };
These are already part of the compilation contract. Exporting them is additive and non-breaking. They would be documented as lower-level APIs for translator authors, similar to how @babel/traverse exposes its internals for plugin authors.
Part 2 — Runtime context API :
New file src/runtime/context.ts, exported at @marko/runtime-tags/context:
const CLOSEST_BRANCH = "#ClosestBranch";
const PARENT_BRANCH = "#ParentBranch";
export function setContext(scope: object, key: string | symbol, value: unknown): void {
(scope as any)[CLOSEST_BRANCH][key] = value;
}
export function getContext<T = unknown>(scope: object, key: string | symbol): T | undefined {
let branch = (scope as any)[CLOSEST_BRANCH];
while (branch) {
if (key in branch) return branch[key] as T;
branch = branch[PARENT_BRANCH];
}
return undefined;
}
export function hasContext(scope: object, key: string | symbol): boolean {
let branch = (scope as any)[CLOSEST_BRANCH];
while (branch) {
if (key in branch) return true;
branch = branch[PARENT_BRANCH];
}
return false;
}
setContext must be called from a render statement (injected via addStatement("render", ...)) so the value is written during the $input phase before any consumer effects run. getContext is safe to call from effects since the render phase always completes first. This timing is equivalent to Svelte's synchronous context.
Part 3 — Built-in tags:
New built-in tags <context-provider> and <context-consumer> in the tags/ directory, with translators that use addStatement to wire context into the signal graph at compile time. The end-user API would be:
<context-provider key="theme" value="dark">
<themed-button/>
</context-provider>
<!-- in themed-button.marko -->
<context-consumer/theme key="theme"/>
${theme}
This mirrors the v5 @marko-tags/context package and gives Marko v6 a batteries-included context API.
Open questions:
- Should Part 1 exports be considered stable public API, or documented as
@internal with a deprecation escape hatch? would prefer stable but understand the concern about committing to internals.
- Should
getContext throw or return undefined when no ancestor provides the key? i'd lean toward undefined with a separate hasContext guard, matching Vue's inject behavior.
- Should symbol keys be supported? include them in our proposal since they prevent key collisions across libraries?
- Is there interest in an
@marko-tags/context-v6 standalone package as an interim step before the built-in tags land?
Is this something you're interested in working on?
Yes. if there are alternaye means to achieving it pls comment. thanks
Description
Marko v6 has no public context API. Every other major frontend framework provides one — React has
createContext/useContext, Svelte hassetContext/getContext, Vue hasprovide/inject, and Marko v5 had@marko-tags/context. Marko v6 currently has nothing equivalent.The infrastructure already exists internally. The
#ClosestBranch/#ParentBranchbranch chain was clearly designed for this purpose — it is a tree of rendering branches that perfectly models the parent-to-child context relationship. The only missing pieces are:addStatementandscopeIdentifierfrom the translator bundle so third-party translator packages can inject context reads/writes into the signal graph at compile timesetContext/getContextfunctions that walk the branch chain<context-provider>/<context-consumer>tags using both of the aboveThese three parts are independently useful and independently mergeable.
Why
Wanting to build a Marko v6 adapter for [dnd-kit](https://github.com/clauderic/dnd-kit), a headless drag-and-drop library. The React, Svelte, and Vue adapters all provide an implicit context pattern where
<create-draggable>,<create-droppable>, and<create-sortable>components automatically find their nearest<drag-drop-provider>ancestor without the user threading props through every intermediate component:I started to impl this using a custom translator that writes the manager onto
scope["#ClosestBranch"]during the render phase and walks the branch chain in consumer effects. The implementation is nearly there, The only blocker is thataddStatementandscopeIdentifierare bundled intodist/translator/index.jsbut not exported making them inaccessible to third-party translators.addStatementis used 47 times inside the bundle and never exported. Thedist/translator/util/anddist/translator/visitors/directories only contain.d.tstype stubs & no.jsimplementations exist at those paths.Without exported internals, the only alternative is a runtime workaround using DOM traversal and
queueMicrotaskto deliver context after mount. This works but defers entity registration by one microtask, adds hidden DOM elements, and cannot work during SSR. It appears less performant than the compile-time approach.This gap also affects every other library author who needs implicit context like form libraries passing validation state to nested inputs, design systems passing theme tokens, router libraries passing route context to nested links. Context is a fundamental pattern for component library authorship and Marko v6 is currently without it.
Possible Implementation & Open Questions
Part 1 — Export translator internals (two lines, non-breaking):
In
src/translator/util/signals.ts:In
src/translator/visitors/program/index.ts:These are already part of the compilation contract. Exporting them is additive and non-breaking. They would be documented as lower-level APIs for translator authors, similar to how
@babel/traverseexposes its internals for plugin authors.Part 2 — Runtime context API :
New file
src/runtime/context.ts, exported at@marko/runtime-tags/context:setContextmust be called from a render statement (injected viaaddStatement("render", ...)) so the value is written during the$inputphase before any consumer effects run.getContextis safe to call from effects since the render phase always completes first. This timing is equivalent to Svelte's synchronous context.Part 3 — Built-in tags:
New built-in tags
<context-provider>and<context-consumer>in thetags/directory, with translators that useaddStatementto wire context into the signal graph at compile time. The end-user API would be:This mirrors the v5
@marko-tags/contextpackage and gives Marko v6 a batteries-included context API.Open questions:
@internalwith a deprecation escape hatch? would prefer stable but understand the concern about committing to internals.getContextthrow or returnundefinedwhen no ancestor provides the key? i'd lean towardundefinedwith a separatehasContextguard, matching Vue'sinjectbehavior.@marko-tags/context-v6standalone package as an interim step before the built-in tags land?Is this something you're interested in working on?
Yes. if there are alternaye means to achieving it pls comment. thanks