A powerful, flexible runtime environment for executing code sandboxes in the browser. @vizhub/runtime powers VizHub and can be used to build similar interactive coding platforms.
Latest (v4.6.0): Upgraded all dependencies (Vite 8, Vitest 4, TS 6, Puppeteer 25, Svelte 5.56+), hardened test infrastructure, improved TypeScript config, and added a React+D3 combo demo fixture — see Changelog.
@vizhub/runtime intelligently detects the appropriate runtime version based on the provided files and generates executable HTML that can be used within an iframe's srcdoc attribute. It handles everything from simple HTML/JS/CSS combinations to complex module bundling, dependency resolution, and cross-viz imports.
The library automatically detects which runtime version to use based on the files provided:
- v1: When only
index.htmlis present - v2: When both
index.htmlandindex.js(orindex.jsx) are present - v3: When only
index.jsis present (noindex.html) - v4: When
index.htmlcontains ES module scripts with import maps
| Feature | V1 | V2 | V3 | V4 |
|---|---|---|---|---|
Custom index.html |
✅ | ✅ | ⬜️ | ✅ |
| Local ES Modules | ⬜️ | ✅ | ✅ | ✅ |
| UMD Libraries | ✅ | ✅ | ✅ | ⬜️ |
package.json |
⬜️ | ✅ | ✅ | ⬜️ |
| ESM Libraries | ⬜️ | ⬜️ | ⬜️ | ✅ |
| React JSX | ⬜️ | ✅ | ⬜️ | ✅ |
| Svelte | ⬜️ | ⬜️ | ✅ | ⬜️ |
| Cross-Viz Imports | ⬜️ | ⬜️ | ✅ | ⬜️ |
| Hot Reloading | ⬜️ | ⬜️ | ✅ | ✅ |
| State Management | ⬜️ | ⬜️ | ✅ | ⬜️ |
| Import from CSV | ⬜️ | ⬜️ | ✅ | ⬜️ |
| TypeScript | ⬜️ | ⬜️ | ⬜️ | ✅ |
fetch proxy |
✅ | ✅ | ⬜️ | ✅ |
The V1 runtime is the simplest version, designed for basic HTML, CSS, and JavaScript projects. This runtime is automatically selected when your project contains only an index.html file.
In V1 runtime:
- Your
index.htmlfile is executed directly in the browser - You can include inline JavaScript and CSS within your HTML file
- The runtime provides fetch request proxying to handle cross-origin requests
As a VizHub user, you simply need to create an index.html file containing your entire project:
index.html
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: sans-serif;
}
</style>
</head>
<body>
<h1>Hello World</h1>
<script>
console.log("Hello from V1 runtime!");
</script>
</body>
</html>V1 is ideal for simple demonstrations or when you want complete control over your HTML structure.
The V2 runtime introduces JavaScript bundling with Rollup, JSX support, and CDN-based dependency resolution. This runtime is automatically selected when your project contains both an index.html and an index.js (or index.jsx) file.
In V2 runtime:
- Your JavaScript files are bundled together using Rollup
- Internally, a file named
bundle.jsis created - The
index.htmlfile references thisbundle.jsfile - You can use ES6 modules to import/export code
- JSX syntax is supported for React development
- Dependencies listed in
package.jsonare automatically resolved via CDNs (jsDelivr/unpkg) - The bundled JavaScript is referenced in your HTML file
As a VizHub user, you'll typically create:
- An
index.htmlfile that references abundle.jsfile - An
index.js(orindex.jsx) file as your entry point - Additional JavaScript modules as needed
- A
package.jsonfile to list dependencies
index.html
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>index.js
import { render } from "./render";
render(document.getElementById("root"));render.js
export function render(element) {
element.innerHTML = "<h1>Hello from V2 runtime!</h1>";
}package.json
{
"dependencies": {
"d3": "7.8.5",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"vizhub": {
"libraries": {
"d3": {
"global": "d3",
"path": "/dist/d3.min.js"
},
"react": {
"global": "React",
"path": "/umd/react.production.min.js"
},
"react-dom": {
"global": "ReactDOM",
"path": "/umd/react-dom.production.min.js"
}
}
}
}The vizhub configuration in package.json specifies how external dependencies should be loaded:
libraries: Maps package names to their UMD configurationglobal: The global variable name that the UMD bundle exposespath: The path to the UMD bundle within the package's CDN distribution
This configuration allows the V2 runtime to properly load and expose UMD bundles from CDNs like jsDelivr or unpkg. The runtime will automatically prefix the path with the appropriate CDN URL.
V2 is ideal for more complex projects that require modular JavaScript and external dependencies that provide UMD builds. Note that the V2 runtime does not support ESM builds for external dependencies (see V4 if you need this).
If you want to replicate the V2 runtime's bundling behavior locally for development or testing, you can set up your own Rollup configuration that mimics the internal build process.
- package.json with a
buildscript and Rollup as a dependency - Rollup configuration that sets up d3 and other packages as importable via browser globals
Add the following to your package.json:
{
"type": "module",
"scripts": {
"build": "rollup -c"
},
"devDependencies": {
"rollup": "^4.50.1",
"sucrase": "^3.35.0"
}
}Create a rollup.config.js file that mimics the V2 runtime's configuration:
import { readFileSync } from 'fs';
// Helper function for JSX/TypeScript transpilation (mimics sucrasePlugin)
async function createSucrasePlugin() {
const { transform } = await import('sucrase');
return {
name: 'sucrase',
transform(code, id) {
if (!id.match(/\.(js|jsx|ts|tsx)$/)) return null;
const result = transform(code, {
transforms: ['jsx', 'typescript'],
filePath: id,
sourceMapOptions: {
compiledFilename: 'bundle.js',
},
production: true,
});
return {
code: result.code,
map: result.sourceMap,
};
},
};
}
// Parse package.json for dependencies and vizhub config
function getGlobalsFromPackageJson() {
let pkg = {};
try {
pkg = JSON.parse(readFileSync('package.json', 'utf-8'));
} catch (e) {
// package.json not found or invalid
}
// Default globals (from vizhub-libraries and defaultGlobals)
const defaultGlobals = {
'd3': 'd3',
'd3-array': 'd3',
'd3-axis': 'd3',
'd3-brush': 'd3',
'd3-chord': 'd3',
'd3-collection': 'd3',
'd3-color': 'd3',
'd3-contour': 'd3',
'd3-dispatch': 'd3',
'd3-drag': 'd3',
'd3-dsv': 'd3',
'd3-ease': 'd3',
'd3-fetch': 'd3',
'd3-force': 'd3',
'd3-format': 'd3',
'd3-geo': 'd3',
'd3-hierarchy': 'd3',
'd3-interpolate': 'd3',
'd3-path': 'd3',
'd3-polygon': 'd3',
'd3-quadtree': 'd3',
'd3-random': 'd3',
'd3-scale': 'd3',
'd3-scale-chromatic': 'd3',
'd3-selection': 'd3',
'd3-shape': 'd3',
'd3-tile': 'd3',
'd3-time': 'd3',
'd3-time-format': 'd3',
'd3-timer': 'd3',
'd3-transition': 'd3',
'd3-voronoi': 'd3',
'd3-zoom': 'd3',
'react': 'React',
'react-dom': 'ReactDOM',
'react-dropdown-browser': 'ReactDropdown',
'react-dropdown': 'ReactDropdown',
'three': 'THREE',
'vega': 'vega',
'vega-embed': 'vegaEmbed',
'vega-lite': 'vegaLite',
'vega-lite-api': 'vl',
'vega-tooltip': 'vegaTooltip',
'vega-themes': 'vegaThemes',
'vizhub-vega-lite-config': 'vizhubVegaLiteConfig',
'semiotic': 'Semiotic',
'viz.js': 'Viz'
};
// Custom globals from package.json vizhub config
const customGlobals = {};
if (pkg.vizhub?.libraries) {
Object.entries(pkg.vizhub.libraries).forEach(([packageName, config]) => {
if (config.global) {
customGlobals[packageName] = config.global;
}
});
}
return { ...defaultGlobals, ...customGlobals };
}
export default async function() {
return {
input: 'index.js',
plugins: [
await createSucrasePlugin(),
],
output: {
file: 'bundle.js',
format: 'iife',
sourcemap: true,
globals: getGlobalsFromPackageJson(),
},
external: Object.keys(getGlobalsFromPackageJson()),
onwarn(warning, warn) {
// Suppress "treating module as external dependency" warnings
if (warning.code === 'UNRESOLVED_IMPORT') return;
warn(warning);
},
};
}- Set up your project files (
index.html,index.js,package.json, etc.) as shown in the V2 Runtime example above - Install dependencies:
npm install - Run the build:
npm run build - This will generate a
bundle.jsfile that yourindex.htmlcan reference
The generated bundle.js will have the same behavior as the bundle created internally by the V2 runtime, with all your modules bundled together and external dependencies (like d3, React) available as browser globals.
The V3 runtime provides advanced module bundling with Svelte support and cross-viz imports. This runtime is automatically selected when your project contains an index.js file but no index.html file.
In V3 runtime:
- Your JavaScript modules are bundled together using Rollup
- A default HTML structure is automatically generated
- Svelte components are supported
- Cross-viz imports allow you to import code from other viz instances
- The runtime provides a built-in state management system
V3 runtime includes a built-in state management system based on the unidirectional-data-flow package (GitHub). This provides React-like state management capabilities with:
- A
mainentry point that receives container and state management options - A minimal state management system based on
stateandsetState - Similar semantics to React's
useStatehook:const [state, setState] = useState({}) - Automatic re-rendering when state changes
- Hot module reloading that preserves state between updates
The hot reloading system will:
- Preserve state between code updates
- Re-execute the main function with the current state
- Only reload changed modules
- Maintain the visualization's current state (e.g. D3 selections, transitions)
While frameworks like React, Svelte, Vue, and Angular offer state management and DOM manipulation solutions, D3 excels in data transformation and visualization, particularly with axes, transitions, and behaviors (e.g. zoom, drag, and brush). These D3 features require direct access to the DOM, making it challenging to replicate them effectively within frameworks.
Unidirectional data flow is a pattern that can be cleanly invoked from multiple frameworks. In this paradigm, a single function is responsible for updating the DOM or rendering visuals based on a single, central state. As the state updates, the function re-renders the visualization in an idempotent manner, meaning it can run multiple times without causing side effects. Here's what the entry point function looks like for a D3-based visualization that uses unidirectional data flow:
index.js
export const main = (container, { state, setState }) => {
// Your reusable D3-based rendering logic goes here
};container: A DOM element where the visualization will be renderedstate: An object representing the current state of the application, initially emptysetState: A function that updates the state using immutable update patterns
Whenever setState is invoked, main re-executes with the new state, ensuring that the rendering logic is both dynamic and responsive.
For cross-viz imports, you can reference other vizzes directly:
example-with-import.js
// Import from another viz using @username/vizIdOrSlug syntax
import { someFunction } from "@username/my-other-viz";V3 is ideal for modern JavaScript applications that benefit from automatic HTML structure generation and built-in state management. Additional features of V3 include:
- Cross-Viz Imports: Import code from other viz instances using
@username/vizIdOrSlugsyntax - Import from CSV: Import CSV files directly into your viz
The V4 runtime leverages modern ES Modules with import maps for direct browser execution. This runtime is automatically selected when your project's index.html contains ES module scripts with import maps.
In V4 runtime:
- Native browser ES modules are used without bundling
- Import maps allow you to specify module resolution directly in the browser
- Module paths can be aliased for cleaner imports
- External dependencies can be loaded directly from CDNs
As a VizHub user, you'll create an index.html file with import maps and ES module scripts:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>React App</title>
<script type="importmap">
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react@19.1.0/+esm",
"react/jsx-runtime": "https://cdn.jsdelivr.net/npm/react@19.1.0/jsx-runtime/+esm",
"react-dom/client": "https://cdn.jsdelivr.net/npm/react-dom@19.1.0/client/+esm"
}
}
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="./index.tsx"></script>
</body>
</html>index.tsx
import React, { useState, FC } from "react";
import { createRoot } from "react-dom/client";
interface CounterProps {}
const Counter: FC<CounterProps> = () => {
const [count, setCount] = useState<number>(0);
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(<Counter />);V4 is ideal for modern browsers with native ES module support, TypeScript development, and when you want direct control over module resolution. It supports:
- TypeScript with full type checking
- React with modern ESM builds
- Import maps for direct CDN dependencies
- Native ES modules without bundling
- Local module imports with relative paths
- Hot reloading for fast development iteration
V4 runtime supports hot reloading similar to V3, allowing for rapid development cycles without full page refreshes. When enableHotReloading is set to true, the V4 runtime:
- Monitors code changes and re-bundles ES modules
- Sends updated JavaScript to the iframe via
runJSmessages - Replaces existing module script tags with updated bundled code
- Preserves the current browser state where possible
The hot reloading system works by:
- Bundling ES modules using Rollup (similar to V3)
- Injecting a hot reload script that listens for update messages
- Replacing module scripts dynamically when code changes
- Re-executing the updated modules in the current context
Unlike V3's built-in state management, V4 hot reloading relies on the application's own state management (e.g., React state, DOM state) to preserve state between updates.
-
Multi-Version Runtime Support
- v1: Simple HTML execution with fetch proxying
- v2: JavaScript bundling with Rollup, JSX support, and CDN-based dependency resolution
- v3: Advanced module bundling with Svelte support and cross-viz imports
- v4: Modern ES Modules with import maps for direct browser execution
-
Comprehensive Tooling
- Bundling: Seamless integration with Rollup for module bundling
- Transpilation: Support for JSX (v2) and Svelte components (v3)
- Dependency Management: Automatic resolution via CDNs (jsDelivr/unpkg)
- Caching: Efficient viz content and slug resolution caching
- Debugging: Sourcemap generation for improved debugging experience
-
Advanced Capabilities
- Cross-Viz Imports: Import code from other viz instances using
@username/vizIdOrSlugsyntax - Fetch Interception: Handle cross-origin requests and authentication
- File Type Support: Process JS, JSX, CSS, CSV, JSON, and more
- Cross-Viz Imports: Import code from other viz instances using
npm install @vizhub/runtimeCreates a runtime environment that manages code execution in an iframe with worker-based build support.
const runtime = createRuntime({
iframe: HTMLIFrameElement,
worker: Worker,
setBuildErrorMessage?: (error: string | null) => void,
getLatestContent?: (vizId: string) => Promise<VizContent | null>,
resolveSlugKey?: (slugKey: string) => Promise<string | null>,
writeFile?: (fileName: string, content: string) => void,
handleRuntimeError?: (formattedErrorMessage: string) => void
});- iframe:
HTMLIFrameElement- The iframe element where the viz will be rendered - worker:
Worker- Web Worker instance that handles code building - setBuildErrorMessage:
(error: string | null) => void- Optional callback for handling build-time errors - getLatestContent:
(vizId: string) => Promise<VizContent | null>- Optional function to fetch viz content for cross-viz imports - resolveSlugKey:
(slugKey: string) => Promise<string | null>- Optional function to resolve viz slugs to IDs - writeFile:
(fileName: string, content: string) => void- Optional callback when code running in the iframe writes files - handleRuntimeError:
(formattedErrorMessage: string) => void- Optional callback for handling runtime errors (exceptions thrown during viz execution)
Security note (v4.5.1): The
windowListenerinsidecreateRuntimenow filters messages byevent.sourceto ensure it only processes messages from its own iframe. Previously, anywindow.postMessagewith matching types (runtimeError,runError,runDone,writeFile) from any iframe on the page would be handled by this runtime, which could cause cross-iframe contamination when multiple runtimes are active (e.g., on an AI edit review page showing a "Before" and "After" comparison).
Returns a VizHubRuntime object with methods:
- run:
(options: RunOptions) => void- Executes code in the iframe- options.files:
FileCollection- Map of filenames to file contents - options.enableHotReloading:
boolean- Enable hot reloading (v3 runtime only) - options.enableSourcemap:
boolean- Enable source maps for debugging - options.vizId:
string- ID of current viz (required for v3)
- options.files:
- cleanup:
() => void- Removes event listeners from worker and iframe - invalidateVizCache:
(changedVizIds: string[]) => Promise<void>- Invalidates cache for specified viz IDs
import { createRuntime } from "@vizhub/runtime";
import BuildWorker from "./buildWorker?worker";
// Get iframe from DOM
const iframe = document.getElementById("viz-iframe");
// Create worker
const worker = new BuildWorker();
// Initialize runtime
const runtime = createRuntime({
iframe,
worker,
setBuildErrorMessage: (error) => {
error && console.error("Build error:", error);
},
getLatestContent: async (vizId) => {
// Fetch viz content from your backend
return await fetchVizContent(vizId);
},
resolveSlugKey: async (slugKey) => {
// Resolve slug to vizId from your backend
return await resolveSlug(slugKey);
},
});
// Run code in the iframe
runtime.run({
files: {
"index.js":
'console.log("Hello from VizHub runtime!");',
},
enableHotReloading: true,
enableSourcemap: true,
vizId: "example-viz",
});
// Clean up when done
runtime.cleanup();When using multiple createRuntime instances on the same page (e.g., a before/after comparison view), each runtime previously processed window.postMessage events from all iframes, not just its own. This caused runtime errors from one viz to appear as errors in an unrelated viz.
As of v4.5.1, each runtime filters messages by event.source, so errors only display in the correct iframe.
import { build } from "@vizhub/runtime";
import { rollup } from "rollup";
// Simple v1 runtime (HTML only)
const html = await build({
files: {
"index.html":
"<html><body><h1>Hello World</h1></body></html>",
},
});
// v2 runtime with bundling
const html = await build({
files: {
"index.html":
'<html><body><div id="root"></div><script src="bundle.js"></script></body></html>',
"index.js":
'import { message } from "./message"; console.log(message);',
"message.js":
'export const message = "Hello, bundled world!";',
},
rollup,
});
// Use the generated HTML in an iframe
const iframe = document.createElement("iframe");
iframe.srcdoc = html;
document.body.appendChild(iframe);import {
build,
createVizCache,
createSlugCache,
} from "@vizhub/runtime";
import { rollup } from "rollup";
import { compile } from "svelte/compiler";
// Create caches for viz content and slug resolution
const vizCache = createVizCache({
initialContents: [
{
id: "viz-123",
files: {
file1: {
name: "index.js",
text: "export const value = 42;",
},
},
},
],
handleCacheMiss: async (vizId) => {
// Fetch viz content from your backend
return await fetchVizContent(vizId);
},
});
const slugCache = createSlugCache({
initialMappings: {
"username/my-viz": "viz-123",
},
handleCacheMiss: async (slug) => {
// Resolve slug to vizId from your backend
return await resolveSlug(slug);
},
});
// Build HTML with cross-viz imports
const html = await build({
files: {
"index.js":
'import { value } from "@username/my-viz"; console.log(value);',
},
rollup,
vizCache,
vizId: "current-viz-id",
slugCache,
getSvelteCompiler: async () => compile,
});Builds HTML that can be used as the srcdoc of an iframe.
- files:
FileCollection- A map of filenames to their contents - rollup:
(options: RollupOptions) => Promise<RollupBuild>- Rollup function (required for v2, v3, v4) - enableSourcemap:
boolean- Whether to include sourcemaps (default: true) - vizCache:
VizCache- Cache for viz content (required for v3 with cross-viz imports) - vizId:
string- ID of the current viz (required for v3 with cross-viz imports) - slugCache:
SlugCache- Cache for slug resolution (optional for v3) - getSvelteCompiler:
() => Promise<SvelteCompiler>- Function that returns Svelte compiler (optional for v3)
Creates a cache for viz content.
- initialContents:
VizContent[]- Initial viz contents to populate the cache - handleCacheMiss:
(vizId: string) => Promise<VizContent>- Function to handle cache misses
Creates a cache for slug resolution.
- initialMappings:
Record<string, string>- Initial slug to vizId mappings - handleCacheMiss:
(slug: string) => Promise<string>- Function to handle cache misses
git clone https://github.com/vizhub-core/vizhub-runtime.git
cd vizhub-runtime
npm installnpm run buildnpm testRun specific tests:
npx vitest run -t "should handle CSS imports"npm run typecheckContributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Draft Pull Request
- Perform the Preflight Check
- Double check if the README needs to be updated with any new changes
- Ensure
DEBUG = trueis set back toDEBUG = falsein the code - Once everything is ready, mark the PR as ready for review
- Once approved, we will merge your PR!
Before finalizing a PR and marking it ready for review, please ensure that:
- Running
npm run preflightpasses without errors - The demo app is still working - run
npm run demoand click through the green buttons to see if everything still works
Dependency upgrades:
- Vite 7 → 8
- Vitest 3 → 4
- TypeScript 5 → 6
- Puppeteer 24 → 25
- Svelte 5.38 → 5.56
- Rollup 4.50 → 4.62
- All other devDeps and runtime deps updated to latest
Test infrastructure hardening:
- Added
--no-sandboxflag to all Puppeteerlaunch()calls for CI compatibility - Safer
browser.close()with null guard inafterAllhooks - Type-safe stack trace parsing in
testStackTrace.ts(error instanceof Error check, safer line number extraction) - Cleaned up stale comments and removed unused imports
TypeScript config:
- Added
rootDir: "./src"for correct declaration output structure - Changed
moduleResolutionto"bundler"for alignment with Vite's resolution strategy
Demo app:
- Added new
reactD3Combofixture demonstrating React + D3 integration - Fixed Vite
optimizeDeps.includefor transitive CJS deps (sucrase, ts-interface-checker) - Extended
server.fs.allowfor WASM file access from @rollup/browser
Fixed: The windowListener in createRuntime now checks event.source !== iframe.contentWindow before processing runDone, runError, runtimeError, and writeFile messages.
Background: When multiple createRuntime instances (or a createRuntime plus raw srcdoc-based iframes built via buildPreviewHtml) coexist on the same page, all iframes call parent.postMessage(...) with the same message types. Without the source check, one runtime could display errors from another iframe's viz, leading to confusing error reports in the wrong view (e.g., an "After" variant's AI-generated code error showing up in the "Before" comparison panel).
Impact: All consumers of @vizhub/runtime that run multiple iframes on one page benefit from this fix automatically — no code changes required.
Supports runtime versions v1–v4 with Rollup bundling, Svelte compilation, cross-viz imports, import maps, hot reloading, and more.
This project is licensed under the MIT License - see the LICENSE file for details.