Skip to content

Instantly share code, notes, and snippets.

@Potherca
Last active April 13, 2024 08:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Potherca/028514c75f581db115797ecb50c6f945 to your computer and use it in GitHub Desktop.
Save Potherca/028514c75f581db115797ecb50c6f945 to your computer and use it in GitHub Desktop.
Loading CodeMirror through a CDN

Various CDNs offer support for bundling and/or pre-ackaging NPM modules.

This gist is an attempt to demonstrate each of these CDNs and CodeMirror.

Status Summary

CDN Status Live Example
esm.sh ✔️ Works! esmsh.html
JSDelivr ✔️ Works! jsdelivr.html
Skypack ❌ Does not work skypack.html
UNPKG ❌ Does not work unpkg.html

The Test

The test is simple: load CodeMirror from a CDN and see if it works.

The test is performed by loading the following HTML file from each CDN:

<main></main>
<script type="module">
  import {basicSetup, EditorView} from 'https://${cdn}/codemirror@6.0.1'
  import {javascript} from 'https://${cdn}/@codemirror/lang-javascript@6.1.7'

  let editor = new EditorView({
    doc: document.querySelector('script:not([src])').innerText,
    extensions: [basicSetup, javascript()],
    parent: document.querySelector('main'),
  });
</script>

And the expected result is:

Screenshot of expected result

That is to say, the CodeMirror editor should load and display the JavaScript code, with syntax highlighting. The actual URL might differ slightly between CDNs.

Issues

For CDN versions of CodeMirror to work, some things have to go right.

  1. The CDN has to be able to bundle/compile NPM modules.
  2. The CND has to serve the module with the correct MIME type.
  3. The CDN has to bundle/compile the CodeMirror module correctly.

Not all CDNs support all of these requirements by default. Several CDNs do provide parameters to help resolve these issues. Still, some CDNs do not appear to resolve all issues.

Bundle NPM modules

The first issue is that the CDN has to be able to bundle NPM modules.

If basic bundling is not provided, there is no need to continue, as the code must be bundled before it can be used in the browser.

Bundle correctly

But just bundling is not enough. The bundling has to be done correctly.

Various things can go wrong, but the most common issue (mentioned in the CodeMirror forum by the author of CodeMirror) is that:

A lot of stuff in CodeMirror doesn’t work when loading through a CDN, because those tend to load multiple versions of packages that are dependended on from multiple other packages.

This issue will trigger an error from CodeMirror itself:

Unrecognized extension value in extension set. This sometimes happens because multiple instances of @codemirror/state are loaded, breaking instanceof checks.

(see the related source code).

Serve the correct MIME type

The specification that defines how scripts should be processed states that the content-type header has to be one of the JavaScript MIME types.

So, for instance, if the script is served as Common JS, the MIME type will be application/node, and the script will not be loaded by the browser but trigger an error:

Screenshot of the error

Results

esm.sh

No issues, no surprises.

Using https://esm.sh/codemirror@6.0.1 and https://esm.sh/@codemirror/lang-javascript@6.1.7 as URLs just worked.

Looking at what is loaded, we see that the bundle contains contain multiple versions of the same module. However, the bundle is loaded correctly.

codemirror@6.0.1/codemirror.mjs
@codemirror/autocomplete@6.6.0/es2022/autocomplete.mjs
@codemirror/commands@6.2.3/es2022/commands.mjs
@codemirror/lang-javascript@6.1.7/es2022/lang-javascript.mjs
@codemirror/language@6.6.0/es2022/language.mjs
@codemirror/lint@6.2.1/es2022/lint.mjs
@codemirror/search@6.4.0/es2022/search.mjs
@codemirror/state@6.2.0/es2022/state.mjs
@codemirror/view@6.10.0/view.mjs
@codemirror/view@6.10.1/es2022/view.mjs
@lezer/common@1.0.2/es2022/common.mjs
@lezer/highlight@1.1.4/es2022/highlight.mjs
@lezer/javascript@1.4.3/es2022/javascript.mjs
@lezer/lr@1.3.4/es2022/lr.mjs
crelt@1.0.5/es2022/crelt.mjs
style-mod@4.0.3/es2022/style-mod.mjs
w3c-keyname@2.2.6/es2022/w3c-keyname.mjs

JSDelivr

