DEV Community

swyx
swyx

Posted on • Updated on

How To Add Monaco Editor to a Next.js app

Bottom Line Up Front

I use a slightly modified version of the steps mentioned in this GitHub comment. Modifications were necessary because I use TailwindCSS with Next.js.

Motivations

Monaco Editor is the open source editor used in VS Code, which itself is open source. I used to write my blogposts in VS Code, and as I make my own Dev.to CMS, I wanted to have all the familiar trappings of Monaco to help me out while I write.

Problems

However there are some issues we have to deal with:

  • Monaco is framework agnostic, so it requires writing some React bindings.
  • Monaco is written for a desktop Electron app, not for a server-side rendered web app.
    • This is solved by using import dynamic from "next/dynamic" and making Monaco a dynamic import.
  • Monaco also wants to offload syntax highlighting to web workers, and we need to figure that out
  • Next.js doesn't want any dependencies importing CSS from within node_modules, as this assumes a bundler and loader setup (e.g. webpack) and can have unintentional global CSS side effects (all global CSS is intended to be in _app.js).

We can solve this with a solution worked out by Elliot Hesp on GitHub and a config from Joe Haddad of the Next.js team.

Solution

The solution I use is informed by my usage of Tailwind CSS, which requires a recent version of PostCSS, which @zeit/next-css only has at 3.0 (because it is deprecated and not maintained).

I also use TypeScript, which introduces a small wrinkle, because Monaco Editor attaches a MonacoEnvironment global on the window object - I just @ts-ignore it.

// next.config.js

const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
const withTM = require("next-transpile-modules")([
  // `monaco-editor` isn't published to npm correctly: it includes both CSS
  // imports and non-Node friendly syntax, so it needs to be compiled.
  "monaco-editor"
]);

module.exports = withTM({
  webpack: config => {
    const rule = config.module.rules
      .find(rule => rule.oneOf)
      .oneOf.find(
        r =>
          // Find the global CSS loader
          r.issuer && r.issuer.include && r.issuer.include.includes("_app")
      );
    if (rule) {
      rule.issuer.include = [
        rule.issuer.include,
        // Allow `monaco-editor` to import global CSS:
        /[\\/]node_modules[\\/]monaco-editor[\\/]/
      ];
    }

    config.plugins.push(
      new MonacoWebpackPlugin({
        languages: [
          "json",
          "markdown",
          "css",
          "typescript",
          "javascript",
          "html",
          "graphql",
          "python",
          "scss",
          "yaml"
        ],
        filename: "static/[name].worker.js"
      })
    );
    return config;
  }
});
Enter fullscreen mode Exit fullscreen mode

and then in your Next.js app code:

import React from "react";
// etc

import dynamic from "next/dynamic";
const MonacoEditor = dynamic(import("react-monaco-editor"), { ssr: false });

function App() {
  const [postBody, setPostBody] = React.useState("");
  // etc
  return (<div>
  {/* etc */}
    <MonacoEditor
      editorDidMount={() => {
        // @ts-ignore
        window.MonacoEnvironment.getWorkerUrl = (
          _moduleId: string,
          label: string
        ) => {
          if (label === "json")
            return "_next/static/json.worker.js";
          if (label === "css")
            return "_next/static/css.worker.js";
          if (label === "html")
            return "_next/static/html.worker.js";
          if (
            label === "typescript" ||
            label === "javascript"
          )
            return "_next/static/ts.worker.js";
          return "_next/static/editor.worker.js";
        };
      }}
      width="800"
      height="600"
      language="markdown"
      theme="vs-dark"
      value={postBody}
      options={{
        minimap: {
          enabled: false
        }
      }}
      onChange={setPostBody}
    />
  </div>)
}
Enter fullscreen mode Exit fullscreen mode

Since I'm using Tailwind, I'm also using PostCSS, which also tries to eliminate Monaco's CSS. You have to tell it to ignore that:

// postcss.config.js
const purgecss = [
  "@fullhuman/postcss-purgecss",
  {
    // https://purgecss.com/configuration.html#options
    content: ["./components/**/*.tsx", "./pages/**/*.tsx"],
    css: [],
    whitelistPatternsChildren: [/monaco-editor/], // so it handles .monaco-editor .foo .bar
    defaultExtractor: content => content.match(/[\w-/.:]+(?<!:)/g) || []
  }
];
Enter fullscreen mode Exit fullscreen mode

Catch up on the Dev.to CMS LiveStream!

Discussion (14)

Collapse
mrahmadawais profile image
Ahmad Awais ⚡️

This is pretty fucking amazing. I've been meaning to work on something like this since all the syntax highlighting choices available are nowhere near as good as the Monaco editor. And I built the Shades of Purple theme because I like the right set of colors for the syntax — this will be a huge help for the new version of my site.

Thank you! 👌

Collapse
patarapolw profile image
Pacharapol Withayasakpunt

Note that Monaco is not mobile supported. That turns me off, and had to fall back to CodeMirror...

Collapse
swyx profile image
swyx Author

doesnt monaco use codemirror under the hood?

Thread Thread
patarapolw profile image
Pacharapol Withayasakpunt

I don't know. I just believe what it says in the website -- microsoft.github.io/monaco-editor/

Thread Thread
swyx profile image
swyx Author

ah. i've tried it briefly on mobile web sites. it can work. but maybe not as well as you might want.

Collapse
jaakkolantero profile image
Tero Jaakkola

This is fantastic. Took me 15 minutes to add to my own starter and get it running. Running pretty smoothly on localhost. Hosting to now breaks it a little bit.

github.com/jaakkolantero/monaco-st...

Big thanks!

Collapse
swyx profile image
swyx Author • Edited on

thanks! looks like the production build is missing this file github.com/microsoft/vscode/blob/a...

I figured it out! postcss is clearing monaco's overflow guard styling since it thinks it is unused.

// postcss.config.js
const purgecss = [
  "@fullhuman/postcss-purgecss",
  {
    // https://purgecss.com/configuration.html#options
    content: ["./components/**/*.tsx", "./pages/**/*.tsx"],
    css: [],
    whitelistPatternsChildren: [/monaco-editor/], // so it handles .monaco-editor .foo .bar
    defaultExtractor: content => content.match(/[\w-/.:]+(?<!:)/g) || []
  }
];
Collapse
timsuchanek profile image
Tim Suchanek

Thanks for the awesome article!
With Next 9.3 this works perfect.
However, with Next 9.5 this stopped working.
Anyone an idea why that could be?

Collapse
swyx profile image
swyx Author

no idea. project not active already. what errors do you get?

Collapse
timsuchanek profile image
Tim Suchanek

It turns out, the solution can be much simpler :D
This also works fine with the latest Next.js

github.com/vercel/next.js/tree/can...

Collapse
christopherhbutler profile image
Christopher Harold Butler

Do you know how to get JSX syntax highlighting working with react-monaco-editor and react? This has me stumped 🧐

Collapse
swyx profile image
swyx Author

i dont think i did anything special here. sorry.

Collapse
beingbook profile image
Han BaHwan

looks not work in Next.js 10...

Collapse
cashewnuts profile image
Tatsuya Kanemoto

I had the same problem...
I ended up just using @monaco-editor/react.

This works great without any hacky approach.