modern-monaco

repository·main·Indexed 23 days ago

https://github.com/esm-dev/modern-monaco

A modern wrapper around Monaco Editor (v0.4.2) that simplifies setup by removing manual web worker and CSS loader configuration. It features Shiki-powered syntax highlighting and supports three integration modes: Lazy Mode for reduced initial load via a <monaco-editor> custom element, SSR Mode for server-side rendering and hydration, and Manual Mode for full control via init(). It includes a Workspace for VSCode-like file management, built-in LSP support for HTML, CSS, JS/TS, and JSON, and a core sub-module to minimize bundle size.

Tokens
12.6K
Snippets
24
Records
80
Agent score
81%

What's inside modern-monaco

  1. How the three loading modes work

    main

    modern-monaco offers three distinct modes for integrating the editor depending on your requirements:

    1. Lazy Mode: Best for reducing initial load times. It uses Shiki to pre-highlight code while loading the heavy editor-core.js in the background. Requires a <monaco-editor> custom element in your HTML.
    2. SSR Mode: Best for SEO and perceived performance. It renders a mock editor on the server and hydrates it on the client side using renderToWebComponent and hydrate.
    3. Manual Mode: Best for full control. You manually initialize the core and create editor instances using init().
  2. Configure Language Server Protocol (LSP) features

    main

    modern-monaco provides full LSP support for HTML, CSS/SCSS/LESS, JavaScript/TypeScript, and JSON by default. You do not need to manually set MonacoEnvironment.getWorker as the required LSP workers are loaded automatically.

    LSP configurations (including formatting and language-specific settings) can be applied using the lsp property within the lazy, init, or hydrate functions.

    lazy({
      lsp: {
        // formatting options for all languages
        formatting: {/* ... */},
        // configure LSP for languages
        html: {/* ... */},
        css: {/* ... */},
        json: {/* ... */},
        typescript: {/* ... */},
      },
    });
  3. Use Manual Mode

    main

    Manual mode gives you direct access to the monaco-editor-core. You use init() to load the core, then use the returned object to create editors and models.

    <div id="editor"></div>
    
    <script type="module">
      import { init } from "modern-monaco";
    
      // load monaco-editor-core.js
      const monaco = await init();
    
      // create a Monaco editor instance
      const editor = monaco.editor.create(document.getElementById("editor"));
    
      // create and attach a model to the editor
      editor.setModel(monaco.editor.createModel(`console.log("Hello, world!")`, "javascript"));
    </script>
  4. Use Lazy Mode with Workspace

    main

    Lazy mode allows you to load the editor on demand. It is often used with a Workspace to manage files and editor models without interacting with native Monaco APIs directly. You must include the <monaco-editor> custom element in your HTML.

    <!-- index.html -->
    <monaco-editor></monaco-editor>
    <script src="app.js" type="module"></script>
    // app.js
    import { lazy, Workspace } from "modern-monaco";
    
    const workspace = new Workspace({
      initialFiles: {
        "index.html": `<html><body>...</body></html>`,
        "main.js": `console.log("Hello, world!")`,
      },
      entryFile: "index.html",
    });
    
    // initialize the editor lazily
    lazy({ workspace });
    
    // interact with the workspace
    await workspace.fs.writeFile("util.js", "export function add(a, b) { return a + b; }");
    workspace.openTextDocument("util.js");
  5. Use SSR Mode with hydration

    main

    SSR mode provides an instant pre-rendered editor on the server. On the client, you must call hydrate() to make the editor interactive.

    import { renderToWebComponent } from "modern-monaco/ssr";
    
    export default {
      async fetch(req) {
        const editorHTML = await renderToWebComponent(
          `console.log("Hello, world!")`,
          {
            language: "javascript",
            theme: "vitesse-dark",
            userAgent: req.headers.get("user-agent"),
          },
        );
        return new Response(
          /* html */ `
            ${editorHTML}
            <script type="module">
              import { hydrate } from "https://esm.sh/modern-monaco";
              // hydrate the editor
              hydrate();
            </script>
          `,
          { headers: { "Content-Type": "text/html" } },
        );
      },
    };
  6. Configure TypeScript Compiler Options

    main

    You can set TypeScript compiler options using two methods:

    1. Via Workspace: Add a tsconfig.json file to the initialFiles in your Workspace.
    2. Via LSP Config: Pass compilerOptions to the lsp.typescript.compilerOptions option in lazy, init, or hydrate functions.
    // Option 2: Via LSP Config
    lazy({
      lsp: {
        typescript: {
          compilerOptions: {
            target: "ES2022",
            strict: true,
          },
        },
      },
    });
  7. Load editor modules from a custom CDN

    main

    By default, modern-monaco loads modules from https://esm.sh. To use a different CDN, provide an import map in your HTML that maps the following modules to your preferred URLs:

    • modern-monaco
    • modern-monaco/editor-core
    • modern-monaco/lsp
    • typescript (if using LSP)
    <script type="importmap">
      {
        "imports": {
          "modern-monaco": "https://mycdn.com/modern-monaco@:version/dist/index.mjs",
          "modern-monaco/editor-core": "https://mycdn.com/modern-monaco@:version/dist/editor-core.mjs",
          "modern-monaco/lsp": "https://mycdn.com/modern-monaco@:version/dist/lsp/index.mjs",
          "typescript": "https://mycdn.com/typescript@:version/lib/typescript.js"
        }
      }
    </script>
  8. Configure TypeScript Import Maps

    main

    modern-monaco uses import maps to resolve bare specifier imports in JavaScript/TypeScript. You can configure these in two ways:

    1. Via Workspace: Include an <script type="importmap"> in your root index.html within the Workspace.
    2. Via LSP Config: Pass an importMap object directly to the lsp.typescript.importMap option in lazy, init, or hydrate functions.
    // Option 2: Via LSP Config
    lazy({
      lsp: {
        typescript: {
          importMap: {
            "react": "https://esm.sh/react@18",
            "react-dom/": "https://esm.sh/react-dom@18/",
          },
        },
      },
    });
  9. Configure Editor Themes

    main

    modern-monaco uses Shiki for syntax highlighting. You can set themes via the <monaco-editor> attribute, through initialization options (lazy, init, or hydrate), or via monaco.editor.setTheme().

    Supported theme inputs include:

    • Theme ID (string) from Shiki.
    • A JSON object (imported or local).
    • A URL to a JSON theme file.
    • A dynamic import function.
    • A hand-crafted theme object.
    <monaco-editor theme="vitesse-dark"></monaco-editor>
    // Using options in lazy mode
    lazy({
      defaultTheme: "one-dark-pro",
      themes: [
        "one-light",
        "https://example.com/themes/mytheme.json",
        () => import("tm-themes/one-light.json", { with: { type: "json" } }),
        {
          name: "mytheme",
          base: "vs-dark",
          colors: { /* ... */ },
          tokenColors: [ /* ... */ ],
        }
      ]
    });
  10. How Workspace history works

    main

    The workspace.history object manages navigation through previously opened files. It supports two modes:

    1. Browser History: Uses the browser's popstate and pushState APIs, allowing the user to use the browser's back/forward buttons to navigate files.
    2. LocalStorage History: Uses localStorage to persist a stack of visited file paths, independent of the browser's URL history.

    You can subscribe to history changes using onChange to update your UI when the user navigates.

  11. Pre-load Language Grammars

    main

    To avoid loading grammars on-demand, you can pre-load them using the langs option in lazy, init, or hydrate. This accepts language IDs, grammar objects, URLs, or dynamic imports.

    import markdown from "tm-grammars/markdown.json" with { type: "json" };
    
    lazy({
      langs: [
        "html",
        "javascript",
        markdown,
        "https://example.com/grammars/mylang.json",
        () => import("tm-grammars/markdown.json", { with: { type: "json" } }),
        {
          name: "mylang",
          scopeName: "source.mylang",
          patterns: [ /* ... */ ],
        },
      ],
      cdn: "https://esm.sh",
    });