clone

a browser-first diagram editor with live svg previews, local drafts and encrypted, accountless sharing

main
diagramzip / docs / monaco-editor-migration.md
monaco-editor-migration.md 20.2K rendered
1# Monaco Editor migration specification23Status: Implemented in this worktree; real-device mobile validation pending45Scope: `diagramzip` web application67Branch: `codex/monaco-editor-spec`89## Summary1011Replace the CodeMirror 6 source editor with Monaco Editor while preserving the12existing diagram state, draft, preview, save, share, and type-switching13behaviour.1415This should not ship as an unconditional component swap. Monaco explicitly does16not support mobile browsers, while diagram.zip has a deliberate mobile editor17flow. The recommended release shape is:1819- Monaco on supported desktop browsers.20- A small native `<textarea>` editor on touch-only/mobile browsers, loaded21 without Monaco.22- A release gate requiring real-device mobile checks and explicit product23 acceptance of the reduced mobile editing experience.2425If syntax highlighting and code-editor features must remain first-class on26mobile, the recommendation is to keep CodeMirror rather than perform this27migration. Shipping both Monaco and CodeMirror indefinitely is not recommended;28it doubles the editor integration surface and weakens the dependency-removal29benefit.3031## Motivation3233Monaco provides a richer desktop editing platform for future diagnostics,34completion, formatting, and language-aware actions. The migration should first35establish behavioural parity and a maintainable editor boundary. Language36servers, renderer-aware completion, and schema-driven validation are follow-up37features, not reasons to broaden the initial change.3839## Current implementation4041CodeMirror is coupled to the application in three places:42431. `apps/editor/src/main.js` creates `EditorView`, reads the document in44 `currentState()`, listens for edits, and replaces the document and language45 in `applyState()`.462. `apps/editor/src/diagram-types.js` imports CodeMirror language packages and47 returns editor-specific language extensions.483. `apps/editor/src/style.css` targets `.cm-*` selectors.4950The important behavioural contract is small:5152- `currentState()` must synchronously read the complete source.53- User edits schedule `commitState()` after 1.2 seconds.54- Programmatic document replacement must not schedule an intermediate commit.55- Changing diagram type replaces both source and syntax mode while preserving56 per-type drafts.57- Save, restore, copy, remote-load, locked-diagram, and preview flows all call58 through the same state functions.59- The editor fills its grid panel at desktop and mobile sizes.60- Command/Ctrl+S saves instead of invoking the browser action.6162The current production build is approximately 618 kB of JavaScript raw and63195 kB gzip. This is the baseline for evaluating Monaco's cost; it is not a64budget that Monaco is expected to match.6566## Goals6768- Preserve all source/state semantics and the 1.2-second preview debounce.69- Preserve undo/redo, find, line numbers, selection, bracket matching, syntax70 colouring, keyboard editing, and clipboard behaviour on desktop.71- Keep editor-specific APIs out of application state and diagram catalog code.72- Use Monaco's ESM build and Vite-generated module workers under the existing73 `/diagram.zip/` base path.74- Load only the editor features, languages, and workers diagram.zip uses.75- Keep source usable if local storage, rendering, or remote persistence fails.76- Provide a functional mobile fallback when Monaco is not selected.77- Remove CodeMirror packages after the fallback decision is accepted.7879## Non-goals8081- VS Code extension compatibility.82- A language server or custom completion service.83- Renderer-backed validation or formatting.84- Per-diagram-type semantic grammars for all 30 renderers.85- Multiple open files, tabs, diff views, or a command palette.86- Dark mode; the application currently defers it across editor and renderers.87- Persisting cursor, selection, fold, scroll, or undo history across reloads.88- Preserving undo history when an external state or diagram-type draft replaces89 the entire document. This matches the conceptual boundary between drafts.9091## Proposed design9293### 1. Introduce an editor-neutral contract9495Add `apps/editor/src/source-editor.js` as the only module imported by96`main.js`. It owns backend selection and exposes this interface:9798```js99const sourceEditor = await createSourceEditor({100 element,101 source,102 diagramType,103 onChange,104 onSave,105})106107sourceEditor.getValue()108sourceEditor.setDocument({ source, diagramType })109sourceEditor.focus()110sourceEditor.layout()111sourceEditor.dispose()112```113114`setDocument()` is atomic from the application's perspective: it suppresses115backend change notifications, replaces the value, changes the language, then116re-enables notifications. Use a nesting counter with `try/finally`, not a bare117boolean, so an exception cannot leave notifications disabled.118119`main.js` changes only at these seams:120121- Await editor creation after initial state has loaded.122- Replace `editor.state.doc.toString()` with `sourceEditor.getValue()`.123- Replace the CodeMirror dispatch in `applyState()` with124 `sourceEditor.setDocument()`.125- Route save keyboard handling through the `onSave` callback while retaining126 the window-level shortcut for focus outside the editor. The focused Monaco127 path must stop or de-duplicate propagation so a single shortcut produces one128 save attempt.129130Move editor language selection out of `diagram-types.js`. That module should131remain a pure diagram catalog/query-string module and be testable without a DOM132or Monaco.133134### 2. Monaco backend135136Add `apps/editor/src/editors/monaco-editor.js` and137`apps/editor/src/editors/monaco-languages.js`.138139Use Monaco 0.56.x (0.56.0 is current at the time of this spec) through its ESM140API. The lockfile remains the source of truth for the exact version. Do not use141the deprecated AMD build or a third-party Monaco Vite plugin.142143Use one `ITextModel` and one standalone editor. A single model matches the144current application, where only one diagram draft is active at a time. On145external replacement:1461471. Enter notification suppression.1482. Call `model.setValue(source)`.1493. Call `monaco.editor.setModelLanguage(model, languageId)`.1504. Clamp the cursor to the new document and reveal it.1515. Exit suppression.152153Dispose the content-change subscription, editor, and model together. This is154required for development hot reload and tests even though the production page155normally owns one editor for its lifetime.156157Recommended editor options:158159```js160{161 ariaLabel: 'Diagram source editor',162 automaticLayout: true,163 contextmenu: true,164 folding: true,165 fontFamily: resolvedMonospaceFont,166 fontSize: 14,167 glyphMargin: false,168 lineHeight: 23,169 lineNumbers: 'on',170 minimap: { enabled: false },171 overviewRulerBorder: false,172 padding: { top: 28, bottom: 80 },173 renderLineHighlight: 'all',174 scrollBeyondLastLine: false,175 stickyScroll: { enabled: false },176 wordWrap: 'off',177}178```179180The Monaco minimap stays off because the adjacent preview already has a181diagram minimap. Resolve `--font-mono` to a concrete computed value before182passing it to Monaco so font measurement does not depend on Monaco resolving a183CSS custom property.184185Define a `diagramzip` theme from the existing CSS colour variables. Keep the186source of truth in CSS, read the resolved colours at startup, and pass concrete187colour values to `monaco.editor.defineTheme()`. Do not hard-code a second visual188palette in JavaScript.189190### 3. Workers and Vite191192Import workers through Monaco 0.56's public ESM export paths with Vite's193`?worker` form and set194`self.MonacoEnvironment.getWorker` before creating the editor:195196- `monaco-editor/editor/editor.worker.js?worker` for the editor.197- `monaco-editor/language/json/json.worker.js?worker` for strict JSON.198199Return the JSON worker only for the `json` label and the editor worker for all200other labels. CSS, HTML, and TypeScript workers are out of scope. The production201build must emit hashed, same-origin worker assets whose URLs include the Vite202base path; there must be no root-relative `/assets/...` assumption.203204The browser console warning that Monaco could not create workers is a release205failure. Do not accept Monaco's main-thread fallback because it can cause input206jank and hides deployment errors.207208### 4. Language mapping209210Register only the languages needed for parity. Prefer Monaco's basic-language211ESM contributions and a custom Monarch tokenizer for the generic diagram212language. Do not import Monaco's aggregate language bundle.213214| Diagram types | Monaco language | Notes |215| --- | --- | --- |216| `excalidraw`, `vega`, `vegalite` | `json` | Strict JSON; use the JSON worker. Do not add renderer schemas in this change. |217| `wavedrom` | custom `json5` | The renderer accepts JSON5; strict JSON diagnostics would be incorrect. |218| `bpmn`, `diagramsnet`, `umlet` | `xml` | Syntax tokenization only. |219| `wireviz` | `yaml` | Syntax tokenization only. |220| `dbml` | `sql` | Preserves the current approximate highlighting. |221| `bytefield` | `clojure` | Corrects the current JSON misclassification; bytefield source is Clojure forms. |222| all remaining types | custom `diagram-source` | Port the existing comment, string, number, colour, operator, directive, and keyword rules to Monarch. |223224The generic language must preserve these current cases:225226- Block comments delimited by `/*` and `*/`.227- Line comments beginning with `%%`, `//`, `'`, or `#` after indentation.228- PlantUML-style `@start*` and `@end*` directives.229- Quoted strings with escapes.230- Diagram arrow/operator forms.231- Hex colours, numbers, known keywords, and identifiers.232233Add bracket and comment configuration only where it is correct across the234generic group. Do not add aggressive auto-closing rules that can corrupt ASCII235diagram formats such as Ditaa, GoAT, and Svgbob.236237### 5. Mobile fallback238239Add `apps/editor/src/editors/textarea-editor.js` implementing the same contract.240It provides plain text editing, selection, undo/redo supplied by the browser,241the save shortcut, full-document replacement, and change notifications. It does242not provide line numbers, syntax highlighting, folding, or find beyond the243browser's capabilities.244245Backend selection happens once at page startup. The initial policy is:246247- Use the textarea on touch-only devices whose primary pointer is coarse.248- Use Monaco elsewhere.249- Allow `?editor=textarea` as a temporary diagnostic escape hatch during the250 rollout. Do not persist it into saved or shared diagram state.251252Input-media detection is only a routing heuristic, not a claim that Monaco is253supported on every selected device. Test the target browser matrix below. Do254not hot-swap backends on resize or device rotation; doing so would complicate255focus and undo semantics.256257If product does not accept the reduced mobile feature set, stop the migration258and retain CodeMirror.259260### 6. Styling and layout261262- Replace `.cm-*` rules with the minimum `.monaco-editor` and textarea fallback263 rules.264- Keep `#editor`, `.editor-panel`, and the grid structure unchanged.265- Monaco must be laid out after the editor panel becomes visible. Call266 `sourceEditor.layout()` when switching the mobile tab back to Edit, even with267 `automaticLayout` enabled.268- Preserve the current surface, gutter, active-line, selection, cursor, font,269 and syntax colour intent.270- Verify that Monaco's find and suggestion widgets are not clipped by271 `.editor-panel { overflow: hidden; }`.272273## Dependency changes274275Expected file-level change set:276277| File | Change |278| --- | --- |279| `package.json`, `package-lock.json` | Add Monaco and remove CodeMirror dependencies. |280| `apps/editor/src/main.js` | Consume the editor-neutral contract; remove CodeMirror imports and direct APIs. |281| `apps/editor/src/diagram-types.js` | Remove editor imports and `languageFor()`. |282| `apps/editor/src/source-editor.js` | Select and expose a backend through one contract. |283| `apps/editor/src/editors/monaco-editor.js` | Create, configure, update, and dispose Monaco. |284| `apps/editor/src/editors/monaco-languages.js` | Register narrow language contributions, JSON5, and the generic Monarch grammar. |285| `apps/editor/src/editors/monaco-workers.js` | Configure the editor and JSON worker constructors. |286| `apps/editor/src/editors/textarea-editor.js` | Implement the mobile/support fallback. |287| `apps/editor/src/style.css` | Replace CodeMirror selectors and style the fallback. |288| `apps/editor/test/*` | Add language routing and editor contract coverage. |289290`apps/editor/vite.config.js` should not need a Monaco plugin. A config change is291justified only if production worker/base-path testing demonstrates a problem292that Vite's standard worker imports cannot solve.293294Add:295296- `monaco-editor` 0.56.x.297- An npm override to DOMPurify 3.4.14 or newer while Monaco pins an older298 vulnerable patch. Keep the override until Monaco's direct dependency is299 updated, and verify it with `npm audit` during upgrades.300301Remove after the mobile decision is accepted and the new backends pass tests:302303- `codemirror`304- `@codemirror/lang-json`305- `@codemirror/lang-sql`306- `@codemirror/lang-xml`307- `@codemirror/lang-yaml`308- `@codemirror/language`309- `@lezer/highlight`310311Run `npm install` rather than hand-editing `package-lock.json`.312313## Loading and performance requirements314315Monaco's npm package is about 98 MB unpacked, so aggregate imports can create a316large accidental payload. The implementation must:317318- Dynamically import the Monaco backend only after choosing it, keeping the319 mobile fallback free of Monaco code.320- Use ESM editor API and specific language contributions.321- Emit only the editor and JSON workers.322- Record raw and gzip sizes for entry JavaScript, Monaco chunks, CSS, and each323 worker before and after the change.324325Release thresholds:326327- No individual initial JavaScript resource larger than 1 MB gzip.328- No more than 1.5 MB gzip total JavaScript for opening the default desktop329 editor and then switching once to a strict JSON type, including both workers.330- No Monaco JavaScript or worker request on the textarea path.331- Typing, paste, type switching, and preview scheduling show no visible332 main-thread stall in the supported desktop matrix.333334These are guardrails, not targets. Report the actual delta in the implementation335PR so reviewers can decide whether the desktop feature gain justifies it.336337## Accessibility requirements338339- The editable control has the accessible name `Diagram source editor`.340- Screen-reader mode remains automatic.341- Keyboard-only users can reach the editor, edit, invoke find, save, and leave342 the editor. Document Monaco's Tab trapping shortcut in a discoverable help343 affordance if testing shows users cannot find it.344- Focus indicators remain visible in forced-colours/high-contrast modes.345- Browser zoom at 200% does not hide the editor or critical controls.346- The textarea fallback keeps the section label and visible focus treatment.347348## Testing plan349350### Unit tests351352Add backend-independent contract tests for both implementations where a DOM is353available, plus pure tests for language routing:354355- `getValue()` returns initial and edited source exactly, including Unicode and356 trailing newlines.357- One user edit produces one application change notification.358- `setDocument()` produces no application change notification.359- `setDocument()` changes both value and language.360- A thrown backend operation does not leave change notifications suppressed.361- Save callback fires for Command+S and Ctrl+S.362- All 30 diagram types resolve to a registered language.363- WaveDrom routes to JSON5, and bytefield routes to Clojure.364365Keep existing state, type-draft, persistence, encryption, and renderer tests366unchanged. Passing them is evidence that the editor boundary did not leak into367application state.368369### Build and asset tests370371- `npm run test:editor`372- `npm run build:editor`373- Inspect emitted HTML, chunks, and worker URLs under the production374 `/diagram.zip/` base.375- Assert the production assets contain no CodeMirror packages.376- Assert only the expected Monaco language contributions and workers are377 emitted.378- Serve the built output over HTTP; `file://` is not a valid worker test.379380### Browser acceptance matrix381382Desktop, supported path:383384- Current Chrome on macOS and Windows.385- Current Firefox on macOS or Windows.386- Current Safari on macOS.387- Current Edge on Windows.388389Mobile/fallback path:390391- Safari on a current iPhone.392- Chrome on a current Android phone.393- At least one iPad/tablet with touch input and, if available, a hardware394 keyboard.395396For every browser, verify:3973981. Open a new diagram and edit source.3992. Confirm one preview request after the debounce.4003. Switch among a generic, JSON, XML, YAML, SQL, JSON5, and bytefield type.4014. Switch back and confirm per-type draft restoration.4025. Save, reload, restore saved state, make a copy, and open a shared edit link.4036. Exercise undo/redo, find, select all, copy/paste, and Unicode input.4047. Change between Edit and Preview mobile tabs and rotate/resize the viewport.4058. Confirm no worker failure, uncaught exception, or unexpected main-thread406 fallback in the console.407408## Rollout409410Implement in reviewable stages on one branch:4114121. Add the source-editor contract and textarea backend without changing the413 default CodeMirror path.4142. Add Monaco, language registration, theme, and workers behind a local query415 switch.4163. Complete automated and real-device acceptance testing and capture bundle417 measurements.4184. Make backend selection default to Monaco desktop/textarea mobile.4195. Remove CodeMirror code and dependencies.4206. Keep `?editor=textarea` for one release as a support escape hatch, then421 decide whether to retain or remove it based on issue reports.422423No server, persistence API, encryption format, shared URL, or stored draft424migration is required. Source remains a string and editor choice is local UI425state.426427## Acceptance criteria428429The migration is complete when:430431- Every current diagram flow reads and writes identical source strings.432- Existing diagramzip tests pass without weakening assertions.433- New editor contract and language-routing tests pass.434- Production workers load under `/diagram.zip/` with no console fallback.435- Desktop keyboard, accessibility, and browser checks pass.436- The mobile fallback passes the phone/tablet matrix and product accepts its437 reduced feature set.438- Bundle measurements satisfy the guardrails and are recorded in the PR.439- CodeMirror runtime code, CSS, direct dependencies, and lockfile entries are440 gone.441- The implementation PR documents any deliberate difference from this spec.442443## Risks and mitigations444445| Risk | Impact | Mitigation |446| --- | --- | --- |447| Monaco is unsupported on mobile | Broken input or navigation for current mobile users | Route touch-only/mobile users to textarea; require real-device gate; retain CodeMirror if parity is mandatory. |448| Bundle growth | Slower first edit, especially on constrained networks | Dynamic desktop import, narrow ESM imports, two workers only, measured thresholds. |449| Worker URL/base-path error | Console warnings and main-thread work | Use Vite worker imports, test production base path over HTTP, fail release on fallback warning. |450| Programmatic changes look like user edits | Extra renders, dirty-state churn | Atomic `setDocument()` with counter-based notification suppression and tests. |451| JSON validation rejects JSON5 | False errors for WaveDrom | Separate `json5` language without strict JSON diagnostics. |452| Generic auto-closing changes ASCII source | Corrupted Ditaa/GoAT/Svgbob editing | Conservative language configuration and fixture-based manual checks. |453| Theme mismatch or clipped widgets | Visual regression | Resolve CSS tokens into a Monaco theme and inspect find/hover widgets at responsive sizes. |454| Accidental aggregate imports | Unnecessary languages and workers in production | Import only explicit ESM contributions and inspect build assets. |455456## Decision gates457458Before implementation is merged, reviewers must answer:4594601. Is a plain-text mobile editor acceptable? If no, this proposal is a no-go461 and CodeMirror should remain.4622. Does the measured desktop payload fit the product's performance posture?4633. Are JSON diagnostics desired for strict JSON formats, or should validation464 remain entirely renderer-driven? The default in this spec is syntax plus465 built-in JSON diagnostics, without external schemas.466467## References468469- [Monaco Editor repository and FAQ](https://github.com/microsoft/monaco-editor)470- [Official ESM and Vite integration guide](https://github.com/microsoft/monaco-editor/blob/main/docs/integrate-esm.md)471- [Monaco Editor API](https://microsoft.github.io/monaco-editor/typedoc/)472- [Monaco accessibility guide](https://github.com/microsoft/monaco-editor/wiki/Monaco-Editor-Accessibility-Guide)