In my previous article, I explained how to lazy load modules using the dynamic import syntax and all the customisation involved when using along Webpack. Now, I’ll target a different scenario:
You have a library which includes different kind of “add-ons”, in a way that you can just create a new JS file under an addons/ directory and your lib will dynamically import it on build time, including it to the lib bundle. However this add-ons can’t work simply alone like lodash or RxJS does, as they’re too out of context to be used directly (or you simply just don’t want expose them directly, without an facade object).
Great, you’re going to use Webpack contexts as Dynamic Imports you basically resolve modules at runtime (although webpack maps the calls to the modules on the build). Dynamic imports are a better approach for code-splitting, something you don’t want in you lib. What you really want is just resolve dynamically the modules at building time including them in the final bundle, and that’s it!
“Wait, what? What’s actually is a context?!”
Ok, I realised that I just came with a magical solution with an very short explanation. Let’s go further on the subject.
Info: If you know what Webpack contexts are and how to use them, you can just skip this section
Contexts are a way to make Webpack dynamically read modules present on directories (and subdirectories if you want), making them available through a importer/requirer function. All this analysis is made at building time, guiding Webpack to create a context module just for resolving this dynamicly included modules.
Let’s create a simple example, a library to do mathematical calculations, let’s call it calculib. The project structure is simple:
Based on that structure, inside index.js we basically import everything inside operations that ends with “.op.js”:
Bundling this index.js as entry point for Webpack will result on this ouput:
The context module name says it all: it imports synchronously and non-recursively the “operations/*.op.js” files. We can see also that differently from dynamic imports, which does async importations by default, breaking the modules into one or more chunks, here we have all found modules bundled together, perfect for a library final distribution.
This solves the problem of not hard-coding static imports on the lib index and for general use it works great, but for our specific use (custom builds) it generates other problems!
As you require a dynamic context, Webpack will be unable to do tree-shaking because it can’t check if all that code is used or not (we aren’t using static imports). The problem is that you want give a way, for your library users, to do at least a manual remotion of unwanted code.
You want to make your lib. able to have custom builds, something that tools like jQueryUI used to have. But in the present days, the majority of the lib users only use npm (or yarn) and refuse themselves to download you library from a site and commit it directly on their repositories.
Webpack comes with the fantastic context replacement plugin, that let us manipulate the contexts on the building time. So, we have a solution for removing unwanted modules while building!
After replacing the context, we can see the results:
And the final bundle:
What we need now is just a way to make any user being able to expose this customisation of excluding unwanted modules.
NPM comes with a handful config API, in a way that we can expose some options for the users. When running any npm scripts, it automatically injects some environment variables, easily accessible by the node process.env object. There two ways to use this configurations:
Putting directly to the package.json and accessing via process.env.npmpackage[KEY]. You can access basically any key on package.json using this, from the default one to custom defined ones. But we need to access, not the local configuration, but the configuration of any application installing our lib. For this, we have the second option:
The config object is a way to let users provide values for local/installed packages via a simple command:
npm config set <packagename>:<option> <value>
In our case we can let the users do:
npm config set calculib:excludes multiplication,division
And read this values inside webpack.config.js by doing:
const excludes = process.env.npmpackageconfig_excludes;
Note I: You’re able to set these same values under the package.json “config” key, but this will overwrite any user settings;
Note II: The configuration values are written on a global .npmrc, but you can create a local one, in the root of the project, and commit this file to make your CI/CD being able to use the same config too.
For getting these values we need to sanitize them, creating an array from this “comma separated string”, and finally create a regex from it:
In the package.json you can add the building process to the “postinstall” script:
"build": "webpack --mode production",
"postinstall": "npm run build"
postinstall script will run after the package is installed. The user will be able to set new configurations and call the command:
$> npm set config calculib:excludes sum,division
$> npm rebuild calculib
forcing the lib. to be re-builded under the new configuration.
After setting up the config values, running the build command will output the following information:
We can see that the configuration was indeed used, but let’s check how it behaves in a exterior package, installing it from the directory:
But hey, I want the entire lib with no exclusions! Let’s remove the config and rebuild:
We can see now all the modules included on the final bundle, and all made on a exterior package!
To make your life easier, I made the project that I use as an example here into a real npm package!
You can just go and try it with and with the configurations we used here 😄
If you want to check the code in details, the project is also available on GitHub:
Just using some small techniques we were able to make a really nice configurable plugin, without the need of users committing the custom builded lib. directly on their repos. Plus, you can easily setup the inverted strategy and build based on “includes” instead of “excludes”.
There’s also another range of uses for what was explained on this article, for instance, to make you application feature flag ready, removing modules in the building, depending on the project configuration.
Of course, there’s some downsides on the proposed strategy, as the build occurs on the installer machine. For libraries, the building dependencies must be installed as “dependencies” instead of “devDependencies”, and this will turn the installation longer. Another fact is, if your library depends on OS environment, specific node versions (or one of the dependencies do), this can make errors arise on the installation. So analyse your situation and use with care! 😉
Thanks for reading!
If you liked and now you’re doing custom builds like a boss 😎, please show you feedback giving some claps 👏 or leave a comment 💬. This is very important to see if you’re liking (or not) the content I’m posting.