In my previous post I shared some tips how you can avoid complicated developer tooling in modern web projects. I shared how you can import packages directly from the browser with esm.sh.
As you accumulate dependencies, and especially as you take on dependencies which themselves have dependencies (which are called called transitive dependencies), you may find that your initial load time suffers. Sure, once the page loaded everything is neatly cached. But your browser needs to load a lot of different files (as your network tab in the developer tools will tell you) and once it's loaded those files, it needs to load yet another bunch of files.
Of course, this is the whole reason why bundlers exist! So the conclusion is that at some point, you need a bundler. Well, maybe. But you don't need to run that bundler yourself. esm.sh has an experimental feature that will actually create a bundle for you with whatever packages you specify. Here is how I use it.
Say we need the following packages for an editor we are building.
import ts from typescript;
import { tsSync, tsFacet, tsLinter, tsAutocomplete } from "@valtown/codemirror-ts";
import { basicSetup, EditorView } from "codemirror";
import { javascript } from "@codemirror/lang-javascript";
import { acceptCompletion, autocompletion } from "@codemirror/autocomplete";
import { Compartment, StateEffect } from "@codemirror/state";
import { oneDark } from "@codemirror/theme-one-dark";
import { indentWithTab } from "@codemirror/commands";
import { keymap, ViewPlugin } from "@codemirror/view";
import {
createDefaultMapFromCDN,
createSystem,
createVirtualTypeScriptEnvironment,
} from "@typescript/vfs";
Using an importmap
Now you could just add these to your importmap in index.html
.
<script type="importmap">
{
"imports": {
"typescript": "https://esm.sh/typescript",
"@valtown/codemirror-ts": "https://esm.sh/*@valtown/codemirror-ts",
"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>
The *
marks all dependencies as external, which is another feature of esm.sh. I also had to add all dependencies of codemirror manually. I found out that this is required, because different codemirror packages have slightly different versions of translative dependencies, and it will import different versions of those transitive dependencies causing conflicts.
This approach works, but as described in the introduction, initial load time will suffer because the browser needs to download a lot of files and it doesn't know ahead of time which files it should download.
Letting esm.sh compile a bundle
You can use this approach to let esm.sh create a bundle without using a bundler yourself. I will also explain how you can get types to work.
First of all I created a file /deps/editor.deps.js
:
import build from "https://esm.sh/build";
const ret = await build({
dependencies: {
"codemirror": "^6.0.1",
"@valtown/codemirror-ts": "^2.3.1",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/autocomplete": "^6.18.4",
"@codemirror/state": "^6.5.0",
"@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/commands": "^6.7.1"
},
source: `
import ts from "typescript";
import { tsSync, tsFacet, tsLinter, tsAutocomplete } from "@valtown/codemirror-ts";
import { basicSetup, EditorView } from "codemirror";
import { javascript } from "@codemirror/lang-javascript";
import { acceptCompletion, autocompletion } from "@codemirror/autocomplete";
import { Compartment, StateEffect } from "@codemirror/state";
import { oneDark } from "@codemirror/theme-one-dark";
import { indentWithTab } from "@codemirror/commands";
import { keymap, ViewPlugin } from "@codemirror/view";
import {
createDefaultMapFromCDN,
createSystem,
createVirtualTypeScriptEnvironment,
} from "@typescript/vfs";
export {
ts,
tsSync,
tsFacet,
tsLinter,
tsAutocomplete,
basicSetup,
EditorView,
javascript,
acceptCompletion,
autocompletion,
Compartment,
StateEffect,
oneDark,
indentWithTab,
keymap,
ViewPlugin,
createDefaultMapFromCDN,
createSystem,
createVirtualTypeScriptEnvironment,
};`
});
const {
ts,
tsSync,
tsFacet,
tsLinter,
tsAutocomplete,
basicSetup,
EditorView,
javascript,
acceptCompletion,
autocompletion,
Compartment,
StateEffect,
oneDark,
indentWithTab,
keymap,
ViewPlugin,
createDefaultMapFromCDN,
createSystem,
createVirtualTypeScriptEnvironment,
} = await import(ret.bundleUrl);
console.log({ret});
export {
ts,
tsSync,
tsFacet,
tsLinter,
tsAutocomplete,
basicSetup,
EditorView,
javascript,
acceptCompletion,
autocompletion,
Compartment,
StateEffect,
oneDark,
indentWithTab,
keymap,
ViewPlugin,
createDefaultMapFromCDN,
createSystem,
createVirtualTypeScriptEnvironment,
};
There is quite a bit of repetition here. And you'll need to update the imports in multiple places if you want to change them. This is admittedly quite a hassle. I let you decide if it's worth it.
In any case, you will see that when you import this file ret
will be printed to the console. Here is what is printed:
{
ret: {
bundleUrl: "https://esm.sh/~e4d1ab3ba39fc16e6de014e6f19bd819605fdd95?bundle",
id: "e4d1ab3ba39fc16e6de014e6f19bd819605fdd95",
url: "https://esm.sh/~e4d1ab3ba39fc16e6de014e6f19bd819605fdd95"
}
}
The bundleUrl
is the URL which contains the bundle esm.sh created for us! We import it with a dynamic import()
and then re-export it.
So you can simply import everything from /deps/editor.deps.js
.
import { ts } from "/deps/editor.deps.js";
And you are done!
If you want imports like
import { basicSetup, EditorView } from "codemirror";
to work we can update our importmap as follows:
<script type="importmap">
{
"imports": {
"codemirror": "/deps/editor.deps.js"
}
}
</script>
This doesn't work for default exports (like with the typescript
package). For this we can create a deps/editor.deps.d.ts
file to get types to work:
import ts from "typescript";
export {
ts,
};
And there you have it! Bundling without a bundler. We may call
it bundlerless, since just like with serverless there's still a server/bundler involved, just not one you need to deal with.
Top comments (0)