Skip to content

RFC: Unified Xote-based playground #8596

Description

@cknitt

Summary

ReScript currently has two separate browser playground implementations:

  1. The Xote-based development playground in this repository under packages/dev-playground.
  2. The React-based production playground behind rescript-lang.org/try, implemented in rescript-lang/rescript-lang.org.

This RFC proposes consolidating them into one reusable Xote-based implementation while preserving the combined feature set.

Note

packages/playground in this repository is not a third playground UI. It produces the browser compiler and .cmij artifacts consumed by playgrounds.

Current implementations

Xote development playground

The development playground is closely coupled to compiler development and supports:

  • Live compilation
  • Compiler builds from the current checkout, master, and pull requests
  • Intermediate compiler output: parse tree, typed tree, Lambda, Lam, and JavaScript
  • Module-system selection
  • Warning configuration
  • JSX and let? options
  • Additional libraries
  • Formatting, reset, and share links
  • A lightweight custom editor built from a textarea, syntax-highlighting overlay, line numbers, and active-line highlighting

It currently does not provide:

  • Program execution or captured console output
  • React rendering
  • CodeMirror diagnostics and hover information
  • Vim mode
  • Light and dark themes
  • Resizable panes
  • Browser-level or end-to-end tests

Production /try playground

The production playground is a large React implementation centered around:

It supports:

  • CodeMirror 6
  • Run and auto-run
  • JavaScript, output, problems, and settings panels
  • Formatting and share links
  • Released compiler versions
  • React execution and captured console output in an iframe
  • Vim mode and light and dark themes
  • Module-system selection
  • JSX preservation and let?
  • Detailed warning flags
  • Resizable panes
  • Diagnostics, hovers, and type information where supported

It does not provide the development playground’s current-checkout, master, and pull-request compiler workflows or its intermediate compiler representations.

The resizable panes are implemented directly in Playground.res, without a split-pane dependency. The implementation tracks dragging, installs global mouse and touch listeners, updates percentage widths or heights, and changes orientation at a responsive breakpoint.

Feature comparison

Capability Development playground Production /try
Live compilation Yes Yes
Format/reset/share Yes Yes
Released compiler versions No Yes
Current checkout/master/PR builds Yes No
Intermediate compiler output Yes No
CodeMirror 6 No Yes
Diagnostics/hovers/type information No Yes
Execute JavaScript No Yes
React rendering No Yes
Captured console output No Yes
Vim mode and themes No Yes
Resizable panes No Yes
Browser/E2E coverage No Yes

Findings

The compiler API varies by version and source

A unified playground needs to load released, development, and pull-request compiler bundles through one abstraction. Capabilities vary across compiler versions, so UI logic should not infer support from version strings or scatter compatibility checks throughout components.

A normalized compiler adapter should expose explicit capabilities:

type capabilities = {
  formatting: bool,
  typeHints: bool,
  debugOutput: bool,
  jsxPreserve: bool,
  experiments: bool,
}

Capabilities with multiple possible values should expose those values directly instead of adding more booleans. For example, the adapter can return the available intermediate-output kinds.

Compiler source identity should use normal variants with payloads where required:

type compilerSource =
  | Release(Semver.t)
  | Master
  | PullRequest(int)
  | Local

type compilerDescriptor = {
  source: compilerSource,
  label: string,
  root: string,
}

This covers published releases, master, pull requests, and the locally built compiler without representing invalid states such as a pull-request source without a PR number. Stable IDs can be derived from the source, while labels and asset locations remain configurable by the host.

Legacy Reason syntax is out of scope. The unified playground only needs to support ReScript syntax.

Keep the existing /try URL format

A new URL format is not necessary. The existing production /try format should remain canonical for newly generated links.

The unified implementation should:

  1. Continue decoding existing ReScript /try links.
  2. Preserve the current production format for ordinary links.
  3. Add parameters only for state the format cannot currently represent, such as a pull-request compiler source or selected intermediate representation.
  4. Add fixtures ensuring existing ReScript /try links continue to decode.
  5. Test that newly generated links use the same canonical format.

Development-playground URLs do not require backward compatibility or migration. Legacy Reason links are also out of scope.

Xote needs an explicit disposal API for React hosting

The production route is currently React-based. A reusable Xote playground could be mounted as an island from the React route during migration, but Xote 7.1’s public View.mount and View.mountById APIs return unit and expose no public unmount or disposal handle.

Removing the host element directly would bypass Xote’s internal disposal and can leak subscriptions and event handlers.

Xote should expose something equivalent to:

let dispose = Playground.mount(container, options)

// Later:
dispose()

or a root handle with an explicit unmount method.

This lifecycle functionality belongs in Xote itself rather than in a playground-specific workaround.

Program execution should remain isolated

Xote should render the playground UI, but user programs should continue to run in an iframe. The evaluator should be retained and hardened around:

  • Sandbox permissions
  • Origin validation
  • Message validation
  • Runtime/compiler version selection
  • React runtime loading
  • Cleanup between runs

