A single HTML page that compiles and runs Marko templates entirely in the browser. No build step, no bundler, no server-side compilation — the page pulls the Marko compiler, translator and DOM runtime from a CDN and does everything client-side.
This is a standalone spike ahead of adding Marko as a language to LiveCodes.
ES modules and import maps need a real origin, so serve the folder statically (any static host works — there is still no backend involved):
npx serve .
# or
python -m http.server 5173Then open http://localhost:5173/. Edit the template on the left; the preview on the right
updates on Run (or ⌘/Ctrl+Enter). Compile errors are shown as Marko code frames.
Everything happens in index.html:
process.env.BUNDLE = '1'—@marko/compiler/modules.jschecks this (or the existence ofdocument) to switch off Node module resolution. Setting it explicitly makes browser mode deterministic.- Import map → CDN —
@marko/compiler@5.42.5(Marko 6's compiler package) andmarko@6.3.52come from esm.sh. translator: marko/translator— Marko 6's core tags (<let>,<if>,<for>, …) are defined programmatically by the translator, so they resolve without touching the file system. Passing the module here is required because the string default"marko/translator"cannot be resolved without Node.fileSystem— a virtual FS that throwsENOENTand returns[]fromreaddirSync, standing in for a project with no files.- Compile with
{ output: 'dom', modules: 'esm' }→ an ES module that imports frommarko/dom. - Load & mount — the compiled code becomes a
Blobmodule. The import map resolves its baremarko/domspecifier, andtemplate.mount({}, host)renders it. Updates are reactive and batched asynchronously.
Yes. The PoC ships a two-file project (template.marko importing button.marko) with a
tab per file. Two things make it work:
- The virtual FS serves every file in the project, so
import Button from "./button.marko"resolves at compile time. The compiled entry keeps that relative specifier (import ... from "./button.marko"). - A blob module cannot resolve a relative
.markospecifier, sobuildModulewalks the import graph depth-first, creates a blob URL per template, and rewrites each relative specifier to the child's blob URL before the parent module is created. Circular imports are detected and reported instead of looping.
Each <Button/> instance keeps its own state, so component encapsulation holds.
- Counter template renders and increments on click.
<style={...}>binding and conditional text update reactively.- A two-file project renders:
template.markoimportsbutton.markoand mounts two independent instances. - Compile errors surface as readable Marko code frames (e.g.
Missing ending "div" tag, or an unresolvable import).
- LiveCodes' existing pattern fits: a
lang-marko-compiler.tsbuilt to an IIFE worker, aLanguageSpecsentry with acompiler.url/factory, and animportsmap that resolves the compiled module's specifiers (marko/dom,marko/debug/dom, …) tovendors.tsURLs. - The compiler package references Node built-ins (
path,assert,fs) but ships a browser babel build (@marko/compiler/internal/babel→dist/babel.web.js), so it is meant to be bundled for the browser — same approach as the Svelte/Vue workers. - Versions pinned here:
@marko/compiler@5.42.5,marko@6.3.52.