Add ESM playground at /playground/esm#325
Conversation
Splits the 565-line playground.astro into compartmentalized pieces:
- styles.css: the CSS block, unchanged
- runner.ts: Runner interface (title, tabs, init, buildSrcdoc)
- shell.ts: runner-agnostic UI (editors, tabs, console, status, URL hash)
- runner-iife.ts: current esbuild.transform + IIFE srcdoc behavior
- entry-iife.ts: page entry; wires CodeMirror/esbuild URL imports + runner
- playground.astro: ~15 lines, just <head> + mount point + script
URL hash format preserved (flat {html, js}) so existing shared URLs keep
working. Dependency injection pattern keeps URL imports (esbuild-wasm,
CodeMirror, esm.sh) at the page entry; shell/runner modules stay pure TS
that Vite bundles cleanly.
No behavior change β prepares the ground for a separate /playground/esm
page that reuses shell.ts with a different runner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Caution Review failedPull request was closed or merged during review π WalkthroughWalkthroughRefactors the inline playground into a modular system: adds ESM and IIFE runners, a shell, entry modules, console-forwarding, and stylesheet; updates the Header link to Changes
Sequence DiagramsequenceDiagram
participant User
participant Editor as CodeMirror Editor
participant Shell as Playground Shell
participant Runner as Runner (ESM/IIFE)
participant Iframe as Preview iframe
participant IframeWindow as iframe Window
User->>Editor: Edit code
Note over Editor: Debounced change event
Editor->>Shell: onChange
Shell->>Shell: persist to URL hash
Shell->>Runner: buildSrcdoc(files)
alt ESM Runner
Runner->>Runner: prepare HTML (no build)
Runner-->>Shell: return srcdoc HTML (with console forward)
else IIFE Runner
Runner->>Runner: compile TSX via esbuild
Runner->>Runner: inject console forward + compiled JS
Runner-->>Shell: return srcdoc HTML
end
Shell->>Iframe: set iframe.srcdoc
Iframe->>IframeWindow: execute HTML/JS
IframeWindow-->>Shell: postMessage(console/errors)
Shell->>Shell: render console & status
Shell->>User: show output/status
Estimated code review effortπ― 4 (Complex) | β±οΈ ~50 minutes Possibly related PRs
Poem
π₯ Pre-merge checks | β 2 | β 1β Failed checks (1 warning)
β Passed checks (2 passed)
βοΈ Tip: You can configure your own custom pre-merge checks in the settings. β¨ Finishing Touchesπ Generate docstrings
π§ͺ Generate unit tests (beta)
Warning Review ran into problemsπ₯ ProblemsTimed out fetching pipeline failures after 30000ms Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codex flagged: wrapping the editor mounts in a data-role="editor-mounts"
div broke .editor-mount { flex: 1 } because the wrapper is a plain block,
not a flex container. CodeMirror collapsed to content height (~27px)
instead of filling the panel.
Fix: drop the wrapper, append mounts directly to .editor-panel (which
is already display: flex, flex-direction: column). Matches the original
DOM layout exactly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single index.html tab, importmap β esm.sh, module script. No esbuild, no JSX, no build step. The iframe srcdoc is the user's HTML verbatim (plus a console-forwarding snippet). Reuses the shell extracted in the previous refactor; only runner-esm.ts and entry-esm.ts are new runtime code. The IIFE and ESM runners share zero mode-specific logic β they meet only at the Runner interface. Why split pages instead of toggling: the two playgrounds tell different stories. IIFE = "one script tag, global tko, JSX via build." ESM = "import maps, zero build, copy-and-deploy." Honest URLs let docs link the right one per context. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
74a4ecb to
a54dd9b
Compare
There was a problem hiding this comment.
Pull request overview
Adds a second docs-site playground at /playground/esm to demonstrate running TKO natively in the browser via import maps + <script type="module"> with no build step, reusing the extracted playground shell from the prior refactor.
Changes:
- Introduces an ESM runner that renders a single
index.htmltab and forwards iframe console output to the shell UI. - Adds a new
/playground/esmAstro page + entry module wiring to mount the shell with the ESM runner. - Extracts shared playground CSS into
src/playground/styles.cssand updates/playgroundto use the modular shell entry.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tko.io/src/playground/styles.css | New shared playground styling (moved from inline <style>). |
| tko.io/src/playground/shell.ts | Runner-agnostic UI shell: editors/tabs/iframe/status/console + URL hash persistence. |
| tko.io/src/playground/runner.ts | Defines the Runner interface + tab/status types. |
| tko.io/src/playground/runner-iife.ts | IIFE/esbuild-backed runner implementing Runner for the existing playground. |
| tko.io/src/playground/runner-esm.ts | New ESM/no-build runner that injects console forwarding into user HTML. |
| tko.io/src/playground/entry-iife.ts | Entry wiring for /playground to mount shell with the IIFE runner. |
| tko.io/src/playground/entry-esm.ts | Entry wiring for /playground/esm to mount shell with the ESM runner. |
| tko.io/src/pages/playground.astro | Simplified page template for /playground using shared CSS + entry module. |
| tko.io/src/pages/playground/esm.astro | New page template for /playground/esm using shared CSS + entry module. |
| const headClose = /<\/head>/i | ||
| if (headClose.test(html)) return html.replace(headClose, `${CONSOLE_FORWARD}</head>`) | ||
| const bodyOpen = /<body[^>]*>/i | ||
| if (bodyOpen.test(html)) return html.replace(bodyOpen, (m) => `${m}${CONSOLE_FORWARD}`) |
There was a problem hiding this comment.
injectConsoleForward() can prepend the console-forwarding <script> before <!doctype html> (when there is no <head>/<body> tag), which makes the doctype no longer the first token and can trigger quirks mode / unexpected parsing. Consider either requiring a full <html><head>β¦</head><body>β¦</body></html> document for the ESM tab, or explicitly inserting after an initial doctype / <html> start tag so the doctype stays first.
| if (bodyOpen.test(html)) return html.replace(bodyOpen, (m) => `${m}${CONSOLE_FORWARD}`) | |
| if (bodyOpen.test(html)) return html.replace(bodyOpen, (m) => `${m}${CONSOLE_FORWARD}`) | |
| const leadingDoctype = /^(\s*<!doctype[^>]*>)/i | |
| if (leadingDoctype.test(html)) return html.replace(leadingDoctype, `$1${CONSOLE_FORWARD}`) | |
| const leadingHtmlOpen = /^(\s*<html\b[^>]*>)/i | |
| if (leadingHtmlOpen.test(html)) return html.replace(leadingHtmlOpen, `$1${CONSOLE_FORWARD}`) |
| const data = e.data | ||
| if (data?.type === 'console') { | ||
| addConsoleMsg(data.args.join(' '), data.method) | ||
| } else if (data?.type === 'error') { | ||
| addConsoleMsg(data.message, 'error') | ||
| } | ||
| }) | ||
|
|
There was a problem hiding this comment.
The message event handler accepts any postMessage from any source. This allows unrelated frames/tabs (or injected scripts) to spoof console/error entries in the playground UI. Filter messages to the preview iframe (e.g., e.source === preview.contentWindow) and, if possible, also validate e.origin (srcdoc sandbox frames typically use "null").
| try { | ||
| const json = decodeURIComponent(atob(location.hash.slice(1))) | ||
| const parsed = JSON.parse(json) | ||
| return typeof parsed === 'object' && parsed !== null ? parsed : null |
There was a problem hiding this comment.
loadFromHash() returns any parsed object without validating the values are strings. If a hash contains non-string values (e.g. { html: 123 }), those get passed to CodeMirror as doc, which can throw and break page load. Consider normalizing/validating per-tab values (only accept strings; otherwise fall back to defaults).
| return typeof parsed === 'object' && parsed !== null ? parsed : null | |
| if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { | |
| return null | |
| } | |
| const files: Record<string, string> = {} | |
| for (const [key, value] of Object.entries(parsed)) { | |
| if (typeof value !== 'string') { | |
| return null | |
| } | |
| files[key] = value | |
| } | |
| return files |
| const dotClass = | ||
| status.state === 'ready' ? 'dot--ready' : status.state === 'error' ? 'dot--error' : 'dot--loading' | ||
| statusEl.innerHTML = `<span class="dot ${dotClass}"></span>${status.label}` | ||
| if (status.state === 'ready') loading.hidden = true | ||
| if (status.state === 'error') loading.textContent = status.label | ||
| } | ||
|
|
There was a problem hiding this comment.
setStatus() uses innerHTML to render status.label. In error paths this label comes from e.message, which can contain </& and accidentally break markup; itβs also an unnecessary XSS footgun. Prefer building the dot element separately and setting the label via textContent.
| const CONSOLE_FORWARD = `<script> | ||
| ;['log','warn','error','info'].forEach(m => { | ||
| const orig = console[m]; | ||
| console[m] = (...args) => { | ||
| window.parent.postMessage({ type: 'console', method: m, args: args.map(a => { | ||
| try { return typeof a === 'object' ? JSON.stringify(a) : String(a) } catch { return String(a) } | ||
| }) }, '*'); | ||
| orig.apply(console, args); | ||
| }; | ||
| }); | ||
| window.onerror = (msg) => { | ||
| window.parent.postMessage({ type: 'error', message: String(msg) }, '*'); | ||
| }; | ||
| window.onunhandledrejection = (e) => { | ||
| window.parent.postMessage({ type: 'error', message: String(e.reason) }, '*'); | ||
| }; | ||
| </script> | ||
| ` | ||
|
|
There was a problem hiding this comment.
The console-forwarding snippet is duplicated (very similar logic in both runners). This increases the chance of the two playgrounds diverging (e.g. one adds a new console method or error handler and the other doesnβt). Consider factoring the forwarder + injection into a small shared helper used by both runners.
One example, one story: importmap + module script, copy-and-deploy. The IIFE script-tag alternative was duplicating the story and pulling attention away from the "no build step" promise. Also links to the new /playground/esm so readers can try the exact code before downloading. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single Hello/TKO example, shared verbatim between the deploy page code block and the ESM playground's default. Full HTML structure (html/head/ body) so a copy-paste into a new file is a complete, teach-friendly document. Also points the top-nav Playground button at /playground/esm so the site-wide 'try it' story matches the Deploy in Seconds promise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 7
π§Ή Nitpick comments (5)
tko.io/src/content/docs/getting-started/deploy.md (1)
38-38: Nit: trailing-slash convention for Astro content pages.Per the retrieved learning about Eleventy trailing slashes, Astro/Starlight pages under
tko.io/src/content/should use trailing-slash URLs. Consider/playground/esm/for consistency with other internal doc links.π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tko.io/src/content/docs/getting-started/deploy.md` at line 38, The link text currently points to "/playground/esm" without a trailing slash; update the internal doc link to use the trailing-slash convention by changing "/playground/esm" to "/playground/esm/" in the deploy.md content so it matches other Astro/Starlight pages and internal docs. Ensure any identical occurrences in the same file (e.g., the "ESM playground" link) are updated consistently.tko.io/src/playground/entry-esm.ts (2)
19-23: Minor:tsx/jslanguage entries are unused for the ESM runner.
createEsmRunner()exposes only thehtmltab (seerunner-esm.tsline 56), sojavascript({ jsx, typescript })andjavascript()are dead code pulled into the bundle. You can drop them to shrink the ESM entry, or keep them if you plan to add a TSX tab shortly.β»οΈ Suggested trim
- languages: { - html: () => htmlLang(), - tsx: () => javascript({ jsx: true, typescript: true }), - js: () => javascript(), - }, + languages: { + html: () => htmlLang(), + },And drop the now-unused
javascriptimport.π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tko.io/src/playground/entry-esm.ts` around lines 19 - 23, The TSX and JS language entries in the languages object (the 'tsx' and 'js' keys) are unused by createEsmRunner() and can be removed to shrink the ESM bundle; delete those entries from the languages object in entry-esm.ts and also remove the now-unused javascript import to avoid dead code while keeping the html entry (htmlLang()) intact for createEsmRunner().
14-14: Non-null assertion ongetElementById('app').
document.getElementById('app')!will throw a crypticCannot read properties of nullif the script ever runs before#appexists (e.g. someone moves the<script>out of<body>or the element id changes). A small guard with a clear error improves debuggability for playground users forking this file.π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tko.io/src/playground/entry-esm.ts` at line 14, Replace the non-null assertion on document.getElementById('app')! with an explicit null check: retrieve the element into a local variable (e.g., const appEl = document.getElementById('app')), verify appEl is not null, and if it is null throw a clear, actionable Error (or wait for DOMContentLoaded) before passing the element into the root/mount call that uses it; this removes the cryptic `Cannot read properties of null` and makes failures clear when getElementById('app') can't find the element.tko.io/src/playground/runner-iife.ts (1)
35-54: Duplicated console-forward snippet withrunner-esm.ts.This block is a near-verbatim copy of
CONSOLE_FORWARDintko.io/src/playground/runner-esm.ts(lines 26β43). Extracting it into a shared helper (e.g../console-forward.ts) avoids drift if one side gains a fix (origin filtering, structured cloning of args, etc.) and the other doesn't.π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tko.io/src/playground/runner-iife.ts` around lines 35 - 54, Duplicate console-forward logic: extract the shared snippet (the console override, window.onerror and window.onunhandledrejection) into a reusable helper (e.g., export function installConsoleForward(...) from a new console-forward module) and replace the inline copy in both runner-iife and runner-esm with an import and call to that function; specifically move the logic that overrides console methods, and sets window.onerror and window.onunhandledrejection into the new installConsoleForward helper (preserving behavior of the current console[m] wrapper and postMessage payload), export it (name it installConsoleForward or CONSOLE_FORWARD installer) and import/use it from the places that currently contain the duplicated code so future fixes (origin filtering, structured cloning of args, etc.) are applied in one place.tko.io/src/playground/shell.ts (1)
198-203: Ctrl/β+Enter listener is attached todocumentrather than the container.Scoping the shortcut to
documentis fine for a full-page playground, but it means ifmountis ever called twice (HMR, SPA navigation) the listeners stack andrun()fires multiple times. Consider attaching tocontainerand/or returning a disposer for future-proofing.π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tko.io/src/playground/shell.ts` around lines 198 - 203, The keydown handler is being attached to document which will stack if mount is called multiple times; change it to attach to the playground container element (use container.addEventListener) and capture the handler in a named function/const so it can be removed later, then return (or expose) a disposer/unmount function that calls container.removeEventListener with that handler; keep the same logic calling run() when (e.ctrlKey || e.metaKey) && e.key === 'Enter' and ensure you still call e.preventDefault() in the handler.
π€ Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tko.io/src/content/docs/getting-started/deploy.md`:
- Around line 18-20: The importmap in getting-started/deploy.md currently points
to the floating URL "https://esm.sh/@tko/build.reference"; change it to a pinned
versioned URL (e.g., https://esm.sh/@tko/build.reference@vX.Y.Z) matching the
current site header version, and make the same change to the DEFAULT_HTML
constant in playground/runner-esm.ts so both examples use a concrete version;
after making these edits, add or update a short deployment guide under
tko.io/public/agents/ describing the need to pin esm.sh imports for reproducible
static deploys.
In `@tko.io/src/playground/runner-esm.ts`:
- Around line 45-51: The fallback in injectConsoleForward currently prepends
CONSOLE_FORWARD before the document which can break <!doctype html>; update
injectConsoleForward to avoid injecting before the doctype or <html> tag: when
neither </head> nor <body> matches, search for a leading <!doctype ...> or an
opening <html[^>]*> and insert CONSOLE_FORWARD immediately after that match (or
after the matched tag's end); if neither doctype nor <html> exists, instead fail
fast by returning the original html unchanged or throwing/returning a clear
status error so we do not force the script into quirks mode. Ensure changes are
made in the injectConsoleForward function and reference CONSOLE_FORWARD for
insertion.
In `@tko.io/src/playground/runner-iife.ts`:
- Around line 56-63: The inline script injection that interpolates ${compiledJs}
inside the IIFE/setTimeout block can be broken by a literal "</script>" coming
from compiledJs; update the code that injects compiledJs (the interpolation used
within the setTimeout/inline <script> in runner-iife.ts) to sanitize/escape any
closing script tags before interpolation (e.g. replace occurrences of
"</script>" with "<\/script>" using a safe replaceAll or regex) or alternatively
emit the compiled JS as a Blob or data: URL and load it via a <script src=...>
to avoid inline parsing issues; change the logic where compiledJs is spliced in
so it either performs the escape or switches to Blob/data-URL loading.
In `@tko.io/src/playground/shell.ts`:
- Around line 21-24: The saveToHash function currently assigns location.hash
which creates a new history entry on every call; change saveToHash to compute
the encoded hash (the result of btoa(encodeURIComponent(json))) and then call
history.replaceState(null, '', window.location.pathname + window.location.search
+ '#' + encoded) so the URL updates without pushing to browser history; update
references inside saveToHash (json, encoded) and remove the direct location.hash
assignment.
- Around line 162-168: The setStatus function currently injects status.label
into statusEl.innerHTML which risks XSS because status.label can contain
untrusted runner/error text; instead, build the DOM nodes and set only
textContent for the label: create (or reuse) the dot span (set its class via
classList using dotClass) and a separate text node or span for the label, set
labelEl.textContent = status.label, clear or replace statusEl's children with
the dot and label nodes, and keep using loading.hidden/loading.textContent for
the loading/error UI; update references in setStatus, statusEl, and loading
accordingly.
- Around line 152-159: The listener for window 'message' assumes data.args is an
array and calls data.args.join(' '), which can throw and break the handler; in
the message handler (the arrow function passed to window.addEventListener in
playground/shell.ts) replace the direct join call with a safe guard: compute a
safe string for console messages by checking Array.isArray(data.args) and
joining when true, otherwise fallback to String(data.args ?? '') (or an empty
string) before calling addConsoleMsg, so malformed/non-array args no longer
cause exceptions.
In `@tko.io/src/playground/styles.css`:
- Around line 208-213: The .console-msg CSS rule uses the deprecated property
word-break: break-word; replace that declaration with the modern equivalent
overflow-wrap: anywhere to satisfy stylelint and maintain the same behavior;
update the .console-msg block (the selector named ".console-msg") to remove
word-break: break-word and add overflow-wrap: anywhere.
---
Nitpick comments:
In `@tko.io/src/content/docs/getting-started/deploy.md`:
- Line 38: The link text currently points to "/playground/esm" without a
trailing slash; update the internal doc link to use the trailing-slash
convention by changing "/playground/esm" to "/playground/esm/" in the deploy.md
content so it matches other Astro/Starlight pages and internal docs. Ensure any
identical occurrences in the same file (e.g., the "ESM playground" link) are
updated consistently.
In `@tko.io/src/playground/entry-esm.ts`:
- Around line 19-23: The TSX and JS language entries in the languages object
(the 'tsx' and 'js' keys) are unused by createEsmRunner() and can be removed to
shrink the ESM bundle; delete those entries from the languages object in
entry-esm.ts and also remove the now-unused javascript import to avoid dead code
while keeping the html entry (htmlLang()) intact for createEsmRunner().
- Line 14: Replace the non-null assertion on document.getElementById('app')!
with an explicit null check: retrieve the element into a local variable (e.g.,
const appEl = document.getElementById('app')), verify appEl is not null, and if
it is null throw a clear, actionable Error (or wait for DOMContentLoaded) before
passing the element into the root/mount call that uses it; this removes the
cryptic `Cannot read properties of null` and makes failures clear when
getElementById('app') can't find the element.
In `@tko.io/src/playground/runner-iife.ts`:
- Around line 35-54: Duplicate console-forward logic: extract the shared snippet
(the console override, window.onerror and window.onunhandledrejection) into a
reusable helper (e.g., export function installConsoleForward(...) from a new
console-forward module) and replace the inline copy in both runner-iife and
runner-esm with an import and call to that function; specifically move the logic
that overrides console methods, and sets window.onerror and
window.onunhandledrejection into the new installConsoleForward helper
(preserving behavior of the current console[m] wrapper and postMessage payload),
export it (name it installConsoleForward or CONSOLE_FORWARD installer) and
import/use it from the places that currently contain the duplicated code so
future fixes (origin filtering, structured cloning of args, etc.) are applied in
one place.
In `@tko.io/src/playground/shell.ts`:
- Around line 198-203: The keydown handler is being attached to document which
will stack if mount is called multiple times; change it to attach to the
playground container element (use container.addEventListener) and capture the
handler in a named function/const so it can be removed later, then return (or
expose) a disposer/unmount function that calls container.removeEventListener
with that handler; keep the same logic calling run() when (e.ctrlKey ||
e.metaKey) && e.key === 'Enter' and ensure you still call e.preventDefault() in
the handler.
πͺ Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
βΉοΈ Review info
βοΈ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 420c05b3-51bf-4091-ae7f-01638bb76161
π Files selected for processing (11)
tko.io/src/components/Header.astrotko.io/src/content/docs/getting-started/deploy.mdtko.io/src/pages/playground.astrotko.io/src/pages/playground/esm.astrotko.io/src/playground/entry-esm.tstko.io/src/playground/entry-iife.tstko.io/src/playground/runner-esm.tstko.io/src/playground/runner-iife.tstko.io/src/playground/runner.tstko.io/src/playground/shell.tstko.io/src/playground/styles.css
| <script type="importmap"> | ||
| { "imports": { "@tko/build.reference": "https://esm.sh/@tko/build.reference" } } | ||
| </script> |
There was a problem hiding this comment.
π§© Analysis chain
π Web query:
Does esm.sh publish @tko/build.referenceand expose a default export compatible withimport ko from '@tko/build.reference'?
π‘ Result:
Yes, esm.sh publishes @tko/build.reference and exposes a default export compatible with import ko from '@tko/build.reference'. esm.sh is a universal CDN that serves any npm package, including @tko/build.reference (latest 4.0.0-beta1.7, published recently), by transforming it for ESM usage in browsers. The package has dist/index.js (1.8KB) and dist/index.mjs (1.8KB) files, which esm.sh uses as entry points. These small index files (vs. large bundled browser.js at 297KB) are designed as unbundled ESM exports that re-export the main TKO library (Knockout successor), providing a default export named 'ko' as per TKO/Knockout convention. esm.sh handles CJS/ESM conversion and tree-shaking, ensuring default import compatibility. Available at https://esm.sh/@tko/build.reference, similar to other packages like React (https://esm.sh/react).
Citations:
- 1: https://jsdelivr.com/package/npm/%40tko/build.reference
- 2: https://registry.npmjs.org/%40tko%2Fbuild.reference
- 3: https://github.com/esm-dev/esm.sh
- 4: https://www.npmjs.com/package/@tko/build.reference
- 5: https://github.com/knockout/tko
π Script executed:
# Find and check the deploy.md file
find . -name "deploy.md" -type f | head -20Repository: knockout/tko
Length of output: 107
π Script executed:
# Find runner-esm.ts
find . -name "runner-esm.ts" -type fRepository: knockout/tko
Length of output: 93
π Script executed:
# Check for agent guides directory
find . -path "*/tko.io/public/agents*" -type dRepository: knockout/tko
Length of output: 120
π Script executed:
# Read deploy.md to verify content at lines 18-20
cat -n ./tko.io/src/content/docs/getting-started/deploy.md | head -30Repository: knockout/tko
Length of output: 1139
π Script executed:
# Read runner-esm.ts to verify lines 8-10
cat -n ./tko.io/src/playground/runner-esm.ts | head -20Repository: knockout/tko
Length of output: 817
π Script executed:
# Check for corresponding agent guide files
ls -la ./tko.io/public/agents/
ls -la ./tko.io/public/agents/verified-behaviors/ 2>/dev/null | head -20Repository: knockout/tko
Length of output: 1831
π Script executed:
# Check if there's a deploy-related guide in the agent directory
find ./tko.io/public/agents -name "*deploy*" -o -name "*getting*"Repository: knockout/tko
Length of output: 38
π Script executed:
# Read the guide.md to see if it covers deployment
head -50 ./tko.io/public/agents/guide.mdRepository: knockout/tko
Length of output: 2138
Pin the @tko/build.reference version in the importmap.
https://esm.sh/@tko/build.reference resolves to the latest published version at request time. A future breaking release of @tko/build.reference (or of its transitive dependencies re-resolved by esm.sh) will silently break every copy of this index.html that users have deployed to static hosts. Since this snippet is explicitly advertised as copy-paste deployable, pinning to a concrete version (and ideally the current v{version} shown in the header) ensures reproducible deploys.
Same concern applies to DEFAULT_HTML in tko.io/src/playground/runner-esm.ts (lines 8β10), which mirrors this example.
Per the coding guidelines, update or create a corresponding deployment guide in tko.io/public/agents/ for AI agents when finalizing the documentation changes.
π‘οΈ Suggested pin
- { "imports": { "@tko/build.reference": "https://esm.sh/@tko/build.reference" } }
+ { "imports": { "@tko/build.reference": "https://esm.sh/@tko/build.reference@<version>" } }π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tko.io/src/content/docs/getting-started/deploy.md` around lines 18 - 20, The
importmap in getting-started/deploy.md currently points to the floating URL
"https://esm.sh/@tko/build.reference"; change it to a pinned versioned URL
(e.g., https://esm.sh/@tko/build.reference@vX.Y.Z) matching the current site
header version, and make the same change to the DEFAULT_HTML constant in
playground/runner-esm.ts so both examples use a concrete version; after making
these edits, add or update a short deployment guide under tko.io/public/agents/
describing the need to pin esm.sh imports for reproducible static deploys.
| function injectConsoleForward(html: string): string { | ||
| const headClose = /<\/head>/i | ||
| if (headClose.test(html)) return html.replace(headClose, `${CONSOLE_FORWARD}</head>`) | ||
| const bodyOpen = /<body[^>]*>/i | ||
| if (bodyOpen.test(html)) return html.replace(bodyOpen, (m) => `${m}${CONSOLE_FORWARD}`) | ||
| return CONSOLE_FORWARD + html | ||
| } |
There was a problem hiding this comment.
injectConsoleForward fallback injects before the document.
If the user removes both </head> and <body> (or starts with <!doctype html> only), the forward script is prepended before <!doctype html>, which invalidates the doctype and puts the browser into quirks mode inside the iframe β breaking CSS the user will then debug in isolation. Prefer injecting after the doctype/<html> tag, or refuse to run with a clear status error.
π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tko.io/src/playground/runner-esm.ts` around lines 45 - 51, The fallback in
injectConsoleForward currently prepends CONSOLE_FORWARD before the document
which can break <!doctype html>; update injectConsoleForward to avoid injecting
before the doctype or <html> tag: when neither </head> nor <body> matches,
search for a leading <!doctype ...> or an opening <html[^>]*> and insert
CONSOLE_FORWARD immediately after that match (or after the matched tag's end);
if neither doctype nor <html> exists, instead fail fast by returning the
original html unchanged or throwing/returning a clear status error so we do not
force the script into quirks mode. Ensure changes are made in the
injectConsoleForward function and reference CONSOLE_FORWARD for insertion.
| .console-msg { | ||
| padding: 0.2rem 0.75rem; | ||
| color: var(--pg-text-muted); | ||
| white-space: pre-wrap; | ||
| word-break: break-word; | ||
| } |
There was a problem hiding this comment.
Replace deprecated word-break: break-word with overflow-wrap: anywhere.
Stylelint flags word-break: break-word as deprecated; browsers historically map it to overflow-wrap: anywhere. Using the modern property avoids the lint error and is the documented replacement.
π οΈ Proposed fix
.console-msg {
padding: 0.2rem 0.75rem;
color: var(--pg-text-muted);
white-space: pre-wrap;
- word-break: break-word;
+ overflow-wrap: anywhere;
}π Committable suggestion
βΌοΈ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .console-msg { | |
| padding: 0.2rem 0.75rem; | |
| color: var(--pg-text-muted); | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| } | |
| .console-msg { | |
| padding: 0.2rem 0.75rem; | |
| color: var(--pg-text-muted); | |
| white-space: pre-wrap; | |
| overflow-wrap: anywhere; | |
| } |
π§° Tools
πͺ Stylelint (17.7.0)
[error] 212-212: Deprecated keyword "break-word" for property "word-break" (declaration-property-value-keyword-no-deprecated)
(declaration-property-value-keyword-no-deprecated)
π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tko.io/src/playground/styles.css` around lines 208 - 213, The .console-msg
CSS rule uses the deprecated property word-break: break-word; replace that
declaration with the modern equivalent overflow-wrap: anywhere to satisfy
stylelint and maintain the same behavior; update the .console-msg block (the
selector named ".console-msg") to remove word-break: break-word and add
overflow-wrap: anywhere.
The Expressive Code playground-button plugin was stripping all <script>
tags from HTML blocks and splitting them into the IIFE playground's
{html, js} hash. That mangles ESM examples β the whole point of an
<script type="importmap"> + <script type="module"> example is that
it's a single self-contained file.
Detect those two script types and route the button to /playground/esm
with the HTML verbatim. Classic <script> blocks (and <script src=...>)
still go to /playground and get the old split-and-auto-applyBindings
treatment.
Fixes the "Deploy in Seconds" code block opening into the wrong
playground with the scripts stripped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two tabs now, both ESM: - Plain ESM β data-bind + <script type="module">, zero tooling - ESM + TSX β same single-file story, but JSX/TypeScript via in-browser esbuild-wasm compile The TSX variant uses <script type="text/tsx"> as an inert source holder (browsers ignore unknown script types, so source is preserved verbatim), then a small module bootstrap: 1. Loads esbuild-wasm via the import map 2. Transforms the TSX source 3. Wraps the output in a blob URL and dynamic-import()s it That keeps everything ESM end-to-end β import statements in the TSX (e.g. `import ko from '@tko/build.reference'`) resolve through the import map like any module. No new Function, no globals, no eval. The playground-button plugin already detects <script type="module"> so both tabs open in /playground/esm automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
data-bind: <input data-bind="textInput: name" /> native: <input ko-textInput="name" /> Shorter, less noise, and better editor tooling since ko-<binding> is a real attribute the HTML language server can see, instead of a string microsyntax inside data-bind. The TSX tab already used native providers (ko-click) β this brings the Plain ESM tab and the playground default into line. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both tabs now demonstrate the TKO MVVM split: - <template id="..."> holds reusable markup with native ko- bindings - viewModel owns state; component.register wires them - drop <ko-greeting> / <ko-counter> anywhere, any number of times The TSX tab's iframe renders two independent counters to make the reuse story visible. Each instance gets its own observable; the template is shared. Custom element names hyphenated per HTML spec (ko-greeting, ko-counter) to silence the "custom elements must contain a hyphen" warning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both variants now extend ko.Component:
class KoGreeting extends ko.Component {
static get template() { return { element: 'ko-greeting-template' } }
name = ko.observable('TKO')
}
KoGreeting.register()
ComponentABC handles the plumbing: element name is auto-derived from the
class name (kebab-case), `register()` wires up template + viewModel in
one call, and the class gets LifeCycle for free.
Also added short inline comments explaining the template/component
split and the class contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot + CodeRabbit flagged a mix of real bugs and defensive improvements. Fixed all of the substantive ones: Real bugs: - saveToHash now uses history.replaceState instead of assigning location.hash. Every debounced edit was pushing a new history entry, so a few seconds of typing filled the Back button. - injectConsoleForward preserves a leading <!doctype html>. If a user omits both <head> and <body>, the forwarder was prepending before the doctype and knocking the iframe into quirks mode. Now we inject after the doctype when there's no head/body, and only fall back to raw prepend when there's no doctype either. - Compiled TSX is escaped with escapeScriptBody before being spliced into an inline <script> block. A stray </script> string literal in user code (or an esbuild emit) would have terminated the script tag early β now <\/script is written so the parser doesn't. Defensive: - Filter postMessage listener to e.source === preview.contentWindow so unrelated frames can't spoof console entries. - loadFromHash drops non-string values (guards CodeMirror against being handed a number). - postMessage handler checks Array.isArray(data.args) and validates data.method before rendering. - setStatus builds the dot span with createElement + appendChild instead of innerHTML β status.label can contain error messages with <, &, etc. and was being parsed as HTML. Code quality: - Console-forward snippet extracted into console-forward.ts, shared by both runners (IIFE and ESM). The two were drifting. - word-break: break-word (deprecated) β overflow-wrap: anywhere. Verified in preview: ESM playground still renders "TKO"/"Hello, TKO"; IIFE playground still compiles TSX and renders "Count: 0"; spoofed postMessage from window is now filtered; replaceState alone grows history by 0 (remaining growth comes from iframe srcdoc navigations, a pre-existing browser quirk orthogonal to this PR). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Adds a second playground at
/playground/esmthat demonstrates TKO running natively in the browser β import maps, a<script type="module">, and zero build step.Depends on #324 (the shell extraction refactor).
What's in the box
src/playground/runner-esm.tsβ ~60 lines. Single tab (index.html), no esbuild, passes user HTML through to the iframe with a small console-forwarding snippet injected.src/playground/entry-esm.tsβ page entry. Same CodeMirror URL imports as IIFE; just uses a different runner.src/pages/playground/esm.astroβ mirrorsplayground.astro, ~15 lines.The shell (
shell.ts) and shared styles didn't need a single change to support this second runner. That's the whole point of the refactor in #324.Default example
Copy that HTML to any static host β it works. That's the story the ESM playground is here to tell.
Why two pages, not a toggle
The playgrounds demonstrate genuinely different things:
/playgroundβ globaltko, JSX, esbuild compiles TSX in-browser/playground/esmβ import maps, data-bind HTML, no build stepBranching on mode inside one file would intermingle two disparate concepts and turn into spaghetti. Two pages + a shared shell keep the concepts isolated.
Test plan
/playground/esmloadsindex.htmltab with importmap + module script@tko/build.referenceimported from esm.sh and applyBindings ran)π€ Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Refactor