Skip to content

Instantly share code, notes, and snippets.

@getify
Last active March 23, 2024 16:16
Show Gist options
  • Save getify/ea3c048c26e95f8797448eac8a82ce1a to your computer and use it in GitHub Desktop.
Save getify/ea3c048c26e95f8797448eac8a82ce1a to your computer and use it in GitHub Desktop.
describing a bundling question in detail

I'm the author of an npm package that comes as a single ESM-format module, let's call it "A". This package is only a client-side (browser) library, it will not work in Node -- it interacts with client-only web platform APIs.

The "A" package doesn't use any typical build tools (typescript, webpack/vite, etc). But it does include a simple "publish-build" script that's used at npm publish time to prepare a dist/ directory for the package.

The package relies on three npm package dependencies (call them "B", "C", and "D"), which I do not own/control. These 3 packages only distribute themselves as plain .js window-global style scripts. They cannot be imported, because (unfortunately) they make assumptions about global-scope this (being window), non-strict mode, etc. That also means they can't just be inlined into the "A" distribution module file.

Moreover, these dependencies are big enough (and potentially useful enough) that a user of "A" might also want to access and use "B", "C", or "D" functionality directly (without just re-including them), so it's actually preferable that they expose themselves (as designed) on the window global.

The "A" publish-build tool copies the relevant distribution files from these "B", "C", and "D" dependencies into its dist/ folder, alongside the "A" distribution module file.

No Bundler Involved?

I do not want to assume (or require!) that someone's client-side web app is using a bundler tool (webpack, vite, etc). So if they are just doing a vanilla web app with no such tooling, they can just copy the 4 dist/* files (A.js, B.js, C.js, and D.js) into their JS assets folder.

To make sure that B.js, C.js, and D.js are loaded, they can do so manually in their HTML:

<script src="/path/to/B.js"></script>
<script src="/path/to/C.js"></script>
<script src="/path/to/D.js"></script>

Alternatively, I provide a "script loader" snippet that will dynamically inject <script> elements for these dependencies.

Either way, then they would import "A" module into their client app code like this:

// if using a client-side import-map:
import { some, thing } from "A";

// otherwise:
import { some, thing } from "/path/to/A.js";

This all works fine and great. But my problem is...

Bundler In Use?

Though I'm not requiring/assuming bundlers for consumers of "A", I do want to be "bundler friendly" if possible, in case they are using such. So let's now assume they will use such tooling.

They import the "A" dependency into their client-side app code like this:

import { some, thing } from "A";

And the package.json file for "A" includes these fields:

{
    "exports": {
        ".": {
            "browser": "./dist/A.js"
        }
    },
    "browser": {
        "A": "./dist/A.js",
        "./A.js": "./dist/A.js",
        "./B.js": "./dist/B.js",
        "./C.js": "./dist/C.js",
        "./D.js": "./dist/D.js"
    },
}

That's enough config that tools (e.g., vite, webpack) seem to find and include the A.js module code into the bundle just fine.

But how to get the tools to include B.js, C.js, and D.js into a bundle as well? Ideally, a bundler sees those entries in package.json and includes them. Perhaps webpack does that (not sure?), but from I can tell, vite does not pick up on those entries. So something else is needed.

Need Split Bundling, Though!

Recall that "A" doesn't use any bundler. So if "A" just had these in it:

import "/path/to/B.js";
import "/path/to/C.js";
import "/path/to/D.js";

...that would be "broken" in that those imports of non-ESM modules actually cause runtime errors, since the code in "B.js", "C.js", and "D.js" is not designed to run inside of ESM. Indeed, vite dutifully tries to inline them into its main app ESM bundle, which seems like it's solving the "discovery" problem (from above), but ultimately is broken when executed.

Vite doesn't seem to have a way to figure out that those dependencies need to be treated as non-ESM for bundling purposes. Obviously, I can't require a client-side web app to contort itself into using vite's "library mode" just so it can output UMD style code for the purposes of fitting in with the "B", "C", and "D" dependencies.

Ideally, vite would just smartly detect and produce a non-ESM bundle (if needed) alongside the main ESM app bundle, but I don't think it can do that, at least not out-of-the-box.

So instead, it looks like just telling authors to include this in their markup is the only reasonable way, and that feels super terrible to me:

<script src="/path/to/B.js"></script>
<script src="/path/to/C.js"></script>
<script src="/path/to/D.js"></script>

Ugh!

I would strongly prefer to suggest a snippet of vite-config they could drop in, that would be like a include directive to tell vite to include the contents from B.js, C.js, and D.js files (from the dist/ directory) in a separate/split non-ESM bundle.

Doesn't seem to be a clean way of doing this. But am I missing something?

I can't just ask the web app authors to change their bundler, or re-design their whole build process, just to use "A".

@getify
Copy link
Author

getify commented Mar 23, 2024

Update: I came to the conclusion this wasn't possible to do (with vite or webpack) via only config options. So I wrote plugins for both vite and webpack. :(

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