The UI framework and the runtime used by compiled user programs are separate concerns. An Xote playground can still execute and render React examples inside the output iframe.

Proposed architecture

1. Framework-neutral PlaygroundEngine

Extract the non-visual behavior into a reusable core:

  • Compiler catalog and loader
  • Compiler-version compatibility adapters
  • Normalized compilation results
  • Playground state and settings
  • /try URL decoding and encoding
  • Program evaluator protocol
  • Capability detection

This layer should not depend on React or Xote.

2. Xote PlaygroundView

Build one Xote UI on top of the engine, using CodeMirror 6 rather than the development playground’s custom textarea editor.

CodeMirror 6 remains the current major CodeMirror architecture. Its packages are versioned independently, so the implementation should use current compatible releases rather than treating “6” as a single package version.

The view should merge both playground feature sets:

  • Editing, diagnostics, hover and type information
  • Live compilation, formatting, and sharing
  • JavaScript, output, problems, and settings
  • Program execution, console output, and React rendering
  • Released, local, master, and pull-request compilers
  • Intermediate compiler representations
  • Themes and Vim mode
  • Resizable panes

Intermediate compiler representations should be exposed through an output selector:

  • JavaScript
  • Parse tree
  • Typed tree
  • Lambda
  • Lam

JavaScript remains the default. Unsupported representations should be disabled or omitted based on the selected compiler’s declared capabilities.

3. Reusable Xote SplitPane

Replace the current React-specific resizing logic with a reusable Xote SplitPane component.

It should support:

  • Horizontal and vertical layouts
  • Responsive orientation changes
  • Pointer events for mouse, touch, and pen input
  • Minimum pane sizes
  • Keyboard resizing
  • An accessible separator
  • Correct listener and subscription cleanup when unmounted

The current behavior is small enough that an additional split-pane dependency does not appear necessary.

4. Thin host integrations

Provide two small entry points around the same view:

  • A standalone host for the compiler repository’s development playground
  • A React wrapper for /try that mounts the Xote view and disposes it from useEffect cleanup

This allows the implementation to be introduced incrementally without requiring the documentation site to stop using React.

The available compiler sources should be configurable by the host:

  • Production /try shows released versions by default.
  • The development host shows the local build, master, pull-request builds, and optionally releases.
  • Non-release sources may remain accessible on /try through explicit URLs without appearing in its normal compiler selector.

5. Canonical package ownership and naming

The shared implementation should live in this compiler repository and be published as @rescript/playground for consumption by the website.

Reasons:

  • Development bundles, pull-request previews, and intermediate representations are closely coupled to compiler internals.
  • Compiler API compatibility can evolve alongside the artifact producer.
  • The website can consume a stable package instead of maintaining a second compiler integration.

The existing private artifact producer under packages/playground should remain, but be renamed to playground-compiler to distinguish it from the published UI package.

The public package name should not include “Xote,” since Xote is an implementation detail rather than part of its external contract.

Proposed migration

  1. Define contracts and parity tests

    • Inventory both feature sets as testable behavior.
    • Define normalized compiler, capability, state, URL, and evaluator interfaces.
    • Capture existing ReScript /try URLs as fixtures.
  2. Extract the shared engine

    • Move compiler loading, compatibility handling, URL state, and evaluation behind framework-neutral APIs.
    • Keep both existing UIs operational while this is introduced.
  3. Introduce CodeMirror 6 in the Xote view

    • Port the production editor capabilities rather than extending the custom textarea implementation.
    • Preserve diagnostics, hovers, keyboard behavior, themes, and Vim support.
  4. Reach production feature parity

    • Port output, problems, settings, execution, React rendering, console capture, and resizing.
    • Add released versions to the unified compiler catalog.
    • Retain the development-only compiler and debug-output workflows.
  5. Add Xote unmount and disposal support

    • Make the view safe to host inside the React route.
    • Test repeated mount and unmount cycles for leaked subscriptions and listeners.
  6. Replace hosts incrementally

    • Replace the standalone development playground first.
    • Mount the same package from /try after parity and compatibility tests pass.
  7. Remove the duplicate implementation

    • Delete the old React playground UI once the Xote package is established in production.
    • Keep only the thin website route wrapper and the compiler artifact producer.

Testing requirements

The consolidated implementation should include:

  • Unit tests for compiler compatibility adapters
  • Fixtures for existing ReScript /try links
  • Tests for canonical URL decoding and encoding
  • Editor integration tests for diagnostics, hovers, and formatting
  • Evaluator tests for execution, console output, errors, and React rendering
  • SplitPane interaction and accessibility tests
  • Repeated mount and unmount tests
  • Browser/E2E coverage for released, local, master, and pull-request compiler sources
  • Feature-parity scenarios covering both existing playgrounds

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions