Bundling without a bundler with esm.sh

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 de…


This content originally appeared on DEV Community and was authored by Bart Louwers

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.


This content originally appeared on DEV Community and was authored by Bart Louwers


Print Share Comment Cite Upload Translate Updates
APA

Bart Louwers | Sciencx (2025-01-05T13:31:51+00:00) Bundling without a bundler with esm.sh. Retrieved from https://www.scien.cx/2025/01/05/bundling-without-a-bundler-with-esm-sh/

MLA
" » Bundling without a bundler with esm.sh." Bart Louwers | Sciencx - Sunday January 5, 2025, https://www.scien.cx/2025/01/05/bundling-without-a-bundler-with-esm-sh/
HARVARD
Bart Louwers | Sciencx Sunday January 5, 2025 » Bundling without a bundler with esm.sh., viewed ,<https://www.scien.cx/2025/01/05/bundling-without-a-bundler-with-esm-sh/>
VANCOUVER
Bart Louwers | Sciencx - » Bundling without a bundler with esm.sh. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/01/05/bundling-without-a-bundler-with-esm-sh/
CHICAGO
" » Bundling without a bundler with esm.sh." Bart Louwers | Sciencx - Accessed . https://www.scien.cx/2025/01/05/bundling-without-a-bundler-with-esm-sh/
IEEE
" » Bundling without a bundler with esm.sh." Bart Louwers | Sciencx [Online]. Available: https://www.scien.cx/2025/01/05/bundling-without-a-bundler-with-esm-sh/. [Accessed: ]
rf:citation
» Bundling without a bundler with esm.sh | Bart Louwers | Sciencx | https://www.scien.cx/2025/01/05/bundling-without-a-bundler-with-esm-sh/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.