Harmonizing how to bundle and package JavaScript/ECMAScript features
frontend packaging architecture - 📁 experiments
Part of my time has been spent on finding a flexible way to organize code and make it reusable between projects.
I enjoy writing in TypeScript, but not all projects are always the same. Some projects are written as a component library, sometimes they aren’t written in TypeScript. We wanted a way to tell an entry-point, and flexibility to re-use the same configuration even when we have many build output targets.
I’ve found a way to harmonize how to bundle JavaScript projects following closely NPM’s packaging conventions. To do so, I’ve found and successfully been able to manage many small projects, each with their respective way of testing and bundling. All of this, without imposing choices when using them.
The objective was that we can write modern ECMAScript, and leverage polyfills that Babel,Rollup and core-js can give us.
For example, a library can be written in TypeScript, but is known to be destined to be used in an Express middleware in ECMAScript 5 on a legacy Node.js service at v8.0.0 LTS. We can have a build target for this. We could equally have a build target to be transpiled to run on Node.js v12.0.0 too.
In this example below you’ll see that I use the same "convention enforcement" tool would be used between many small modules.
I called that tool "@renoirb/conventions-use-bili
", it’s a micro package
inside a monorepo (using Microsoft Rush.js, link to monorepo’s
rush.json
), and published on NPM.
Notice that "@renoirb/...
" packages shown here are public copies (my employer
allowed me sharing publicly) of our internal packages namespaced as
"@frontend-bindings/...
" that I’ve been working on in the last months. The
original package name is @frontend-bindings/conventions-use-bili and is
effectively the same as shown throught this document. In the following Video is
on YouTube Publishing and importing vue-app-layout into a Nuxt.js
project shown in "Data-Driven UI" document, we can also see
conventions-use-bili in use.
(Aside: I’m showing code maintained on two distinct projects, one hosted on GitLab, and a library maintained on GitHub, published on NPM.)
Introduction
Before this library, I was regularly copy-pasting Babel+Rollup.js configuration code around resulting about 50 lines and more of code that would sometimes be the same, or with just slight modifications. Making it hard to peers to see the difference. Copy-Pasting 50 lines for a project or two is OK, but when we have more than 30 packages, it becomes wild, quickly. Even more so when we know we’ll have many more small packages coming up.
It’s been useful to me on many occasions. I would have to copy-paste many lines and tweaks around, until I created that module.
I’d like to make this example an illustration of code publicly visible of infrastructure package and a few example modules leveraging that infrastructure package.
Design:
- Simplest usage surface (i.e. one file, a handful of lines)
- Support modern ESM module in
src/*
- Support loading tests system
- Support using TypeScript, but not mandatory
- Support multiple build targets
Scenario
egoist/bili is a tool written by a popular Vue.js contributor @egoist which helps setting Rollup.js, and Babel, and TypeScript together. It helps with managing configuration but had a few options missing (notice that there are other packages that helps with missing features mentioned here, but won’t be covered here).
The module mostly piggy-back on BROWSERSLIST
, whether or not to add
node_modules dependencies as part of the build output
(hasBiliBundleNodeModulesOption
), and with core-js.
@renoirb/conventions-use-bili is used in a project with simplest usage surface
(e.g. jsonschema-aware-loader
, date-epoch
, validatable
in
their respective bili.config.js
’ conventions-use-bili’s
config). Then the rest is handled by build CLI
arguments. convention-use-bili is a package that is published through NPM
(public copy), and is part of many other
packaging (see conventions-use-bili’s
package.json) specialized packages. See the
conventions-use-bili’s top-level Rush.js rush.json
monorepo
configuration.
Breaking into smaller packages doesn’t make us forced to put ALL projects together, for example JSONSchema Aware Loader is maintained in a separate monorepo hosted on GitLab. So we have full flexibility in where we maintain and in what language packages are written in.
Notice that @renoirb/validatable
validatable isn’t maintained in
TypeScript, yet it is managed using the same tooling.
In the case of JSONSchema Aware Loader, a utility I’m using to enforce JSON schema validation BEFORE importing, we won’t need many build variants.
But what if we do need many variants.
Let’s say we have a bit of code that uses for-of ECMAScript 2015 iteration (for-of-example module), and we want the same code, with same tests, to be deployed on both modern and legacy.
We’d need two build outputs for-of-example module as ESM module (notice the code is used as-is), for-of-example module to work on Internet Explorer 6 up to 9.
Notice in the case of Internet Explorer’s version, we are
inlining automatically from publicly maintained
list of "polyfills" (see core-js
source). All of which
is possible because one of the build target is specifically targetted for
Internet Explorer
use-cross-env BROWSERSLIST='ie 6-9' \
use-bili \
--target browser \
--format iife \
--module-name ForOfExample \
--file-name index.browser.ie6to9.js \
--bundle-node-modules
With all of this in place, we can write modern code with all testing and maintenance tooling and have multiple bundles of the same tested code to run in different run-time conditions.