and then I could use the library with the global Chart environment variable. Couldn’t be easier. I just copied chart.umd.js into my Git repository so that I didn’t have to worry about using NPM or the CDNs going down or anything. the build files aren’t always in the dist directory A lot of libraries will put their build in the dist directory, but not always! The build files’ location is specified in the library’s package.json . For example here’s an excerpt from Chart.js’s package.json . \"jsdelivr\": \"./dist/chart.umd.js\", \"unpkg\": \"./dist/chart.umd.js\", \"main\": \"./dist/chart.cjs\", \"module\": \"./dist/chart.js\", I think this is saying that if you want to use an ES Module ( module ) you should use dist/chart.js , but the jsDelivr and unpkg CDNs should use ./dist/chart.umd.js . I guess main is for Node. chart.js ’s package.json also says \"type\": \"module\" , which according to this documentation tells Node to treat files as ES modules by default. I think it doesn’t tell us specifically which files are ES modules and which ones aren’t but it does tell us that something in there is an ES module. example library 2: @atcute/oauth-browser-client @atcute/oauth-browser-client is a library for logging into Bluesky with OAuth in the browser. Let’s see what kinds of Javascript files it provides in its build! $ npm install @atcute/oauth-browser-client $ cd node_modules/@atcute/oauth-browser-client/dist $ ls *js constants.js dpop.js environment.js errors.js index.js resolvers.js It seems like the only plausible root file in here is index.js , which looks something like this: export { configureOAuth } from './environment.js'; export * from './errors.js'; export * from './resolvers.js'; This export syntax means it’s an ES module . That means we can use it in the browser without a build step! Let’s see how to do that. how to use an ES module with importmaps Using an ES module isn’t an easy as just adding a The reason we need an import map instead of just doing something like import { BrowserOAuthClient } from \"./oauth-client-browser.js\" is that internally the module has more import statements like import {something} from @atcute/client , and we need to tell the browser where to get the code for @atcute/client and all of its other dependencies. Here’s what the importmap I used looks like for @atcute/oauth-browser-client : Getting these import maps to work is pretty fiddly, I feel like there must be a tool to generate them automatically but I haven’t found one yet. It’s definitely possible to write a script that automatically generates the importmaps using esbuild’s metafile but I haven’t done that and maybe there’s a better way. I decided to set up importmaps yesterday to get github.com/jvns/bsky-oauth-example to work, so there’s some example code in that repo. Also someone pointed me to Simon Willison’s download-esm , which will download an ES module and rewrite the imports to point to the JS files directly so that you don’t need importmaps. I haven’t tried it yet but it seems like a great idea. problems with importmaps: too many files I did run into some problems with using importmaps in the browser though – it needed to download dozens of Javascript files to load my site, and my webserver in development couldn’t keep up for some reason. I kept seeing files fail to load randomly and then had to reload the page and hope that they would succeed this time. It wasn’t an issue anymore when I deployed my site to production, so I guess it was a problem with my local dev environment. Also one slightly annoying thing about ES modules in general is that you need to be running a webserver to use them, I’m sure this is for a good reason but it’s easier when you can just open your index.html file without starting a webserver. Because of the “too many files” thing I think actually using ES modules with importmaps in this way isn’t actually that appealing to me, but it’s good to know it’s possible. how to use an ES module without importmaps If the ES module doesn’t have dependencies then it’s even easier – you don’t need the importmaps! You can just: put in your HTML. The type=\"module\" is important. put import {whatever} from \"https://example.com/whatever.js\" in YOURCODE.js alternative: use esbuild If you don’t want to use importmaps, you can also use a build system like esbuild . I talked about how to do that in Some notes on using esbuild , but this blog post is about ways to avoid build systems completely so I’m not going to talk about that option here. I do still like esbuild though and I think it’s a good option in this case. what’s the browser support for importmaps? CanIUse says that importmaps are in “Baseline 2023: newly available across major browsers” so my sense is that in 2024 that’s still maybe a little bit too new? I think I would use importmaps for some fun experimental code that I only wanted like myself and 12 people to use, but if I wanted my code to be more widely usable I’d use esbuild instead. example library 3: @atproto/oauth-client-browser Let’s look at one final example library! This is a different Bluesky auth library than @atcute/oauth-browser-client . $ npm install @atproto/oauth-client-browser $ cd node_modules/@atproto/oauth-client-browser/dist $ ls *js browser-oauth-client.js browser-oauth-database.js browser-runtime-implementation.js errors.js index.js indexed-db-store.js util.js Again, it seems like only real candidate file here is index.js . But this is a different situation from the previous example library! Let’s take a look at index.js : There’s a bunch of stuff like this in index.js : __exportStar(require(\"@atproto/oauth-client\"), exports); __exportStar(require(\"./browser-oauth-client.js\"), exports); __exportStar(require(\"./errors.js\"), exports); var util_js_1 = require(\"./util.js\"); This require() syntax is CommonJS syntax, which means that we can’t use this file in the browser at all, we need to use some kind of build step, and ESBuild won’t work either. Also in this library’s package.json it says \"type\": \"commonjs\" which is another way to tell it’s CommonJS. how to use a CommonJS module with esm.sh Originally I thought it was impossible to use CommonJS modules without learning a build system, but then someone Bluesky told me about esm.sh ! It’s a CDN that will translate anything into an ES Module. skypack.dev does something similar, I’m not sure what the difference is but one person mentioned that if one doesn’t work sometimes they’ll try the other one. For @atproto/oauth-client-browser using it seems pretty simple, I just need to put this in my HTML: and then put this in script.js . import { BrowserOAuthClient } from \"https://esm.sh/@atproto/[email protected]\" It seems to Just Work, which is cool! Of course this is still sort of using a build system – it’s just that esm.sh is running the build instead of me. My main concerns with this approach are: I don’t really trust CDNs to keep working forever – usually I like to copy dependencies into my repository so that they don’t go away for some reason in the future. I’ve heard of some issues with CDNs having security compromises which scares me. I don’t really understand what esm.sh is doing. esbuild can also convert CommonJS modules into ES modules I also learned that you can also use esbuild to convert a CommonJS module into an ES module, though there are some limitations – the import { BrowserOAuthClient } from syntax doesn’t work. Here’s a github issue about that . I think the esbuild approach is probably more appealing to me than the esm.sh approach because it’s a tool that I already have on my computer so I trust it more. I haven’t experimented with this much yet though. summary of the three types of files Here’s a summary of the three types of JS files you might encounter, options for how to use them, and how to identify them. Unhelpfully a .js or .min.js file extension could be any of these 3 options, so if the file is something.js you need to do more detective work to figure out what you’re dealing with. “classic” JS files How to use it: : Ways to identify it: The website has a big friendly banner in its setup instructions saying “Use this with a CDN!” or something A .umd.js extension Just try to put it in a