@bigmistqke/repl
provides unstyled building blocks to create TypeScript playgrounds directly in the browser, featuring adaptable editor integration. Currently, it supports both the feature-rich Monaco Editor
and the lighter Shiki Editor
It supports real-time transpilation of TypeScript into ECMAScript Modules (ESM) and facilitates seamless imports of external dependencies, including their type definitions, making it ideal for both quick prototyping and complex browser-based IDE development.
Screen.Recording.2024-04-29.at.22.46.58.mov
Click here for a line-by-line explanation of the above example and here for a live-demo.
- Modular Editor Integration: Start with
Monaco Editor
for a fully featured IDE-like experience orShiki Editor
for a more minimal editor. The architecture is designed to accommodate additional editors as needed. - Real-time Transpilation: Transpile TypeScript into ESM on-the-fly, enabling immediate feedback and iteration.
- Automatic Dependency Management: Effortlessly manage imports of external libraries and their associated types, streamlining the development process.
- Configurable and Extensible: Tailor the setup to your needs with configurable TypeScript compiler settings, and easily extend functionalities through a flexible API.
Import package from package manager.
npm install `@bigmistqke/repl`
yarn add `@bigmistqke/repl`
pnpm install `@bigmistqke/repl`
Initializes the Repl environment by dynamically loading the required libraries (Babel
, TypeScript
and monaco-editor
) and any Babel presets/plugins defined in the props. Configures and instantiates Runtime
, which sets up FileSystem
, TypeRegistry
and FrameRegistry
. The component ensures no children are rendered until all dependencies are fully loaded and the optionally provided onSetup
-prop has been resolved.
It provides access for its descendants to its internal Runtime
through the useRuntime
-hook.
Usage
<Repl
typescript={{
resolveJsonModule: true,
esModuleInterop: true,
...
}}
initialState={{
files: {
// Add 2 files to the virtual file-system.
sources: {
// One exporting a sum-function.
'src/sum.ts': `export const sum = (a: number, b: number) => a + b`,
// Another importing this function and exporting a subtract-function.
'src/index.ts': `import { sum } from "./sum";
export const sub = (a: number, b: number) => sum(a, b * -1)`,
}
}
}}
class={styles.repl}
onSetup={({ fileSystem }) => {
createEffect(async () => {
// Get the module-url of the File at a given path.
const moduleUrl = fileSystem.get('src/index.ts')?.module.url
if (!moduleUrl) return
// Import the subtract-function of the module.
const { sub } = await import(moduleUrl)
// Call the function.
console.log(sub(2, 1)) // Will log 1
})
}}
>
Props
- babel: Configuration for Babel transformations.
presets
: Array of string identifiers for Babel presets.plugins
: Array of plugins or strings for Babel transformations.
- cdn: Cdn to import external dependencies from, defaults to
esm.sh
. - initialState: Defines the initial state of the filesystem with predefined files and content.
files
:sources
: Record of virtual path and source-code (.js
/.jsx
/.ts
/.tsx
/.css
).alias
: Record of package-name and virtual path.
types
:sources
: Record of virtual path and source-code (.d.ts
).alias
: Record of package-names and virtual path.
- mode: Theme mode for the editor, either
light
ordark
. - onSetup:
- A function that runs after the editor setup is complete. It allows access to the
Runtime
for custom initialization scripts; for example pre-loading a local package. - The initial file-system state will only be processed after this callback returns. This callback can be async.
- A function that runs after the editor setup is complete. It allows access to the
- typescript: Configuration options for the TypeScript compiler, equal to
tsconfig.json
.
Type
type ReplProps = ComponentProps<'div'> & Partial<ReplConfig>
type ReplConfig = {
babel: {
presets: string[]
plugins: (string | babel.PluginItem)[]
}
cdn: string
initialState: {
files: {
sources: Record<string, string>
alias: Record<string, string>
}
types: {
sources: Record<string, string>
alias: Record<string, string[]>
}
}
mode: 'light' | 'dark'
onSetup: (replContext: Runtime) => Promise<void> | void
typescript: TypescriptConfig
actions?: {
saveRepl?: boolean
}
}
Manages individual <iframe/>
containers for isolated execution environments.
Usage
<Repl.Frame
style={{ flex: 1 }}
name="frame-2"
bodyStyle={{
padding: '0px',
margin: '0px',
}}
/>
Props
- name: An identifier for the (
Frame
)(#frame). Defaults todefault
. - bodyStyle: CSS properties as a string or JSX.CSSProperties object to apply to the
<iframe/>
body.
Type
type FrameProps = ComponentProps<'iframe'> &
Partial<{
name: string
bodyStyle: JSX.CSSProperties | string
}>
Repl.DevTools
embeds an iframe to provide a custom Chrome DevTools interface for debugging purposes, provided by chii
and chobitus
.
This component connects to a Repl.Frame
with the same name
prop to display and interact with the frame's runtime environment, including console outputs, DOM inspections, and network activities. If no name
is provided it will default to default
.
Usage
// To debug a frame named 'example':
<Repl.Frame name="example" />
<Repl.DevTools name="example" />
Props
- props: Props include standard iframe attributes and a unique
name
used to link the DevTools with a specificRepl.Frame
.
Type
type ReplDevToolsProps = ComponentProps<'iframe'> & { name: string }
Returns
- Returns the iframe element that hosts the embedded Chrome DevTools, connected to the specified
Repl.Frame
.
Example
// Example usage to integrate the DevTools with a named frame:
<Repl.Frame name="exampleFrame" />
<Repl.DevTools name="exampleFrame" />
A minimal wrapper around <For/>
to assist with navigating between different files opened in the editor.
Usage
<Repl.TabBar style={{ flex: 1 }}>
{({ path }) => <button onClick={() => setCurrentPath(path)}>{path}</button>}
</Repl.TabBar>
Props
- children: A callback with the
path
and the correspondingFile
as arguments. Expects aJSX.Element
to be returned. - paths: An array of strings to filter and sort existing paths.
Type
type TabBarProps = ComponentProps<'div'> & {
children: (arg: { path: string; file: File | undefined }) => JSXElement
paths: string[]
}
Repl.MonacoEditor
embeds a monaco-editor
instance for editing files. This editor supports integrated typing assistance, including auto-completions and type-checking, and offers the standard keybindings expected in code editors.
The Repl.MonacoProvider
component is responsible for initializing Monaco and making it available to descendant components via context. This setup enables multiple instances of monaco-editor to utilize a single Monaco instance. It is essential that all Repl.MonacoEditor
components are nested within a Repl.MonacoProvider
.
Usage
<Repl.MonacoProvider>
<Repl.MonacoEditor
style={{ flex: 1 }}
path={currentPath()}
onMount={editor => {
createEffect(on(currentPath, () => editor.focus()))
}}
/>
</Repl.MonacoProvider>
Props
- path: The file path in the virtual file system to bind the editor to.
- onMount: Callback function that executes when the editor is mounted, with the current
monaco-editor
as argument.
Type
type EditorProps = ComponentProps<'div'> & {
path: string
onMount?: (editor: MonacoEditor) => void
}
Repl.ShikiEditor
is a tiny, minimal text editor built on the shiki
syntax highlighting library, which utilizes TextMate
grammar. Internally, it is composed of a standard <textarea/>
with syntax-highlighted HTML rendered underneath.
In contrast to the Repl.MonacoEditor
, Repl.ShikiEditor
lacks type-checking and type-information capabilities, and it does not modify any existing key-bindings. As such, it is not ideal for full featured playgrounds, but is well-suited for simpler applications such as articles and documentation.
Usage
<Repl.ShikiEditor style={{ flex: 1 }} path={currentPath()} />
Props
- path: The file path in the virtual file system to bind the editor to.
- themes: A light/dark shiki-theme
Type
type EditorProps = ComponentProps<'div'> & {
path: string
themes: {
dark: string
light: string
}
}
Hook to interact with the internal Runtime
of @bigmistqke/repl
. This class contains the virtual FileSystem
, TypeRegistry
and FrameRegistry
.
This hook should be used in a descendant of Repl
, otherwise it will throw.
Usage
const { frameRegistry, fileSystem } = useRuntime()
const frame = frameRegistry.get('default')
const entry = fileSystem.get('src/index.ts')
frame?.injectFile(entry)
Type
type useRuntime = (): Runtime
The Runtime
class serves as the central coordination point of the Repl
environment, integrating essential libraries and configurations necessary for its operation. It orchestrates interactions between various subsystems, including the file system, frame registry, type management, and code transpilation. This setup ensures a cohesive and efficient development environment within the browser.
Key Methods and Properties
- config: Configurations for the runtime environment that ensure mandatory settings like 'cdn' are always included. The 'cdn' is crucial for loading external libraries such as TypeScript.
- fileSystem: Manages file operations within the virtual file system. It is responsible for creating, retrieving, managing, and manipulating files and directories. See
FileSystem
. - frameRegistry: Handles the registration and management of iframe containers for isolated code execution. This is crucial for maintaining security and stability by sandboxing different parts of code execution. See
FrameRegistry
. - typeRegistry: Manages TypeScript type definitions within the system. This component is essential for providing accurate type information, enhancing code quality and IntelliSense in the editor. See
TypeRegistry
. - import: Manages the import of modules and dependencies from URLs or package names, streamlining the integration of external libraries and frameworks. see
Import
. - transpiler: Utilizes Babel and Typescript to transform code according to specified typescript-config, babel-presets and -plugins.
- toJSON(): Serializes the current state of the REPL into a JSON format. This method is useful for saving the state of the environment for later restoration or sharing.
- initialize(state): Sets up the initial state of the file system and type registry based on provided configurations. This method ensures that all necessary files and types are preloaded and ready for use.
- download(name): Allows users to download the current state of the REPL as a JSON file. This functionality is helpful for backing up configurations or sharing them with others. The default filename is
repl.config.json
, but it can be customized.
Type
class Runtime {
constructor(
public libs: {
typescript: Ts,
babel: Babel,
babelPresets: any[],
babelPlugins: Babel.PluginItem[]
},
config: ReplConfig,
)
config: Mandatory<ReplConfig, 'cdn'>
fileSystem: FileSystem
frameRegistry: FrameRegistry
typeRegistry: TypeRegistry
import: Import
transpiler: Transpiler
initialize(): void
toJSON(): ReplState
download(name: string = 'repl.config.json'): void
}
The Import
utility-class facilitates the importation of external packages into the REPL environment by managing the fetching and parsing of a package.json
file. This utility class enables importing dependencies that are not uploaded to an esm-friendly cdn like esm.sh
.
This class is available from Runtime.import
.
Key Properties and Methods
- fromPackageJson(url): Asynchronously imports a package by parsing its
package.json
from the specified URL. This method oversees the entire process from fetching thepackage.json
, parsing it, resolving paths, loading scripts, and integrating type definitions, ensuring all components are properly configured within the REPL environment.
Usage
const runtime = useRuntime()
runtime.import.fromPackageJson('https://example.com/package.json')
This method is particularly useful for dynamically loading packages that are not pre-bundled with the application, allowing for a more flexible and expandable development environment.
Type
class Import {
constructor(public runtime: Runtime) {}
async fromPackageJson(url: string): Promise<void>
}
The Transpiler
utility-class within the REPL environment is designed to manipulate and transform TypeScript module declarations within the provided code. This class is available from Runtime.transpiler
Key Properties and Methods
- transformModuleDeclarations(code, callback): Transforms import/export declarations based on the criteria defined in the callback. The callback can directly modify the nodes by returning updated nodes, or it can remove nodes by returning
false
. If an exception is thrown within the callback, it halts further execution. It is used internally byTypeRegistry
andJsModule
.
Usage
const runtime = useRuntime()
const updatedCode = runtime.transpiler.transformModuleDeclarations(originalCode, node => {
if (node.moduleSpecifier.text.includes('old-path')) {
node.moduleSpecifier.text = 'new-path'
}
})
Type
class Transpiler {
constructor(private runtime: Runtime) {}
transformModuleDeclarations(
code: string,
callback: (node: ts.ImportDeclaration | ts.ExportDeclaration) => void | false,
): string | undefined {
// Transformation logic...
}
}
The FileSystem
API manages a virtual file system, allowing for the creation, retrieval, and manipulation of files as well as handling imports and exports of modules within the monaco-editor environment.
Key Methods and Properties
- create(path): Creates and returns a new
File
instance at the specified path. - get(path): Retrieves a
File
instance by its path. - has(path): Checks if a
File
exists at the specified path. - resolve(path): Resolves a path according to TypeScript resolution rules, supporting both relative and absolute paths. Returns
File
orundefined
. - importFromPackageJson(url): Imports a package from a specified URL by parsing its package.json.
- initialize(): Initializes the file system with the specified initial state, including preloading files and setting aliases.
Type
class FileSystem {
constructor(
public repl: Runtime,
)
alias: Record<string, string>
config: ReplConfig
packageJsonParser: PackageJsonParser
typeRegistry: TypeRegistry
addProject(files: Record<string, string>): void
all(): Record<string, File>
create(path: string): File
get(path: string): File | undefined
has(path: string): boolean
importFromPackageJson(url: string): Promise<void>
initialize(): void
resolve(path: string): File | undefined
toJSON(): {
files: {
sources: Record<string, string>
alias: Record<string, string>
}
types: {
sources: Record<string, string>
alias: Record<string, string[]>
}
}
}
Abstract class representing a source-file withing the virtual FileSystem
.
JsFile
extends from File
and is linked to a JsModule
, managing JavaScript files within the virtual `FileSystem.
Key Methods and Properties
- module: Linked to a
JsModule
for handling JavaScript execution specifics. - get(): Retrieves the current source code.
- set(value): Updates the source code.
- toJSON(): Serializes the source code to a JSON-compatible string.
Type
class JsFile extends File {
module: JsModule
constructor(runtime: Runtime, path: string) {}
}
CssFile
extends from File
and is linked to a CssModule
, specializing in CSS file management.
Key Methods and Properties
- module: Associated with a
CssModule
for CSS management. - get(): Retrieves the current CSS content.
- set(value): Updates the CSS content.
- toJSON(): Returns the CSS content as a JSON-compatible string.
Type
class CssFile extends File {
module: CssModule
constructor(path: string) {}
}
Abstract class representing an esm-representation of a given source-file withing the virtual FileSystem
.
JsModule
represents a JavaScript module within the runtime, extending the generic Module
class. It is responsible for transpilation and execution of JavaScript code, managing dependencies, and tracking CSS imports.
Key Methods and Properties
- url: Retrieves the currently active module URL.
- generate(): Generates a new URL for an ES Module based on current source code.
- dispose(frame): Cleans up module-specific artifacts or bindings from the provided frame.
Type
class JsModule extends Module {
generate: Accessor<string | undefined>
url: string | undefined
dispose(frame: Frame)
constructor(runtime: Runtime, file: JsFile) {}
}
CssModule
manages CSS content, transpiling stylesheets into executable JavaScript modules that dynamically apply styles within a document.
Key Methods and Properties
- url: Retrieves the currently active module URL.
- generate(): Generates executable JavaScript to apply styles dynamically.
- dispose(frame): Removes the style element from the document.
Type
class CssModule extends Module {
generate: Accessor<string | undefined>
url: string | undefined
dispose(frame: Frame)
constructor(file: CssFile) {}
}
Manages a registry of Frame
instances, each associated with its distinct Window
.
Key Methods and Properties
- add(name, window): Adds a new
Frame
with the given name and window reference. - delete(name): Removes a
Frame
from the registry. - get(name): Retrieves a
Frame
by name. - has(name): Checks if a
Frame
exists by name.
class FrameRegistry {
add(name: string, window: Window): void
delete(name: string): void
get(name: string): Frame
has(name: string): boolean
}
Represents an individual <iframe/>
within the application. It offers method to inject and execute JsFile
and CssFile
into its Window
. Creation of Frame
is done internally by the Repl.Frame
component.
Key Methods and Properties
- injectFile(file): Injects the module of a given
File
into the frame's Window. - injectModuleUrl(url): Injects a given module-url into the frame's Window.
class Frame {
constructor(public window: Window)
injectModuleUrl(file: File): HTMLScriptElement | undefined
injectFile(file: File): HTMLScriptElement | undefined
dispose(file: File): void
}
The TypeRegistry
class manages TypeScript type definitions across the application, enhancing the editor's IntelliSense by maintaining accurate type information and resolving type definitions from various sources.
Key Methods and Properties
- initialize(initialState): Initializes the registry with predefined types and aliases.
- toJSON(): Converts the current state of the registry into a JSON object for serialization.
- aliasPath(packageName, virtualPath): Maps a package name to an aliased path.
- set(path, value): Adds or updates a type definition in the registry.
- has(path): Checks if a type definition is already registered.
Types
type TypeRegistryState = {
alias: Record<string, string[]>
sources: Record<string, string>
}
class TypeRegistry {
sources: Record<string, string>
alias: Record<string, string[]>
import: TypeImport
constructor(runtime: Runtime) {}
}
TypeImport
utitilies assists the TypeRegistry
by importing type definitions from URLs or package names.
It is available through TypeRegistry.import
.
Key Methods and Properties
- fromUrl(url, packageName): Imports type definitions from a URL if they are not already cached.
- fromPackageName(packageName): Imports type definitions based on a package name by resolving it to a CDN path.
- initialize(initialState: Partial): Caches the initial state of type sources and aliases to prevent re-fetching.
Types
class TypeImport {
constructor(runtime: Runtime, typeRegistry: TypeRegistry) {}
async fromUrl(url: string, packageName?: string): Promise<void>
async fromPackageName(packageName: string): Promise<void>
}
This basic example illustrates the core functionality of setting up a TypeScript playground using @bigmistqke/repl. It demonstrates how to initialize the REPL environment, load a simple TypeScript file, and execute a function from it within the browser. This example is ideal for those new to @bigmistqke/repl, showcasing how straightforward it is to get started with creating browser-based development environments. For a more interactive experience, check out the live demo.
<Repl
// Initialize repl-state
initialState={{
files: {
'src/index.ts': 'export const greet = (): string => "Hello, world!";',
},
}}
onSetup={({ fileSystem }) => {
// Get file from file-system
const file = fileSystem.get('src/index.ts')
// Get esm-url from file
const moduleUrl = file?.module.url
// Import module and call its function
import(moduleUrl).then(module => console.log(module.greet()))
}}
/>
This application demonstrates complex interactions between various components and hooks, designed to facilitate an interactive and intuitive coding environment directly in the browser. Click here for a live-demo.
import { Repl, useRuntime, JsFile } from '@bigmistqke/repl'
import { solidReplPlugin } from '@bigmistqke/repl/plugins'
import { createEffect, createSignal, mapArray, on, onCleanup } from 'solid-js'
import { JsxEmit } from 'typescript'
// Main component defining the application structure
const App = () => {
// State management for the current file path, initialized to 'src/index.tsx'
const [currentPath, setCurrentPath] = createSignal('src/index.tsx')
// Setting up the editor with configurations for Babel and TypeScript
return (
<Repl
babel={{
// Babel preset for SolidJS
presets: ['babel-preset-solid'],
// Plugin to enhance SolidJS support in Babel
plugins: [solidReplPlugin],
}}
typescript={{
// Preserve JSX to be handled by another transformer (e.g., Babel)
jsx: JsxEmit.Preserve,
// Specify the JSX factory functions import source
jsxImportSource: 'solid-js',
// Enable all strict type-checking options
strict: true,
}}
// Initialize repl's state
initialState={{
files: {
'src/index.css': `body { background: blue; }`,
'src/index.tsx': `import { render } from "solid-js/web";
import "./index.css"
const Counter = () => {
const [count, setCount] = createSignal(0)
const increment = () => setCount(count => count + 1)
return <button onClick={increment}>{count()}</button>
}
render(Counter, document.body)`,
},
}}
// CSS class for styling the Repl container-component
class={styles.repl}
// Event called when all dependencies are loaded
onSetup={async ({ fileSystem, frameRegistry }) => {
createEffect(() => {
// Access the default frame
const frame = frameRegistry.get('default')
if (!frame) return
// Get the current main file
const entry = fs.get(currentPath())
if (entry instanceof JsFile) {
// Inject the JS file into the iframe for execution
frame.injectFile(entry)
// Cleanup action to remove injected scripts on component unmount
onCleanup(() => frame.window.dispose?.())
// Process CSS imports and inject them into the iframe
createEffect(
mapArray(entry.cssImports, css => createEffect(() => frame.injectFile(css))),
)
}
})
}}
>
<div style={{ overflow: 'hidden', display: 'flex', 'flex-direction': 'column' }}>
<Repl.TabBar class={{ display: 'flex' }}>
{({ path }) => <button onClick={() => setCurrentPath(path)}>{path}</button>}
</Repl.TabBar>
<Repl.Editor style={{ flex: 1 }} path={currentPath()} />
</div>
<Repl.Frame
style={{ flex: 1 }}
bodyStyle={{ padding: '0px', margin: '0px' }} // Style for the iframe body
/>
</Repl>
)
}
export default App
The main inspiration of this project is my personal favorite IDE: solid-playground. Some LOC are copied directly, p.ex the css- and js-injection into the iframe.