ESM-compatible CodeMirror build (directly importable in browser)

I was a bit disappointed by the lack of CDN support (auto-building ones such as esm.run fail due to the instanceof issue), so I made a build that runs directly in the browser:

It’s not thoroughly battle-tested yet, but seems to be working fine. Here’s a minimal example of how to use it via deno.land/x, modified from the example at https://codemirror.net/try:

<div id="html-input"></div>

<script type="importmap">
	{
		"imports": {
			"codemirror/": "https://deno.land/x/codemirror_esm@v6.0.1/esm/"
		}
	}
</script>
<script async type="module">
	import { basicSetup, EditorView } from "codemirror/codemirror/dist/index.js"
	import { javascript } from "codemirror/lang-javascript/dist/index.js"

	new EditorView({
		doc: "console.log('hello')\n",
		extensions: [basicSetup, javascript()],
		parent: document.body,
	})
</script>

By far the simplest way to use CodeMirror for those who don’t want to faff around with build tools and NPM. Please open issues on the repo if you find bugs!

4 Likes

Nice! What I do for local testing is use a server that rewrites import paths (my own esmoduleserve, but there’s probably more mature stuff out there by now) to get a similar effect.

1 Like

This piggybacks on top of that solution by running the dev server with esmoduleserve, importing all the modules, then recursively fetching all the JS files, writing them to the esm subdirectory in the same folder structure, and rewriting the absolute paths to relative ones (as CDNs typically don’t serve content from the root URL). It’s a little hacky to be sure, but I figured it’d be quicker to get working this way than try to learn Vite or something… maybe Vite’s super easy to learn, I don’t know, but I’m jaded from too many hundreds of hours spent trying to get webparcelpackbabelsnowscriptrollupjs to spit out code that actually runs.

1 Like

Thank you for this! I was just looking for a CDN-based version and this worked. I wish this was part of the docs, which don’t mention CDNs at all: site:https://codemirror.net/docs/ cdn - Google Zoeken

This is awesome, thank you — I was trying to move from codemirror 5 and the requirement of using a bundler was a dealbreaker (I’ve also spent too much time dealing with js tooling dependency hell; vanilla or bust for me)

Do you know if there’s a trick to get this to work in Safari? The glitch demo gives me this error in the console:

TypeError: Module specifier, '@codemirror/codemirror/dist/index.js' does not start with "/", "./", or "../". Referenced from https://codemirror-esm-demo.glitch.me/

Looks like Safari ≤16.3 doesn’t support import maps (but the upcoming 16.4 will). I don’t have a device running Safari to check on, but should be fixable by replacing all the import statements with fully-qualified URLs.

1 Like

For what it’s worth, esm.sh works without any special configuration for me.

Any chance you could share a minimal code snippet? I tried to get this working before and it completely failed to load, maybe because https://esm.sh/codemirror redirects to https://esm.sh/codemirror@6.65.7, which seems to be a nonexistent version…? Meanwhile, if I specify v6.0.1, the best I can manage is this:

import { basicSetup, EditorView } from "https://esm.sh/codemirror@v6.0.1"
import { javascript } from "https://esm.sh/@codemirror/lang-javascript@v6.0.1"

new EditorView({
	doc: "console.log('hello')\n",
	extensions: [basicSetup, javascript()],
	parent: document.body,
})

That loads the editor without crashing, but silently fails to load any JS language features.

@lionel-rowe That code actually seems to work now (i.e. syntax highlighting works), so maybe esm.sh has pushed an update which fixed the issue. Or maybe you meant other JS language features.

But then when I add the one-dark theme, it changes the text color to weird bright ones, and doesn’t make the background dark:

Does anyone know what the fundamental blocker here is to making this “just work”, regardless of the CDN? If I change the above example to import from jsdelivr, the syntax highlighting silently fails.

@marijn is this something that you care about - i.e. users being able to just import things via URL?

Yes, looks like that particular issue is now fixed on esm.sh :tada:

It looks like those “weird bright colors” are the correct one-dark syntax highlighting colors, it’s just lacking the correct styles for background/gutter. Not sure why, and there don’t seem to be any errors in browser console (so presumably unrelated to the instanceof issue).

FWIW, importing oneDark via my custom ESM version does indeed “just work” — I’ve updated the codemirror-esm-demo glitch.me project in OP to include it. But I agree it’d be more ideal if there was official support for this.

Depends on what “just” importing things via URL means. You can, if you set up an import map, import by URL from a properly installed node_modules directory. I run my entire test/dev setup for the project with URL imports (using an import-rewriting webserver because import maps weren’t supported yet when I set this up). But I don’t really intend to make it my problem what CDNs do when a package appears multiple times in the dependency tree.

The current state of not having an easier way to import CodeMirror 5 from an URL prevents me from using it. I want to import the latest version of CodeMirror without having to bundle it myself. Or even a guide on how to do it myself would be nice.

@marijn I know that you don’t care but it really is a dealbreaker for some people, which is sad imho.

The issue is that @codemirror/view and @codemirror/lang-javascript depend on different versions of @codemirror/state.

This is not a problem when using a bundler, but using esm.sh will cause both versions to be loaded which leads to a conflict. The codemirror package is especially problematic because it has three dependencies which all depend on different versions of @codemirror/state.

You can now specify dependencies with esm.sh:

import { basicSetup, EditorView } from "https://esm.sh/codemirror@6.0.1?deps=@codemirror/state@6.4.1";
import {javascript} from "https://esm.sh/@codemirror/lang-javascript?deps=@codemirror/state@6.4.1"

But this doesn’t work for transitive dependencies.

HOWEVER, you can also specify dependencies as external:

import { basicSetup, EditorView } from "https://esm.sh/codemirror@6.0.1?external=@codemirror/state";
import {javascript} from "https://esm.sh/@codemirror/lang-javascript?external=@codemirror/state"

Do that, and pin @codemirror/state to an exact version.

    <script type="importmap">
      {
        "imports": {
          "@codemirror/state": "https://esm.sh/@codemirror/state@6.5.0"
        }
      }
    </script>

That works great.

Edit: still works not so great.

Automatic indentation fails. When I add the no-bundle parameter, that works, but syntax highlighting is still broken.


Edit2: Finally got it to work.

<script type="importmap">
  {
    "imports": {
      "style-mod": "https://esm.sh/style-mod",
      "w3c-keyname": "https://esm.sh/w3c-keyname",
      "crelt": "https://esm.sh/crelt",
      "@marijn/find-cluster-break": "https://esm.sh/@marijn/find-cluster-break",
      "@lezer/": "https://esm.sh/*@lezer/",
      "@codemirror/": "https://esm.sh/*@codemirror/",
      "codemirror": "https://esm.sh/*codemirror"
    }
  }
</script>

Then simply import as:

import { basicSetup, EditorView } from "codemirror";
import { javascript } from "@codemirror/lang-javascript"

This will ensure that everything uses the same versions. Of course, you’ll probably want to pin top-level dependencies to a specific version.