It is possible to use packages installed in node_modules
in your frontend without any bundlers today.
This is amazing because we don’t need to create complicated workflows to reuse code — we can simply download the library from npm and import it. Our projects can become simpler and more straightforward.
Prerequisites
Two things:
- Your server needs to be able to serve up the
node_modules
folder (or at least a part of it that you want to expose) - The library you’re using must be ES Modules (ESM) compatible. It cannot be written in Common JS (CJS)
Although many libraries are still written in CJS today, it shouldn’t be a problem going forward with prolific authors like Sindre Sorheus supporting a move to ESM.
Importing the library
Let’s assume you have this project structure.
- project |- index.html |- js |- main.js |- node_modules
If your server can serve the node_modules
folder, importing the library is easy — you simply traverse into the node_modules
folder and grab the correct javascript file.
Here’s how I import a library called zlFetch
.
import zlFetch from '../node_modules/zl-fetch/dist/index.mjs'
You can test this with http-server since http-server serves up the node_modules
folder.
npx http-server
Serving node_modules with Express
Now let’s assume you have a common Express folder structure, like this:
- project |- views |- index.html |- public |- js |- main.js |- node_modules
Express doesn’t serve up the node_modules
folder so you have to make slight changes.
// Doesn't work app.use(express.static('node_modules'))
The best way I found is to create a manual route for each library you want to import.
// Allows browsers to access `node_modules/zl-fetch` app.use('/zl-fetch', express.static('node_modules/zl-fetch')
We can now import zlFetch
like this.
import zlFetch from `../zl-fetch/dist/index.mjs`
Notice we don’t even need to include node_modules
in the path anymore, which is a much better developer experience (because there are fewer things to write!).
We can improve it further by configuring any necessary paths in Express. In this case, we can include the dist
path so we don’t even need to write it in the frontend.
// Adds the `/dist` folder app.use('/zl-fetch', express.static('node_modules/zl-fetch/dist'))
import zlFetch from `../zl-fetch/index.mjs`
You notice the import path begins with ../
? We did this because the main.js
file is in the js
folder. We can convert ../
to ./
if we include the js
folder in our Express route.
// Includes the `/js` folder in the express route app.use('/js/zl-fetch', express.static('node_modules/zl-fetch/dist'))
import zlFetch from `./zl-fetch/index.mjs`
Using this method with multiple libraries
We can create an object of library–path mappings. Here’s an example that would work.
const libraries = { '@zellwk/javascript': '@zellwk/javascript/lib', 'zl-fetch': 'zl-fetch/dist' } Object.entries(libraries).forEach(library => { const [lib, libPath] = library app.use(`/js/${lib}`, express.static(path.join('node_modules', libPath))) })
We can import multiple libraries without any problems once we do this. Here’s an example:
import * as localStore from './@zellwk/javascript/localstore.js' import zlFetch from './zl-fetch/index.mjs'
Demo
Here’s a demo for you to play with.
Improving it further?
It’s possible to read the package.json
file to find the correct file to import, so we don’t need to create manual mappings. I’m not sure how this would work yet since I haven’t tested it out.
If you work on this, let me know!