See how we built a super-extensible customization engine using only Webpack’s native features and a few lightweight plugins — empowering customers to create unique widgets directly in their storefronts.
The Customization Headache
When I joined Findify, our integration process seemed deceptively simple: ship a React bundle that renders autocomplete and search widgets directly inside a merchant’s storefront. Customization was handled by adding a jQuery script atop that bundle to modify the rendered DOM - toggling colors, tweaking copy, or injecting extra styles. It worked, but as our customer base expanded, it quickly became clear that maintenance and support costs would skyrocket well before reaching any of our growth targets.
The Bottleneck and Solution
Once we realized that bottleneck, we stopped all further development, and I went hunting for a better way to handle customizations. We needed to keep overrides on our side perfectly in sync with the source code - essentially, “forking” individual components while leaving the master
version untouched. While debugging the compiled bundle one day, I noticed how Webpack stores modules like:
// simplified webpack bootstrap
(function(modules) {
var cache = {};
function __webpack_require__(id) {
if (cache[id]) return cache[id].exports;
var module = (cache[id] = { exports: {} });
modules[id].call(module.exports, module, module.exports, __webpack_require__);
return module.exports;
}
})({
11: function(module, exports) { /* React component A */ },
42: function(module, exports) { /* React component B */ },
/* ... */
});
It’s basically a plain JavaScript object: the module ID is the key and the factory function is the value. When an async chunk arrives, Webpack adds more factories into the same map. Then it clicked: if the module registry is mutable, could we swap out one factory for another at runtime, letting merchants override components?
Watching the webpackJsonp
call register modules made it obvious that Webpack was already set up to accept new factories at runtime - they just had to look like any other async chunk. So instead of patching the DOM after render, why not feed Webpack a replacement module before the component ever mounts? From that point on, the goal became figuring out how to collaborate with the bundle rather than fight it.
Swapping Modules
Step one was to make the modules addressable. By default, Webpack assigns incremental numeric IDs during bundling. To enable future module replacement, I developed a lightweight plugin that produces stable and reproducible IDs derived from each module’s original path:
const cache = new Map();
compilation.hooks.beforeModuleIds.tap('HashedPlugin', modules => {
const { moduleGraph, chunkGraph } = compilation;
for (const module of modules) {
if (!module.libIdent) continue;
const request = module.libIdent({ context: options.context || compiler.options.context });
const logicalPath = findModulePath(request, moduleGraph.getIssuer(module), module);
const logicalId = crypto
.createHash('md5')
.update(options.mapping[logicalPath] || logicalPath)
.digest('base64')
.substr(0, 4);
cache.set(request, logicalId);
chunkGraph.setModuleId(module, logicalId);
}
});
findModulePath
trims noise such as node_modules/
prefixes so we end up with short, stable IDs that our override tooling can reference. Alongside the bundle, we ship a manifest that maps human-friendly paths to those IDs. Now an override script can target the exact module to replace.
Step two was preparing the replacement code. We wanted to keep the entire pipeline under our control so migrations wouldn’t depend on any third-party tooling. The answer was a lightweight Chrome extension. Merchants edited the component source right inside the extension - it ran Babel (with the same plugins as our Webpack build) under the hood, injected the compiled factory into the page for instant feedback, and stored a copy in our database. That kept the override code in sync with our stack, and, crucially for a tiny team with a shoestring budget, avoided standing up extra build infrastructure.
Finally, we needed to play by Webpack’s rules to inject the override. The runtime already exposes a webpackJsonp
helper that accepts [[chunkId], moduleMap]
. We simply queued a call with the hashed ID and compiled the factory:
window.webpackJsonp.push([
['overrides'],
{
c1f3: function(module, exports, __webpack_require__) {
// compiled component code
}
}
]);
Webpack happily registered the new factory alongside our own code. In theory, the next __webpack_require__('c1f3')
would return the merchant version. In practice, the runtime cache still had the original factory warm and ready, which meant we needed one more trick up our sleeve.
Beating the Cache
Webpack caches module exports the first time they are required. Our injected factory was being registered correctly, but __webpack_require__
simply returned the already-cached module. The fix was to give ourselves an escape hatch that clears the cache for a given logical ID and forces Webpack to execute the new factory.
We patched the bootstrap once - right after the __webpack_module_cache__
object is declared to expose an invalidate helper that flushes the entire cache in one go. The production code lives in webpackHashPlugin.js
, but the essence is:
// inside webpack bootstrap
__webpack_require__.invalidate = function() {
var __cache = __webpack_module_cache__;
__webpack_module_cache__ = {}; // optionally repopulated with ignored modules
__webpack_require__.frozen = true;
__cache = null;
};
window.__invalidate_bundle = __webpack_require__.invalidate;
And then, once the page loaded and we fetched replacements from the server, we ran:
window.webpackJsonp.push(/* ...overrides ... */);
window.__invalidate_bundle();
The next time the widget requires that module, Webpack falls back to the registry, sees the override factory, and executes it. It’s a tiny “hack” but contained and reliable, and it gave us the confidence to ship module swaps without worrying about stale caches.
From Hack to Workflow
With the plumbing ready, we needed a process that felt safe for merchants and engineers. Our simple pipeline became:
- Customization-ready bundle. We structured the React code so that the presentation lived in separate “view” modules, and the business logic stayed untouched. The shared bundle shipped with hashed module IDs, and because it was open source, merchants could inspect the view layer they were allowed to tweak and update it locally through the Chrome extension.
- In-browser edits. When a merchant updated a component (say, the product card) inside the extension, Babel compiled the module on the spot, injected the new factory into the page immediately (felt like live reload), and sent a copy to our database.
-
Runtime injection. On the next page load, our boot script fetched the stored factories, called
webpackJsonp.push(...)
, and invalidated the cache so the shared bundle hot-swapped the updated modules. - Done. No redeploys, no special builds - just a tiny override diff layered over the shared bundle.
Because the base bundle remained constant and overrides were a handful of compiled factories, the customization pipeline felt nearly instant. Support could troubleshoot by comparing stored factories to the source, and our core team shipped features without touching merchant-specific code.
Beyond the Plumbing
What I’ve shared so far is only the essential mechanics. Around it, we shipped a mountain of DX polish: the Chrome extension evolved into a Monaco-powered editor with rich typings, inline docs, version history, and CSS-aware diffs. Because overrides lived in our database, we could script migrations whenever a breaking change slipped through, or roll everything back with a single job. Owning the customization data turned the system from a clever hack into something we trusted.
Seven years on, the code still hums along, quietly powering custom storefront experiences for hundreds of merchants. Remarkably, the core method of runtime customization has remained viable, even through several major Webpack upgrades. Despite every refactor and dependency change, the underlying approach has proven robust and adaptable.
Top comments (0)