---
title: "Add a runtime"
description: "Add a runtime for another language, so that the Run code button can run its code blocks in the browser."
url: "https://ewels.github.io/starlight-codeblocks/extend/add-a-runtime/"
markdown: "https://ewels.github.io/starlight-codeblocks/extend/add-a-runtime.md"
section: "Extend"
site: "starlight-codeblocks documentation"
context: "This page is from the documentation of starlight-codeblocks. A Starlight plugin that adds focus, line states, annotations, API auto-linking and 22 more features to the code blocks of a site."
index: "https://ewels.github.io/starlight-codeblocks/llms.txt"
---

# Add a runtime

> Add a runtime for another language, so that the Run code button can run its code blocks in the browser.

The **Run code** button of [run code](https://ewels.github.io/starlight-codeblocks/features/run-code/) gives the code of a block to a runtime. The plugin has runtimes for Python, JavaScript and TypeScript. A site can add a runtime for any other language in the `runnable.runtimes` option, or replace a built-in one.

## The interface

A runtime is a module whose default export has two functions:

```ts
interface Runtime {
  load(code: string, options?: { packages?: string[] }): Promise<void>;
  run(code: string, options: { signal: AbortSignal; session?: boolean }): Promise<{ stdout: string; stderr: string }>;
}
```

| Function | Description |
|---|---|
| `load()` | Downloads and starts the runtime, and gets the packages that `code` needs. `packages` holds the names in the block's `runnable.packages` attribute. The plugin calls it with the code before each run, so it must return at once when the runtime and the packages are ready. The timeout does not apply to it. |
| `run()` | Runs the copied text of the block, and returns its standard output and standard error as text. |

The browser imports the module when a reader clicks **Run code** for the first time, and calls `load()` at once. Import the `Runtime` type from `starlight-codeblocks` to check a module in TypeScript.

When the block is a session with prompts, `code` is its commands and `session` is `true`. Run the commands as the language's REPL does, so that the output shows the value of each expression, as the block does.

The `signal` of `run()` aborts when the run takes longer than `runnable.timeout`. The runtime must then stop the code and reject the promise. The plugin shows the timeout message, and calls `load()` again before the next run.

## An example: the JavaScript runtime

The built-in JavaScript runtime is a complete example. It runs the code in a web worker. Each run gets a new worker, which the runtime ends when the top-level code is done or the signal aborts. The output of a timer or a callback that the code does not `await` is lost, because the worker is gone when it runs:

```ts title="starlight-codeblocks/src/runtimes/javascript.ts"
import type { Runtime } from '../options.ts';

// The worker has no access to the page. console.log, info, warn and error go to the output panel.
const source = `
const stdout = [];
const stderr = [];
const format = (a) => {
  if (typeof a === 'string') return a;
  if (typeof a === 'bigint') return a + 'n';
  if (a instanceof Error) return String(a);
  if (a instanceof Map || a instanceof Set) return a.constructor.name + ' ' + format([...a]);
  try {
    return JSON.stringify(a) ?? String(a);
  } catch {
    return String(a);
  }
};
const text = (args) => args.map(format).join(' ');
console.log = console.info = (...args) => stdout.push(text(args));
console.error = console.warn = (...args) => stderr.push(text(args));
const done = () => postMessage({ stdout: stdout.join('\\n'), stderr: stderr.join('\\n') });
// Errors in timers and other callbacks escape the try/catch.
onerror = (message) => {
  stderr.push(String(message));
  done();
  return true;
};
onunhandledrejection = ({ reason }) => {
  stderr.push(String(reason));
  done();
};
onmessage = async ({ data: code }) => {
  try {
    const AsyncFunction = (async () => {}).constructor;
    await new AsyncFunction(code)();
  } catch (error) {
    stderr.push(String(error));
  }
  done();
};`;

let url: string | undefined;

/** Runs JavaScript in a new web worker, which ends when the top-level code is done or the signal aborts. */
export function runJavaScript(code: string, { signal }: { signal: AbortSignal }) {
  url ??= URL.createObjectURL(new Blob([source], { type: 'text/javascript' }));
  const worker = new Worker(url);
  return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
    signal.addEventListener('abort', () => {
      worker.terminate();
      reject(signal.reason);
    });
    worker.onmessage = ({ data }) => {
      worker.terminate();
      resolve(data);
    };
    worker.postMessage(code);
  });
}

const runtime: Runtime = {
  async load() {},
  run: runJavaScript,
};

export default runtime;
```

## Add a runtime to a site

1. Save the module in the project, for example as `src/runtimes/ruby.ts`.
2. Open `astro.config.mjs`.
3. Add the language and the path of the module to `runnable.runtimes`.

```js title="astro.config.mjs"
codeblocks({
  runnable: {
    runtimes: { ruby: './src/runtimes/ruby.ts' },
  },
});
```

A code block with ` ```ruby runnable ` now has the **Run code** button. The key is the language name, and its aliases work too: `rb` and `ruby` both use this runtime. A path that starts with `.` is from the project root. Any other path is a package path, such as `starlight-codeblocks/runtimes/pyodide`.

A runtime for `python`, `javascript` or `typescript` replaces the built-in one. The built-in runtimes are `starlight-codeblocks/runtimes/pyodide`, `starlight-codeblocks/runtimes/javascript` and `starlight-codeblocks/runtimes/typescript`. A site runtime can import them, for example to run JavaScript after it compiles another language.

## Change the Pyodide URL

The Python runtime loads Pyodide from the jsDelivr CDN. To load it from a different URL, make a runtime module with the `pyodide()` function of the package:

```ts title="src/runtimes/python.ts"
import { pyodide } from 'starlight-codeblocks/runtimes/pyodide';

export default pyodide({ url: 'https://example.com/pyodide/v314.0.7/full/' });
```

Then map `python` to it in `runnable.runtimes`, as for any other runtime. The URL is the folder that holds `pyodide.mjs`, with a trailing slash. The runtime resolves a relative URL, such as `/pyodide/full/` for a copy in `public/`, against the page.

## Rules

- Run the code away from the page, in a web worker or a sandboxed `iframe`. Code in a worker cannot freeze the page or read it.
- Stop the code when the signal aborts. Code in a worker stops only when the runtime ends the worker.
- Return an error in the code as `stderr`, and resolve the promise. Reject the promise only when the runtime cannot run code at all. The panel then shows the message of the error.
- Download nothing when the module is imported. Start downloads in `load()`.
- Give each run a clean state, so that the result of a block does not depend on the blocks that ran before it.
- Test the runtime in a browser, with an error, an endless loop and a block with no output.
- Without `codeblocks()`, a site that adds the Expressive Code plugins itself has no build step for runtimes. The values of `runnable.runtimes` are then URLs of JavaScript modules, and there are no built-in runtimes unless the site adds them.
