Skip to content

@ox-content/code-play#

View Markdown

Code Play runs documentation samples on demand. It is a separate plugin: @ox-content/vite-plugin does not enable it, and installing this package does nothing until you list languages.

The docs example on this site and the standalone examples/code-play app demonstrate the browser UI. Both enable JavaScript, TypeScript, Rust, and Go; the standalone app also renders Python with an explicit Piston-compatible endpoint when one is configured. Rust, Go, and remote languages stay off unless you opt in.

Install#

vp install @ox-content/code-play@beta
pnpm add @ox-content/code-play@beta
bun add @ox-content/code-play@beta
npm install @ox-content/code-play@beta
yarn add @ox-content/code-play@beta

import { oxContent } from "@ox-content/vite-plugin";
import { codePlay } from "@ox-content/code-play";

export default {
  plugins: [
    oxContent({ highlight: true }),
    codePlay({
      languages: {
        typescript: { execute: true, typecheck: true },
        javascript: true,
        rust: true,
        go: true,
        python: { endpoint: "https://piston.example/api/v2/piston" },
      },
      ui: "default",
      viewers: { config: true, stdio: true, stderr: true, provenance: true, timing: true },
      srcDir: "content",
    }),
  ],
};

The plugin is a second opt-in layer on top of the package install. A fence without play, or a language that is not listed, stays an ordinary highlighted block. Pages without a matching Code Play block do not receive the hydration script, and builds that never use Code Play do not emit ox-code-play.js.

Plugin options#