Just using https://cdn.jsdelivr.net/npm/codemirror@6.0.1 and https://cdn.jsdelivr.net/npm/@codemirror/lang-javascript@6.1.7 loads both files as .cjs files.

The issue with this is that content-type is application/node, triggering the strict MIME type checking error.

This can be resolved by adding ?module to the URL which will load the files as ES Modules, with the correct MIME type.

At this point, the CodeMirror editor is loaded but syntax highlighting does not work.

But the "multiple instances" error is not triggered.

So what is going on?

When compare what is being loaded with what is loaded from esm.sh, we see that the bundle loads some extra files:

  • @codemirror/view@6.9.2/dist/index.js
  • @codemirror/view@6.9.4/dist/index.js
  • @codemirror/autocomplete@6.5.1/dist/index.js
codemirror@6.0.1/dist/index.js
@codemirror/autocomplete@6.5.1/dist/index.js
@codemirror/autocomplete@6.6.1/dist/index.js
@codemirror/commands@6.2.2/dist/index.js
@codemirror/lang-javascript@6.1.7/dist/index.js
@codemirror/language@6.6.0/dist/index.js
@codemirror/lint@6.2.1/dist/index.js
@codemirror/search@6.3.0/dist/index.js
@codemirror/state@6.2.0/dist/index.js
@codemirror/view@6.10.1/dist/index.js
@codemirror/view@6.11.0/dist/index.js
@codemirror/view@6.9.2/dist/index.js
@codemirror/view@6.9.4/dist/index.js
@lezer/common@1.0.2/dist/index.js
@lezer/highlight@1.1.4/dist/index.js
@lezer/javascript@1.4.3/dist/index.es.js
@lezer/lr@1.3.4/dist/index.js
crelt@1.0.5/index.es.js
style-mod@4.0.3/src/style-mod.js
w3c-keyname@2.2.6/index.es.js

There are several things that could be tried to make the codemirror package work, but none of these work.

By loading all the packages directly, I managed to figure out that, with @codemirror/view pinned to the right version, the editor works.

Skypack

Just including https://cdn.skypack.dev/codemirror@v6.0.1 and https://cdn.skypack.dev/@codemirror/lang-javascript@v6.1.7 does not work. It triggers the "multiple instances" error, which isn't surprising if we look at everything that is loaded:

codemirror@v6.0.1
@codemirror/autocomplete@v6.0.2
@codemirror/autocomplete@v6.5.1
@codemirror/commands@v6.0.1
@codemirror/lang-javascript@v6.1.7
@codemirror/lang-javascript@v6.1.7
@codemirror/language@v6.0.0
@codemirror/language@v6.2.0
@codemirror/language@v6.6.0
@codemirror/lint@v6.0.0
@codemirror/search@v6.0.0
@codemirror/state@v6.0.0
@codemirror/state@v6.0.1
@codemirror/state@v6.1.0
@codemirror/state@v6.2.0
@codemirror/view@v6.0.0
@codemirror/view@v6.0.2
@codemirror/view@v6.8.1
@codemirror/view@v6.9.4
@codemirror/view@v6.9.5
@lezer/common@v1.0.0
@lezer/common@v1.0.2
@lezer/highlight@v1.0.0
@lezer/highlight@v1.1.3
@lezer/highlight@v1.1.4
@lezer/javascript@v1.4.2
@lezer/lr@v1.3.3
crelt@v1.0.5
style-mod@v4.0.0
style-mod@v4.0.2
style-mod@v4.0.3
w3c-keyname@v2.2.4
w3c-keyname@v2.2.6

As there don't seem to be other options available, this is as far as it goes.

UNPKG

Simply loading https://unpkg.com/codemirror@6.0.1 and https://unpkg.com/@codemirror/lang-javascript@6.1.7 does not work.

Not only are *.cjs files loaded but for some reason the content-type is set to text/plain.

Screenshot of UNPKG

Using the ?module query parameter loads the *.js files, but triggers the "multiple instances" error.

And by "multiple" it means "a lot":

