Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ Then open the local URL shown by Vite. (The lockfile is generated with [Bun](htt

The app is built with TanStack Start and Vite. User code runs in a sandboxed iframe, and the event-loop trace is generated from Acorn-based AST instrumentation plus controlled queue tracking.

## Embed this

The REPL and event-loop visualizer are also available as a standalone, iframe-friendly page — no app chrome, no search, just the tool — so you can drop a runnable example straight into a blog post or docs page.

The embed lives at `/embed`. Add `?example=<id>` to preload a specific snippet (either a REPL example id or an interview-pattern id from the main site); unknown ids fall back to the default example. `?view=loop` opens the event-loop view instead of the console.

```html
<iframe
src="https://runtimejs.vercel.app/embed?example=event-loop-order"
width="100%"
height="480"
style="border:0;border-radius:8px;"
></iframe>
```

The small "via runtime.js" badge in the corner links back to the main site, which is the whole point of embedding it.

## Analytics

Cookie-less, privacy-friendly analytics are built in but **disabled by default** — no script is loaded until you configure a provider:
Expand Down
30 changes: 27 additions & 3 deletions src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
import { Route as EmbedRouteImport } from './routes/embed'
import { Route as EraEraRouteImport } from './routes/era.$era'
import { Route as EraEraIndexRouteImport } from './routes/era.$era.index'
import { Route as EraEraConceptRouteImport } from './routes/era.$era.$concept'
Expand All @@ -19,6 +20,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const EmbedRoute = EmbedRouteImport.update({
id: '/embed',
path: '/embed',
getParentRoute: () => rootRouteImport,
} as any)
const EraEraRoute = EraEraRouteImport.update({
id: '/era/$era',
path: '/era/$era',
Expand All @@ -37,32 +43,42 @@ const EraEraConceptRoute = EraEraConceptRouteImport.update({

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/embed': typeof EmbedRoute
'/era/$era': typeof EraEraRouteWithChildren
'/era/$era/$concept': typeof EraEraConceptRoute
'/era/$era/': typeof EraEraIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/embed': typeof EmbedRoute
'/era/$era/$concept': typeof EraEraConceptRoute
'/era/$era': typeof EraEraIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/embed': typeof EmbedRoute
'/era/$era': typeof EraEraRouteWithChildren
'/era/$era/$concept': typeof EraEraConceptRoute
'/era/$era/': typeof EraEraIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/era/$era' | '/era/$era/$concept' | '/era/$era/'
fullPaths: '/' | '/embed' | '/era/$era' | '/era/$era/$concept' | '/era/$era/'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/era/$era/$concept' | '/era/$era'
id: '__root__' | '/' | '/era/$era' | '/era/$era/$concept' | '/era/$era/'
to: '/' | '/embed' | '/era/$era/$concept' | '/era/$era'
id:
| '__root__'
| '/'
| '/embed'
| '/era/$era'
| '/era/$era/$concept'
| '/era/$era/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
EmbedRoute: typeof EmbedRoute
EraEraRoute: typeof EraEraRouteWithChildren
}

Expand All @@ -75,6 +91,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/embed': {
id: '/embed'
path: '/embed'
fullPath: '/embed'
preLoaderRoute: typeof EmbedRouteImport
parentRoute: typeof rootRouteImport
}
'/era/$era': {
id: '/era/$era'
path: '/era/$era'
Expand Down Expand Up @@ -114,6 +137,7 @@ const EraEraRouteWithChildren =

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
EmbedRoute: EmbedRoute,
EraEraRoute: EraEraRouteWithChildren,
}
export const routeTree = rootRouteImport
Expand Down
140 changes: 140 additions & 0 deletions src/routes/embed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { lazy, Suspense, useEffect, useState } from "react";
import { createFileRoute, ClientOnly } from "@tanstack/react-router";
import { REPL_EXAMPLES } from "@/lib/js-eras";
import { INTERVIEW_PATTERNS } from "@/lib/interview-patterns";

const Workbench = lazy(() => import("@/components/devtools/Workbench"));

type EmbedView = "console" | "loop";

const DEFAULT_CODE = REPL_EXAMPLES[0]!.code;

/** Resolve an example/pattern id to code, falling back to the first REPL example. */
function codeForExample(example?: string): string {
if (!example) return DEFAULT_CODE;
const repl = REPL_EXAMPLES.find((e) => e.id === example);
if (repl) return repl.code;
const pattern = INTERVIEW_PATTERNS.find((p) => p.id === example);
if (pattern) return pattern.code;
return DEFAULT_CODE;
}

function labelForExample(example?: string): string | undefined {
if (!example) return undefined;
return (
REPL_EXAMPLES.find((e) => e.id === example)?.label ??
INTERVIEW_PATTERNS.find((p) => p.id === example)?.title
);
}

export const Route = createFileRoute("/embed")({
validateSearch: (
search: Record<string, unknown>,
): { example?: string | undefined; view: EmbedView } => {
const example = typeof search["example"] === "string" ? search["example"] : undefined;
return {
...(example !== undefined ? { example } : {}),
view: search["view"] === "loop" ? "loop" : "console",
};
},
head: () => ({
meta: [
{ title: "Embeddable REPL — runtime.js" },
{
name: "description",
content:
"A small embeddable sandboxed REPL and event loop visualizer. Drop it into any page with an iframe.",
},
{ name: "robots", content: "noindex, nofollow" },
],
}),
component: Embed,
});

function Embed() {
const { example, view: searchView } = Route.useSearch();
const [code, setCode] = useState(() => codeForExample(example));
const [view, setView] = useState<EmbedView>(searchView);

// React to ?example= / ?view= changes without a reload (e.g. SPA links).
useEffect(() => {
setCode(codeForExample(example));
}, [example]);

useEffect(() => {
setView(searchView);
}, [searchView]);

const exampleLabel = labelForExample(example);

return (
<div className="flex h-svh flex-col overflow-hidden bg-background">
<header className="flex shrink-0 flex-wrap items-center gap-x-3 gap-y-1 border-b border-border bg-panel px-3 py-1.5">
<a
href="/"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 rounded-sm"
aria-label="runtime.js — back to the full site"
>
<span className="grid size-4 place-items-center rounded-[3px] bg-primary text-[9px] font-bold text-primary-foreground">
JS
</span>
<span className="text-xs font-bold tracking-tight">
runtime<span className="text-muted-foreground">.js</span>
</span>
</a>

{exampleLabel && (
<span className="hidden min-w-0 truncate text-[11px] text-muted-foreground sm:block">
{exampleLabel}
</span>
)}

<div className="ml-auto flex overflow-hidden rounded-sm border border-border">
{(["console", "loop"] as const).map((v) => (
<button
key={v}
onClick={() => setView(v)}
aria-pressed={view === v}
className={`px-2.5 py-1 text-[11px] transition-colors ${
view === v
? "bg-secondary text-secondary-foreground"
: "bg-transparent text-muted-foreground hover:text-foreground"
}`}
>
{v === "console" ? "console" : "event loop"}
</button>
))}
</div>
</header>

<main className="min-h-0 flex-1 p-2 sm:p-3">
<ClientOnly fallback={<EmbedFallback>booting sandbox…</EmbedFallback>}>
<Suspense fallback={<EmbedFallback>loading editor…</EmbedFallback>}>
<Workbench code={code} onCodeChange={setCode} view={view} />
</Suspense>
</ClientOnly>
</main>

<footer className="flex shrink-0 items-center justify-end gap-2 border-t border-border bg-panel px-3 py-1">
<a
href="/"
target="_blank"
rel="noopener noreferrer"
className="rounded-sm text-[10.5px] font-medium text-muted-foreground transition-colors hover:text-primary"
>
via runtime.js
</a>
</footer>
</div>
);
}

function EmbedFallback({ children }: { children: React.ReactNode }) {
return (
<div className="flex min-h-[300px] items-center justify-center rounded-sm border border-border bg-card p-6 text-sm text-muted-foreground">
{children}
</div>
);
}
21 changes: 20 additions & 1 deletion src/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,25 @@ const errorMiddleware = createMiddleware().server(async ({ next }) => {
}
});

// The /embed route exists to be dropped into <iframe>s on third-party sites.
// Everything else keeps whatever frame protection applies; this one route is
// explicitly exempted so blogs can embed it without a framing block.
const embedFrameMiddleware = createMiddleware().server(async ({ pathname, next }) => {
const result = await next();
if (pathname !== "/embed") return result;
const response = result instanceof Response ? result : result.response;
if (!(response instanceof Response)) return result;
const headers = new Headers(response.headers);
headers.delete("X-Frame-Options");
headers.set("Content-Security-Policy", "frame-ancestors *");
const framed = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
return result instanceof Response ? framed : { ...result, response: framed };
});

// Start installs this automatically when src/start.ts is absent; defining the
// file opts out, so re-add it explicitly to keep server functions protected
// from cross-site requests.
Expand All @@ -25,5 +44,5 @@ const csrfMiddleware = createCsrfMiddleware({
});

export const startInstance = createStart(() => ({
requestMiddleware: [errorMiddleware, csrfMiddleware],
requestMiddleware: [errorMiddleware, csrfMiddleware, embedFrameMiddleware],
}));
Loading