Webpack: Creating dynamically named outputs for wildcarded entry files
If that isn’t a sexy title I don’t know what is.
Recently I hit a Webpack-sized brick wall whilst trying to get it to output bundles dynamically named after their entry files. The tricky part is that I wanted it to do it prospectively on all files of a certain type that I may want to write in the future.
I couldn’t find any answers online or node packages I could enlist, so I decided to write my own.
The problem was as follows; I have a folder for all my core JavaScript:
|-- core
|-- file1.js
|-- file2.js
I know what those files are called, I also know that I want them to be bundled together and where to put that output file:
|-- build
|-- main.js << Bundle all core files here
|-- core
|-- file1.js
|-- file2.js
Easily done:
// webpack.config.jsmodule.exports = {
entry: ['./core/file1.js', './core/file2.js'],
output: {
filename: './build/[name].js', << [name] defaults to 'main'
}, << more on that later
}
Then I have a bunch of other folders somewhere that relate only to certain pieces of content and I want their bundles to be output relative to where the source entry files are:
|-- build
|-- main.js
|-- core
|-- file1.js
|-- file2.js
|-- content
|-- one
|-- index.js << This index
|-- main.js << Gets bundled here
|-- two
|-- sub
|-- index.js << This index
|-- main.js << Gets bundled here
As devs, we write the index.js
but we want Webpack to pick it up and process the output main.js
right next to it in the same folder. We were already able to do that by passing an Object to Webpack’s entry
value, overriding the default [name]
, and process multiple bundles. This does work as long as we know about the files we’re dealing with:
// webpack.config.jsmodule.exports = {
entry: {
build: ['./core/file1.js', './core/file2.js'],
content/one: ['./content/one/index.js'],
content/two/sub: ['./content/two/sub/index.js'],
},
output: {
filename: './[name]/main.js',
},
}
Webpack will build a bundle with each of the entry
Object’s entries. The Keys (build, content/one, content/two/sub)
will be passed down to output
as the [name]
value, and the files to be bundled will be the array values for each Key.
This technique is quite powerful and can be utilised to produce absolute filenames with extensions if you so wish, e.g:
...entry: {
build/main.js: ['./core/file1.js', './core/file2.js'],
},
output: '[name]',...
The final challenge in our puzzle is the fact that we don’t know all the content
bundles we’re going to want to build, and even if we did we wouldn’t necessarily want to hard-code entries for all of them. So we use the glob
node package to match a wildcard and return an array of all files that match it:
// webpack.config.jsconst glob = require('glob');entry: glob.sync('content/**/index.js'), << Returns Array of files
And here, finally, is where the problem lay. There was no way to create a bundle with each of those wildcarded files, which was then output to the same folder as the entry.
But knowing what we know about the entry
value Object and being able to manipulate the [name]
to our advantage, it was a simple matter of one afternoon of coding to come up with a solution. Manipulate the filepath value to return a suitable [name]
for our build:
// webpack.config.jsconst glob = require('glob');const entryArray = glob.sync('content/**/index.js');const entryObject = entryArray.reduce((acc, item) => {
const name = item.replace('/index.js', '');
acc[name] = item;
return acc;
}, {});module.exports = {
entry: entryObject,
output: '[name]/main.js',
}
The entryObject
from above would return this, a suitable value for Webpack’s entry
value:
{
content/one: ['./content/one/index.js'],
content/two/sub: ['./content/two/sub/index.js'],
}
If necessary we could use Object.assign()
to combine multiple Objects of specified or wildcarded entry files to create appropriate output bundles using this technique, or use .reduce()
or .map()
or whatever we like. And that’s exactly what I did :)
But because I saw this as a common enough problem without an existing solution I created a simple package that can take care of all that for us, as well as give us a reasonable API for most other use cases.
Whether you want to roll your own custom implementation or want an out-of-the-box solution, I would recommend checking it out: