A pnpm + Turborepo monorepo that detects marimo .py notebooks on code-hosting sites and renders them as live interactive WASM notebooks inline via a browser extension, replacing the raw Python view.
packages/
notebook-core/ Public API: detect, render, playground URL, types
extension-runtime/ Reconcile-on-observe controller + switching UI
host-github/ GitHub.com + gist.github.com implementation
host-gitlab/ GitLab.com implementation
apps/
extension/ WXT browser extension (Chrome + Firefox)
This is the property to protect. The dependency graph must flow inbound:
notebook-core
↑ ↑
| |
runtime github-host gitlab-host
↑ ↑ ↑
+----+----+ |
| |
extension ←--------+
notebook-coreimports nothing internal: zero monorepo deps. Ships:isMarimoNotebook(),isPythonPath(),playgroundUrl(),renderNotebookAsIframe(), plus types (Host,Theme,View,RenderOptions,PlaygroundOptions).- Hosts and runtime import only
notebook-core. No circular deps, no reaching sideways. - The extension wires a host + runtime together.
- Adding a new host requires zero runtime edits. Implement the
Hostinterface and pass it tocreateRuntime().
A host bridges the page's DOM and source. Implement it to add a new platform:
interface Host {
id: string;
matches(url: URL): boolean; // Does this page belong to us?
getSource(url: URL): Promise<string | null>; // Fetch .py source
findAnchor(): HTMLElement | null; // Original code element (insert before, hide)
readTheme?(): Theme; // Optional: detect page theme (light/dark/system)
}The extension passes the host to createRuntime(host), which handles detection, mounting, switching, and resilience, no host-specific logic needed in the runtime.
isMarimoNotebook(source): detect marimo notebook from first 4096 charsisPythonPath(path): check if path ends.pyplaygroundUrl(source, opts?): compress notebook, return marimo.app URL withembed=truerenderNotebookAsIframe(source, opts?): build iframe DOM subtreeclassName(suffix): CSS class builder withmv-prefix- Types:
Host,Theme,View,PlaygroundOptions,RenderOptions
createRuntime(host, options?)→{ start(), stop(), syncNow() }: host-agnostic reconcile-on-observe controllerstart(): begin observing; run initial reconcilestop(): disconnect observer, teardown injection, reject further worksyncNow(): run one reconcile pass (used in tests and manual triggers)
RuntimeOptions:ref(provenance tag),schedule(injectable scheduler),loadTimeoutMs(iframe timeout)
githubHost: Host impl for github.com (blobs) + gist.github.com- Blob: source from
/blob/→/raw/conversion - Gist: source + anchor from DOM (first
.pyfile) readTheme()from<html data-color-mode>
- Blob: source from
- Helpers:
isBlobUrl(),isGistUrl(),blobRawUrl(),findBlobAnchor(),findGistPythonFile(),readGitHubTheme()
All commands run through Turbo at the repo root:
pnpm build: compile all packages; outputs todist/(packages) oroutput/(extension)pnpm dev: watch mode (tsc --watch per package, WXT hot reload for extension)pnpm compile: typecheck only (tsc --noEmit)pnpm lint: oxlint across all sourcepnpm test: run tests (node --test per package)pnpm clean: remove build artifacts
Per-package scripts (e.g., cd packages/notebook-core && pnpm build) work too, but prefer repo-level commands.
- Language: TypeScript 5.9.3, target ES2022
- Linter: oxlint (replaces eslint)
- Tests: native Node test runner (
node --test), jsdom for DOM tests - Extension: WXT 0.20.27 (esbuild bundler, MV3 for Chrome, MV2 for Firefox)
Per-package structure:
src/index.ts: public exportssrc/*.ts: implementationtest/*.test.mjs: tests (ESM, run withnode --test)tsconfig.json: package config (extends../tsconfig.base.json)
Extension build outputs:
apps/extension/output/chrome-mv3/: Chrome production buildapps/extension/output/firefox-mv2/: Firefox production build- WXT generates
apps/extension/.wxt/during build (gitignored)
pnpm must run with the sandbox disabled in this environment. The fnm multishell symlink causes EPERM under sandbox. Workaround:
pnpm buildThis runs fine. Plain tsc and node are safe sandboxed, so you can run per-package commands that way:
cd packages/notebook-core
tsc --noEmit -p tsconfig.json # ✓ sandboxed
node --test test/*.test.mjs # ✓ sandboxed- Edit code in any
packages/*/src/orapps/extension/entrypoints/. - Typecheck:
pnpm compile(or per-packagetsc --noEmit) - Test:
pnpm test(or per-packagenode --test test/*.test.mjs) - Lint:
pnpm lint - Dev mode:
pnpm dev(WXT watches extension, tsc watches packages)
For manual testing:
pnpm buildto produce a production build- Load
apps/extension/output/chrome-mv3/as an unpacked extension in Chrome dev mode (chrome://extensions) - Or
pnpm devto auto-reload during development
- Create
packages/host-<platform>/with the structure:package.json (list @marimo/notebook-core as a dep) tsconfig.json src/index.ts (export your Host instance + any helpers) src/*.ts (implementation) test/*.test.mjs (tests) - Implement the
Hostinterface insrc/host.ts - Add the package to the extension's
package.jsonif you want it wired in (or leave it as a standalone lib) - In the extension's content script, import and pass the new host to
createRuntime()
That's it: no changes to the runtime or core.
- The extension's content script runs on
github.com/*,gist.github.com/*, andgitlab.com/*only (seeapps/extension/wxt.config.ts) - Hosts share the runtime's floating UI switcher (notebook ↔ original code), theme detection, CSP fallback, and reconcile-on-observe logic
- GitLab host is implemented (
gitlab.comblobs), fetching raw source same-origin using the user's session - Playground ref is
marimo-glance:<surface>(e.g.,marimo-glance:github,marimo-glance:gist)