BundleFree is an ExpressJS middleware for use during development that:
serves NPM packages to the browser without requiring a bundler or build server - and does so in a manner that's compatible with bundling for production
can use Rollup to convert CommonJS modules to ES6 for use client side
provides an easy way to inject livereload scripts so that when files are saved during development the browser automatically refreshes and updates
can display prominent in-browser JavaScript error messages
Notes:
this is only intended to be used during development - for production you should still use a bundler.
this is not a browserification tool and only works for NPM packages designed to work in browsers in the first place.
npm install --save codeonlyjs/bundle-free
Suppose you have a client side ES6 app that's in the ./client
sub-folder
of your ExpressJS project. Also, assume the bundled version is
available in the ./client/dist
folder.
For production we want to serve ./client/dist
.
For development we want to serve ./client
.
(Obviously, you can adjust paths to suit your project).
First, import the middleware:
import { bundleFree } from '@codeonlyjs/bundle-free';
Next, "use" the middleware:
if (process.env.NODE_ENV == "production")
{
// Production, serve bundled app
app.use(express.static(path.join(__dirname, "client/dist")));
}
else
{
// Development, serve unbundled app
app.use(bundleFree({
// The location of the unbundled client app
path: path.join(__dirname, "client"),
// Modules to be made available to the unbundled app
modules: [
'@scoped/package1',
'package2'
],
}));
}
Now, in your client side .js
files you can directly reference any
modules listed in the modules
option.
// Client side script files can now import directly from the bare
// module name:
import * from '@scoped/package1';
Since browsers only support a single ES6 import map, if you need to specify
other modules, use an object with module
and url
keys instead of a
string in the modules list:
modules: [
{ module: '@scoped/package', url: "/mylibs/package/index.js" },
'package2'
],
Since bundle-free is patching .html
files anyway, why not also inject in the
livereload
script so that saving
files automatically updates the browser.
By setting the livereload
option to either true
(to use the default livereload
server port) or to port number, bundle-free will automatically insert the script
at the bottom of the page.
See livereload
for more.
eg:
// npm install --save livereload
import livereload from 'livereload';
// omitted...
if (developmentMode)
{
// Development only
app.use(bundleFree({
// other settings omitted...
// Insert the live reload script
livereload: true,
}));
// Create live reload server and watch directories...
let lrs = livereload.createServer();
lrs.watch(path.join(__dirname, "client"));
}
While BundleFree is designed primarily for serving ES6 NPM packages it will also attempt to serve CommonJS packages.
If a referenced package or any of its dependencies are only available as CommonJS, BundleFree will use Rollup to convert the entire package to ES6 and serve the repackaged bundle.
YMMV
Mounting on a sub-path is supported as follows:
app.use("/somepath", bundleFree({ ... }));
Single page apps that use normal URL paths for in-page navigation need server
support to serve the main index.html
file of the SPA for any URL that doesn't
match a file in the client directory.
eg: suppose the page http://somesite.com/products/productname
should
be handled by the single page app at /index.html
To support this, set the spa
property to true:
app.use(bundleFree({
// The location of the unbundled client app
path: path.join(__dirname, "client"),
// Serve URLs that don't match a file as index.html
spa: true;
// Modules to be made available to the unbundled app
modules: [
'@scoped/module1',
'@scoped/module2'
]
}));
Since you probably want this same behaviour for the production release, you can use bundle-free without the module remapping:
app.use(bundleFree({
path: path.join(__dirname, "client/dist"),
spa: true,
}));
Finally, if the /index.html
file references relative files you'll probably
want to make them absolute too (otherwise they won't work in sub-path urls).
eg: suppose index.html
references ./Main.js
, this won't work for a single page
app if index.html
is served in response to a request for /sub/sub/page
-
because the browser will try to request this as /sub/sub/Main.js
which
doesn't exist.
We can't just use an absolute URL in the index.html
file because then the
bundler won't find it (at least Vite doesn't seem to)
By using the replace
option we can serve the absolute path the /Main.js
during development, but leave the relative ./Main.js
in place for when
running the bundler.
replace: [
{ from: "./Main.js", to: "/Main.js" }
],
from
can be a string or regular expression.
Usually web-browsers are fairly quiet about JavaScript errors unless you bring up the debugger/inspector and check in the console.
BundleFree includes an option inYaFace
that when set to true injects
a script that watches for client side JavaScript errors and displays
a very prominent "in your face" error message.
app.use(bundleFree({
// Other options omitted
// Display prominent errors
inYaFace: true,
}));
Here's a complete example that supports single-page app mode, live reload and prominent error display.
if (process.env.NODE_ENV == "production")
{
app.use(bundleFree({
path: path.join(__dirname, "client/dist"),
spa: true,
}));
}
else
{
app.use(bundleFree({
path: path.join(__dirname, "client"),
modules: [
'@scoped/module1',
'@scoped/module2'
],
replace: [
{ from: "./Main.js", to: "/Main.js" }
],
spa: true,
inYaFace: true,
livereload: true
}));
// Create live reload server and watch directories...
let lrs = livereload.createServer();
lrs.watch(path.join(__dirname, "client"));
}