RunJS is a browser-based JavaScript playground built with React and Vite. Write code in Monaco, run it in a sandboxed preview runtime, and share snippets through the URL.
- Monaco editor with a custom Dracula-based theme, Fira Code ligatures, bracket pair colorization, and TypeScript-powered semantic highlighting.
- Built-in snippets (
if,try,for,forof,forin) with tab stops. - IntelliSense for preview runtime globals (
log,warn,error,logTable,perf,expect,spyOn). - Editor context menu (copy, cut, paste, select all) via right-click or long-press on touch devices.
- Live execution in an isolated iframe runtime (
sandbox="allow-scripts"). - Captured output for
console.log,console.warn,console.error,console.info, and tabular data viaconsole.table/logTable. - Lightweight test helpers:
expectmatchers andspyOnfor call tracking. - URL-synced code state (debounced), so snippets can be shared as a link.
- Header actions: copy link, session history, new instance, code snapshot (PNG), and open in ChatGPT.
- Session history stored in
localStorage, with rename, reopen, and delete actions. - Resizable editor/preview layout (horizontal on desktop, vertical on mobile).
- Web app manifest and platform icons for installable/mobile-friendly behavior.
- React 19
- Vite 8
- TypeScript
- Tailwind CSS 4
- Monaco Editor
- Zustand
- ESLint (with pre-commit lint-staged via simple-git-hooks)
- Node.js (current LTS recommended)
- npm
npm installnpm run devOpen the URL shown in the terminal (typically http://localhost:5173).
npm run dev- start Vite dev servernpm run build- create production buildnpm run preview- preview production build locallynpm run lint- run ESLint checksnpm run lint:fix- run ESLint and auto-fix issuesnpm run phoenix- cleandistandnode_modules, reinstall, and build
- The editor content is stored in a Zustand store.
- Changes are debounced and encoded into the
codeURL parameter. - The preview builds an HTML runtime and injects your code into a
try/catchblock. - Console calls are intercepted in the iframe and sent to the app via
postMessage. - Output is rendered in the preview panel with type-aware styling.
- Starting a new session stores the previous snippet in session history (deduped by payload) for later retrieval.
The editor is Monaco configured in src/components/editor/config.ts. A custom TypeScript worker (ts.worker.ts) exposes semantic classifications to a document-range semantic tokens provider (semantic-tokens.ts), so variables, parameters, types, and members are highlighted beyond plain syntax coloring.
Type a prefix and pick the snippet from autocomplete (or press Tab/Enter to accept):
| Prefix | Inserts |
|---|---|
if |
if (condition) { } |
try |
try { } catch (error) { } — also matches “try catch” |
for |
Indexed for loop |
forof |
for (const item of iterable) — also matches “for of” |
forin |
for (const key in object) — also matches “for in” |
Snippets expand with tab stops for conditions, bindings, collections, and loop bodies.
The same config registers TypeScript extra libs so autocomplete and hovers work for preview-only globals (log, warn, error, logTable, perf, expect, spyOn, and their matcher methods).
Inside the preview runtime, a few global helper functions are available in addition to console.*:
log(...args)- same behavior asconsole.log(...), output appears in the preview panel.warn(...args)- same behavior asconsole.warn(...).error(...args)- same behavior asconsole.error(...).info(...args)- same behavior asconsole.info(...).logTable(...args)- same behavior asconsole.table(...), rendered as a table in the preview panel.expect(value)- creates async assertions with matchers (see below).spyOn(object, methodName)- wraps a method to record calls while still invoking the original implementation.
perf calls your function and prints:
=>[perf] Function name: <label>duration: <ms>msmemory usage: <delta> MB(orunavailable in this browser/runtime)
perf returns the wrapped function result. If the function is async/Promise-like, perf returns that Promise and reports metrics in a finally block.
Sync example:
perf(
() => {
for (let i = 0; i < 1_000_000; i++) {
// do something
}
},
{ label: 'loop' },
);Async example:
await perf(
async () => {
await new Promise((resolve) => setTimeout(resolve, 250));
},
{ label: 'fetch simulation' },
);Supported options:
label(string) - custom function name shown by the helper. Defaults tofn.nameoranonymous.
expect can receive either a direct value or a callback. If a callback is provided, it is awaited before each assertion.
Available matchers:
expect(value).toBe(expected)- checks strict equality (===) and returns a Promise.expect(value).toEqual(expected)- checks deep structural equality and returns a Promise.expect(value).stringMatching(expected)- checks that a string containsexpected(whenexpectedis a string) or matches it (whenexpectedis a RegExp).expect(value).objectContaining(expectedObject)- checks that all expected keys exist in the received object and their values are deep-equal.expect(value).arrayContaining(expectedArray)- checks that each expected item exists in the received array using deep equality.expect(spy).toHaveBeenCalled()- checks that a spy created withspyOnwas called at least once.expect(spy).toHaveBeenCalledTimes(n)- checks an exact call count.expect(spy).toHaveBeenCalledWith(...args)- checks that at least one call used the given arguments (deep equality).
Examples:
expect(2 + 2).toBe(4);
expect({ id: 1, tags: ['a'] }).toEqual({ id: 1, tags: ['a'] });
expect(() => Promise.resolve({ id: 1 })).toEqual({ id: 1 });
expect('Hello World').stringMatching('World');
expect('Version v1.2.3').stringMatching(/v\d+\.\d+\.\d+/);
expect({ id: 1, user: { name: 'Max' } }).objectContaining({
user: { name: 'Max' },
});
expect([{ id: 1 }, { id: 2 }]).arrayContaining([{ id: 2 }]);spyOn replaces an object method with a wrapper that records every call (arguments and return/throw results) while still running the original implementation.
spy.mock.calls- array of argument lists, one per call.spy.mock.results- array of{ type: 'return' | 'throw', value }per call.spy.mockRestore()- restores the original method on the object.
Example:
const counter = {
value: 0,
increment() {
this.value += 1;
},
};
const incrementSpy = spyOn(counter, 'increment');
counter.increment();
counter.increment();
expect(incrementSpy).toHaveBeenCalled();
expect(incrementSpy).toHaveBeenCalledTimes(2);
expect(incrementSpy).toHaveBeenCalledWith();
incrementSpy.mockRestore();- Create a branch for your change.
- Run
npm run lintbefore opening a PR (pre-commit hooks also runlint:fixon staged files). - Keep changes focused and include a clear PR description.