Skip to content

Add a runtime

The Run code button of 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.

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

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.

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:

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;
  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.
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.

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:

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.

  • 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.