Option Type Default Role
languages Record<string, true | LanguageEnableOptions> {} Enable execute / typecheck / endpoint
ui "default" | "compact" | "headless" default Chrome around the sample
viewers Partial<ViewerFlags> all on Show or hide stdio / stderr / config / …
timeoutMs number 10000 Per-run timeout
endpoints { rust?, go?, typecheck? } official Playground / typecheck URLs
proxy boolean true Mount Vite dev /__ox-code-play/*
srcDir string "docs" Markdown root used to match play fences
outDir string Vite out Written HTML to enhance after SSG
base string "/" Public path for ox-code-play.js

LanguageEnableOptions accepts execute, typecheck, endpoint, and config overrides for that language's schema (TypeScript strict, Rust crateType, Go withVet, …).

Authoring#

Mark a fence with play. Add typecheck when the language supports it.

```ts play typecheck play-title="Strict TypeScript" play-strict=false play-target=ESNext
const n: number = 1;
console.log(n);
```

```rust play typecheck play-title="Release-mode Rust" play-mode=release
fn main() {
    println!("ok");
}
```

```go play typecheck play-title="Go vet on"
package main

import "fmt"

func main() {
    fmt.Println("ok")
}
```

```python play play-title="Python via Piston"
print("ok")
```

play-title labels the widget. play-compact / play-headless override the UI preset for one sample, play-timeout=2500 overrides the timeout, and play-viewers=stdio,stderr,-timing toggles viewers. play-<config-key>=... sets one language config value for that sample, so TypeScript can use play-strict=false, Rust can use play-edition=2021, and Go can use play-withVet=false. Python and other remote languages need their languages.<id>.endpoint set at plugin configuration time.

HTML / MDX form:

<CodePlay lang="ts" title="Loose TS" typecheck ui="compact" config-strict="false">
  const n = 1;
</CodePlay>

Project-level examples opt in per sample with play-project or project. The current fence stays the primary executable snippet, while project metadata adds file names, provider choice, and an external fallback link:

```ts play play-project=stackblitz play-file=src/main.ts play-entry=src/main.ts play-files=package.json,src/App.tsx play-project-url=https://stackblitz.com/edit/example
console.log("project");
```

play-file names the current fence inside the project. play-files is a comma-separated list of extra files, resolved relative to the Markdown source file and confined to srcDir. Supported provider metadata adapters are stackblitz, codesandbox, webcontainer, and external. Code Play does not load provider scripts; the generated widget renders project metadata and an Open fallback link when a safe http(s) URL is supplied.

Headless API#

import { createCodePlay } from "@ox-content/code-play";

const play = createCodePlay({ languages: { typescript: true } });
const session = play.createSession({
  language: "ts",
  code: "const n: number = 1;\nconsole.log(n);",
});

const check = await session.typecheck();
const run = await session.run();

run.stdio; // timestamped stdin / stdout / stderr events
run.stdout; // concatenated stdout text
run.stderr; // concatenated stderr text
run.provenance; // where it compiled, where it ran
run.timing; // phase durations and totalMs
session.config; // editable language config

createCodePlay() throws if you ask for a language that is not enabled. session.setConfig({ strict: false }) updates the same object the config viewer edits. session.cancel() aborts an in-flight run or typecheck and returns status: "cancelled". The default toolbar shows Cancel while a run is busy. Inject transport (for example createMemoryTransport) in tests so CI never hits a live playground.

Field Meaning
run.status ok / error / offline / timeout / cancelled / unsupported
run.stdio Timestamped stdin / stdout / stderr events
run.stdout Concatenated stdout text
run.stderr Concatenated stderr text
run.diagnostics Compiler / runtime messages with optional line/col
run.provenance Where it compiled and where it ran
run.timing Phase durations and totalMs
run.preview Framework iframe srcdoc when the backend is UI
session.stdout Same as lastResult.stdout
session.stderr Same as lastResult.stderr

Custom UIs can use the exported RunActionState helpers: idleRunActionState(), runningRunActionState(action), and resultRunActionState(action, result). Transport and CORS failures use status: "offline" so they can be styled separately from compiler/runtime errors.

UI#

Preset Behavior
default Toolbar plus stdio / stderr / config / provenance / timing tabs
compact Run / type-check plus stdio and stderr
headless No DOM chrome; use the session API

Viewers can be toggled independently through viewers. The hydrated widget exposes a polite status region, aria-busy, tab panels, and arrow-key tab navigation.

Languages#

Languages Execute Type-check Backend
TypeScript yes yes local strip-types + tsgo + node:vm
Rust yes yes play.rust-lang.org (or endpoints.rust)
Go yes yes play.golang.org (or endpoints.go)
JavaScript yes no node:vm / sandbox iframe
Vue, React, Svelte, Solid yes no iframe srcdoc + esm.sh import map
Python, PHP, Ruby, sh, … yes no Piston-compatible languages.<id>.endpoint

The full catalog is the same list as the roadmap. Aliases such as ts, c++, bash, and coq resolve to the canonical id.

Playground proxies#

Vite dev server only. codePlay({ proxy: true }) (the default) mounts:

Path Forwards to
POST /__ox-code-play/rust endpoints.rust (default https://play.rust-lang.org/execute)
POST /__ox-code-play/go endpoints.go (default https://play.golang.org/compile)
POST /__ox-code-play/typecheck local tsgo (no remote compiler)

These routes accept POST only, cap the body at 256 KiB, and refuse non-http(s) destinations or URLs with embedded credentials. Upstream failures return generic JSON { "error": "..." } and do not leak fetch details.

The proxy is not installed in production SSG output. Set endpoints to the official playgrounds (or your own HTTPS executor) for published pages, or proxy: false if you do not want the dev middleware.

Static hosts do not serve POST /__ox-code-play/typecheck. TypeScript Run still works in the browser (strip types, then a sandboxed iframe). The Typecheck button is omitted from published widgets unless you set a reachable endpoints.typecheck. The Vite proxy path is used only during vite dev.

Rust and Go on a published page call endpoints.rust / endpoints.go directly from the browser. The official playgrounds are the defaults; point endpoints at an executor you control when you need stricter isolation, auditing, or a fallback if an upstream playground changes its browser policy.

During vite dev, Code Play payloads use /__ox-code-play/rust and /__ox-code-play/go by default when proxy is enabled. Explicit endpoints.rust and endpoints.go values are preserved. Production builds embed the configured endpoints instead, so static hosts do not depend on the dev middleware.

Python has no bundled public executor. Configure a Piston-compatible languages.python.endpoint that you operate or trust. If Python is enabled without an endpoint, the widget still renders, but Run returns status: "unsupported" with an endpoint diagnostic instead of silently doing nothing.

Security#

play fences are trusted site content, same as any other script you ship. Do not mark visitor-supplied or unreviewed snippets as play.

  • Samples are not executed during Markdown transform or SSG.
  • JavaScript / TypeScript execute in node:vm on Node, or in <iframe sandbox="allow-scripts"> in the browser (no allow-same-origin). They are never run with page-origin Function. The sample cannot read the host page's DOM or storage.
  • Vue / React / Svelte / Solid previews use the same iframe flags and srcdoc. Preview runtimes load from esm.sh.
  • sh never spawns a local shell on the docs host.
  • Rust / Go POST source to play.rust-lang.org / play.golang.org (or your endpoints override). Those hosts see the sample and their privacy policy applies.
  • A Piston-compatible languages.<id>.endpoint receives source for that language. Only set HTTPS endpoints you trust, without embedded credentials.
  • Project sandbox payloads embed trusted source for the current fence and any play-files entries. Extra files must be relative paths under the Markdown source root; symlink real paths are checked before embedding, missing or oversized files become widget warnings, and provider URLs are limited to http(s) without credentials.

First publish#

@ox-content/code-play is new on npm. Trusted publishing cannot create the package, so a maintainer publishes once from a laptop, then registers the trusted publisher with npm trust. Commands live in Release Operations.

See the Code Play roadmap for follow-up PRs.

Last updated: