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 import
ed, 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.
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...
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.
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 import
s 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".
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. :(