codemirror@6.0.1/dist/index.js
@codemirror/autocomplete@%5E6.0.0
@codemirror/autocomplete@6.6.1
@codemirror/autocomplete@6.6.1/dist/index.js
@codemirror/commands@%5E6.0.0
@codemirror/commands@6.2.4
@codemirror/commands@6.2.4/dist/index.js
@codemirror/lang-javascript@6.1.7/dist/index.js
@codemirror/language@%5E6.0.0
@codemirror/language@%5E6.6.0
@codemirror/language@6.6.0
@codemirror/language@6.6.0
@codemirror/language@6.6.0/dist/index.js
@codemirror/language@6.6.0/dist/index.js
@codemirror/lint@%5E6.0.0
@codemirror/lint@6.2.1
@codemirror/lint@6.2.1/dist/index.js
@codemirror/search@%5E6.0.0
@codemirror/search@6.4.0
@codemirror/search@6.4.0/dist/index.js
@codemirror/state@%5E6.0.0
@codemirror/state@%5E6.1.4
@codemirror/state@%5E6.2.0
@codemirror/state@6.2.0
@codemirror/state@6.2.0
@codemirror/state@6.2.0
@codemirror/state@6.2.0/dist/index.js
@codemirror/state@6.2.0/dist/index.js
@codemirror/state@6.2.0/dist/index.js
@codemirror/view@%5E6.0.0
@codemirror/view@%5E6.6.0
@codemirror/view@6.11.1
@codemirror/view@6.11.1
@codemirror/view@6.11.1/dist/index.js
@codemirror/view@6.11.1/dist/index.js
@lezer/common@%5E1.0.0
@lezer/common@1.0.2
@lezer/common@1.0.2/dist/index.js
@lezer/highlight@%5E1.0.0
@lezer/highlight@%5E1.1.3
@lezer/highlight@1.1.4
@lezer/highlight@1.1.4
@lezer/highlight@1.1.4/dist/index.js
@lezer/highlight@1.1.4/dist/index.js
@lezer/javascript@%5E1.0.0
@lezer/javascript@1.4.3
@lezer/javascript@1.4.3/dist/index.es.js
@lezer/lr@%5E1.3.0
@lezer/lr@1.3.4
@lezer/lr@1.3.4/dist/index.js
crelt@%5E1.0.5
crelt@1.0.5
crelt@1.0.5/index.es.js
style-mod@%5E4.0.0
style-mod@4.0.3
style-mod@4.0.3/src/style-mod.js
w3c-keyname@%5E2.2.4
w3c-keyname@2.2.6
w3c-keyname@2.2.6/index.es.js

Trying to joad the *.cjs files combined with the ?module query parameter is not supported:

module mode is available only for JavaScript and HTML files
<!doctype html>
<meta charset="UTF-8">
<title>ESM CodeMirror Example</title>
<link rel="icon" href="https://favicon.potherca.workers.dev/34" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<header class="full">
<h1>Simple CodeMirror Example </h1>
<h2>ESM</h2>
</header>
<main></main>
<script type="module">
import {basicSetup, EditorView} from 'https://esm.sh/codemirror@6.0.1'
import {javascript} from 'https://esm.sh/@codemirror/lang-javascript@6.1.7'
let editor = new EditorView({
doc: document.querySelector('script:not([src])').innerText.trim(),
extensions: [basicSetup, javascript()],
parent: document.querySelector('main'),
});
</script>
<!doctype html>
<meta charset="UTF-8">
<title>JSDelivr CodeMirror Example</title>
<link rel="icon" href="https://favicon.potherca.workers.dev/34" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<header class="full">
<h1>Simple CodeMirror Example </h1>
<h2>JSDelivr</h2>
</header>
<main></main>
<script type="module">
import {
crosshairCursor,
drawSelection,
dropCursor,
EditorView,
highlightActiveLine,
highlightActiveLineGutter,
highlightSpecialChars,
keymap,
lineNumbers,
rectangularSelection,
} from 'https://cdn.jsdelivr.net/npm/@codemirror/view@6.10.1/+esm';
import { EditorState } from 'https://cdn.jsdelivr.net/npm/@codemirror/state@6.2.0/+esm';
import { foldGutter, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldKeymap } from 'https://cdn.jsdelivr.net/npm/@codemirror/language@6.6.0/+esm';
import { history, defaultKeymap, historyKeymap } from 'https://cdn.jsdelivr.net/npm/@codemirror/commands/+esm';
import { highlightSelectionMatches, searchKeymap } from 'https://cdn.jsdelivr.net/npm/@codemirror/search/+esm';
import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap } from 'https://cdn.jsdelivr.net/npm/@codemirror/autocomplete/+esm';
import { lintKeymap } from 'https://cdn.jsdelivr.net/npm/@codemirror/lint/+esm';
import {javascript} from 'https://cdn.jsdelivr.net/npm/@codemirror/lang-javascript/+esm'
const basicSetup = [
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter(),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
crosshairCursor(),
highlightActiveLine(),
highlightSelectionMatches(),
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap
])
];
let editor = new EditorView({
doc: document.querySelector('script:not([src])').innerText.trim(),
extensions: [basicSetup, javascript()],
parent: document.querySelector('main'),
});
</script>
<!doctype html>
<meta charset="UTF-8">
<title>JSDelivr CodeMirror Example</title>
<link rel="icon" href="https://favicon.potherca.workers.dev/34" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<header class="full">
<h1>Simple CodeMirror Example </h1>
<h2>JSDelivr</h2>
</header>
<main></main>
<script type="module">
import {EditorView} from 'https://cdn.jsdelivr.net/npm/@codemirror/view@6.10.1/+esm';
import {basicSetup} from 'https://cdn.jsdelivr.net/npm/codemirror@6.0.1/+esm'
import {javascript} from 'https://cdn.jsdelivr.net/npm/@codemirror/lang-javascript@6.1.7/+esm'
let editor = new EditorView({
doc: document.querySelector('script:not([src])').innerText.trim(),
extensions: [basicSetup, javascript()],
parent: document.querySelector('main'),
});
</script>
<script type="text/plain">
/**
* Uncaught TypeError: Cannot read properties of null (reading 'lineNumbers')
*/
import {basicSetup, EditorView} from 'https://cdn.jsdelivr.net/npm/codemirror@6.0.1/dist/index.cjs/+esm'
import {javascript} from 'https://cdn.jsdelivr.net/npm/@codemirror/lang-javascript@6.1.7/dist/index.cjs/+esm'
/**
* Uncaught SyntaxError: Identifier 'e' has already been declared
*/
import {basicSetup, EditorView, javascript} from 'https://cdn.jsdelivr.net/combine/npm/codemirror@6.0.1/dist/index.js/+esm,npm/@codemirror/lang-javascript@6.1.7/dist/index.js/+esm'
/* same as */
import {basicSetup, EditorView, javascript} from 'https://cdn.jsdelivr.net/combine/npm/codemirror@6.0.1/dist/index.cjs/+esm,npm/@codemirror/lang-javascript@6.1.7/dist/index.cjs/+esm'
/**
* Uncaught TypeError: Failed to resolve module specifier "@codemirror/view".
* Relative references must start with either "/", "./", or "../".
*/
import {basicSetup, EditorView, javascript} from 'https://cdn.jsdelivr.net/combine/npm/codemirror@6.0.1/dist/index.js,npm/@codemirror/lang-javascript@6.1.7/dist/index.js'
/**
* This loads the editor but not syntax highlighting:
*/
import {basicSetup, EditorView} from 'https://cdn.jsdelivr.net/npm/codemirror@6.0.1/+esm'
import {javascript} from 'https://cdn.jsdelivr.net/npm/@codemirror/lang-javascript@6.1.7/+esm'
/* This is the same as: */
import {basicSetup, EditorView} from 'https://cdn.jsdelivr.net/npm/codemirror@6.0.1/dist/index.js/+esm'
import {javascript} from 'https://cdn.jsdelivr.net/npm/@codemirror/lang-javascript@6.1.7/dist/index.js/+esm'
/* This is the same as: */
import {basicSetup, EditorView} from 'https://esm.run/codemirror@6.0.1'
import {javascript} from 'https://esm.run//@codemirror/lang-javascript@6.1.7'
/**
* Failed to load module script: Expected a JavaScript module script but the
* server responded with a MIME type of "application/node". Strict MIME type
* checking is enforced for module scripts per HTML spec.
*/
import {basicSetup, EditorView} from 'https://cdn.jsdelivr.net/npm/codemirror@6.0.1'
import {javascript} from 'https://cdn.jsdelivr.net/npm/@codemirror/lang-javascript@6.1.7'
</script>
<!doctype html>
<meta charset="UTF-8">
<title>SkyPack CodeMirror Example</title>
<link rel="icon" href="https://favicon.potherca.workers.dev/34" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<header class="full">
<h1>Simple CodeMirror Example </h1>
<h2>SkyPack</h2>
</header>
<main></main>
<script type="module">
import {basicSetup, EditorView} from 'https://cdn.skypack.dev/codemirror@v6.0.1'
import {javascript} from 'https://cdn.skypack.dev/@codemirror/lang-javascript@v6.1.7'
let editor = new EditorView({
doc: document.querySelector('script:not([src])').innerText.trim(),
extensions: [basicSetup, javascript()],
parent: document.querySelector('main'),
});
</script>
<!doctype html>
<meta charset="UTF-8">
<title>UNPKG CodeMirror Example</title>
<link rel="icon" href="https://favicon.potherca.workers.dev/34" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<header class="full">
<h1>Simple CodeMirror Example </h1>
<h2>UNPKG</h2>
</header>
<main></main>
<script type="module">
import {basicSetup, EditorView} from 'https://unpkg.com/codemirror@6.0.1?module';
import {javascript} from 'https://unpkg.com/@codemirror/lang-javascript@6.1.7?module'
let editor = new EditorView({
doc: document.querySelector('script:not([src])').innerText.trim(),
extensions: [basicSetup, javascript()],
parent: document.querySelector('main'),
});
</script>
@cben
Copy link

cben commented Jun 13, 2023

I'm getting it to more-or-less work with UNPKG + importmap.
EDIT: CAVEATS:

  • This means no bundling! First-load performance will suffer (HTTP 2/3 + caching mitigate this somewhat, esp. on later load).
  • Unversioned URLs for lots of separate packages increases chance of incompatible versions?
  • Unversioned URLs also impact caching — must hit unpkg.com to get redirect to latest version before browser knows it can reuse cache of specific version => later loads still sensitive to round-trip time :-(
    I suppose the Right Way to use large importmaps is generate them from npm/yarn resolving versions? But that's getting not fun, all I wanted is to avoid bundlers + package managers...

Without the importmap, it needed the ?module so that cross-package imports resolve, but apparently there were duplicates, as the browser didn't realize https://unpkg.com/@codemirror/state that my code is importing was same as @codemirror/state that other CodeMirror modules are importing (and at least once i saw a CodeMirror console error suggesting state must be imported only once)...

With importmap, I can drop ?module conversion, but need sup-paths to get ESM sources — as default path unpkg picks tends to give CJS versions. The sub-paths are nearly uniform dist/index.js (except for lezer/javascript).

<script type="importmap">
  {
    "imports": {
      "@codemirror/basic-setup": "https://unpkg.com/@codemirror/basic-setup/dist/index.js",
      "@codemirror/autocomplete": "https://unpkg.com/@codemirror/autocomplete/dist/index.js",
      "@codemirror/commands": "https://unpkg.com/@codemirror/commands/dist/index.js",
      "@codemirror/language": "https://unpkg.com/@codemirror/language/dist/index.js",
      "@codemirror/lang-javascript": "https://unpkg.com/@codemirror/lang-javascript/dist/index.js",
      "@codemirror/lint": "https://unpkg.com/@codemirror/lint/dist/index.js",
      "@codemirror/search": "https://unpkg.com/@codemirror/search/dist/index.js",
      "@codemirror/state": "https://unpkg.com/@codemirror/state/dist/index.js",
      "@codemirror/view": "https://unpkg.com/@codemirror/view/dist/index.js",
      "@lezer/common": "https://unpkg.com/@lezer/common/dist/index.js",
      "@lezer/highlight": "https://unpkg.com/@lezer/highlight/dist/index.js",
      "@lezer/javascript": "https://unpkg.com/@lezer/javascript/dist/index.es.js",
      "@lezer/lr": "https://unpkg.com/@lezer/lr/dist/index.js",
      "style-mod": "https://unpkg.com/style-mod/src/style-mod.js",
      "w3c-keyname": "https://unpkg.com/w3c-keyname/index.js",
      "crelt": "https://unpkg.com/crelt/index.js"
    }
  }
</script>

<script type="module">
  import { EditorState, basicSetup } from '@codemirror/basic-setup';
  import { EditorView, keymap } from '@codemirror/view';
  import { defaultKeymap, indentWithTab } from '@codemirror/commands';
  import { javascript } from '@codemirror/lang-javascript';
  
  ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment