Editor Singleton & Client/Server Split

The Editor class is the central hub of the frontend editor. This page documents its events, methods, and properties, and explains the TypeScript compilation split between client and server code.

The Editor Singleton

Editor lives at src/editor/Editor.ts and compiles to public/editor/Editor.js. It is a singleton accessed everywhere via Editor.get(). It owns all open document state, the FileEditorRegistry, and the events that panels and the tab container subscribe to. It is the only place where files are fetched from the server and where saves are sent back.

Never import Editor in server-side code. It is a browser-only module. The split between client and server code is described in the section below.

Accessing the singleton

import { Editor } from '../../editor/Editor.js';

const editor = Editor.get();

The .js extension is required in all client-side imports because the TypeScript output is consumed directly by the browser as ES modules (no bundler).

Properties

Property Type Description
projectId string UUID of the open project. Set by EditorShell on init from the URL query string.
projectName string Display name of the open project. Set by EditorShell on init.
openDocs Map<string, { content, dirty }> In-memory cache of all documents fetched this session. Keyed by file path.
activeDoc string | null File path of the most recently opened document.
fileEditorRegistry FileEditorRegistry Suffix-to-editor-tag resolver. Loaded lazily on first openDocument call.

Methods

Editor.get(): Editor

Returns the singleton instance, creating it on first call.

openDocument( filePath: string ): Promise<void>

The main entry point for opening a file. Loads the registry (once), resolves the editorTag for the file's suffix. If no tag is found, dispatches onFileTypeUnknown and returns early. Otherwise fetches the file content (cached after first fetch), then dispatches onDocumentOpened.

markDirty( filePath: string, content: string ): void

Called by a panel whenever the user edits content. Updates the in-memory cache and dispatches onDocumentDirty. The tab container uses this to show the dirty dot.

save( filePath: string ): Promise<void>

PUTs the cached content to /api/files/${projectId}/${filePath} with Content-Type: text/plain. Clears the dirty flag and dispatches onDocumentSaved.

Events

All events use EventSlot from the shared library — not DOM events and not a pub/sub bus. Add a listener with Editor.get().onSomeEvent.addListener( e => ... ). Listeners are called synchronously when the event is dispatched.

onDocumentOpenedEventSlot<DocumentOpenedEvent>

interface DocumentOpenedEvent {
  path: string;      // file path relative to project root
  content: string;   // raw file content
  editorTag: string; // custom element tag resolved by FileEditorRegistry
}

Fired after the file is fetched and the editor tag is resolved. Every editor panel listens to this. Panels must check editorTag and return early if it does not match their own tag — all panels receive every event.

Editor.get().onDocumentOpened.addListener( ( e ) =>
{
  if ( 'my-panel' !== e.editorTag ) return;
  this._loadDocument( e.path, e.content );
} );

onDocumentDirtyEventSlot<DocumentPathEvent>

interface DocumentPathEvent { path: string; }

Fired by markDirty. The TabContainer listens and sets tab.dirty = true for the tab whose panel's currentPath matches. Panels do not need to listen to this themselves — they call markDirty and update their own Save button.

onDocumentSavedEventSlot<DocumentPathEvent>

Fired by save after a successful PUT. The TabContainer listens and clears the dirty dot for the matching tab.

onFilesChangedEventSlot<void>

Fired with no payload when the file tree changes (file or folder created, renamed, or deleted). FileTreePanel listens and re-fetches the tree. Dispatch it after any operation that modifies the filesystem:

Editor.get().onFilesChanged.dispatch();

onFileTypeUnknownEventSlot<DocumentPathEvent>

Fired when openDocument is called for a file whose suffix has no entry in FileEditorRegistry. FileTreePanel listens and shows a 3-second error banner. No file content is fetched when this fires.

Client / Server Split

Two separate TypeScript pipelines

Client and server TypeScript are compiled independently and must never import from each other's side.

Side Source Output How it runs
Client src/ public/ npm run buildtsc --build tsconfig.client.json
Server server/ none (in-process) npm startts-node with tsconfig.ts-node.json
Library (browser) src/library-ts/browser/ public/library-ts/browser/ Compiled via TypeScript project references as part of npm run build

CSS is not compiled

Component CSS is written directly in public/components/<name>/<name>.css and is never processed by TypeScript. Do not put CSS files in src/. Edit the file in public/ directly; the browser picks it up on next reload with no build step.

After changing code

  • Client TypeScript changed — run npm run build, then reload the browser.
  • Server TypeScript changed — restart the server (npm start). No build step.
  • CSS changed — reload the browser. No build step.

Module imports on the client

Client TypeScript uses module: ESNext and moduleResolution: bundler. There is no bundler — the browser receives individual .js files as ES modules. All imports in client code must include the .js extension, even though the source files end in .ts:

import { Editor } from '../../editor/Editor.js';
import { EventSlot } from '../library-ts/browser/events/EventSlot.js';

TypeScript resolves these correctly during compilation because moduleResolution: bundler allows importing .js paths that correspond to .ts source files.