TLDR;
You can find a working example at this StackBlitz app.
Explanation
Let's start with how the config file will look like:
webpack.config.js
const path = require('path');
// We're doing this import so that we can get autocompletion.
/**
* @type {import("webpack/types").Configuration}
*/
const config = {
entry: {
home: path.join(__dirname, 'src', 'home.js'),
home_site2: path.join(__dirname, 'src/overrides/site2', 'home.js'),
home_site5: path.join(__dirname, 'src/overrides/site5', 'home.js')
},
output: {
path: path.join(__dirname, 'dist'),
filename: fileData => {
const { chunk } = fileData;
if (!chunk.entryModule) {
return crtPath;
}
const { userRequest: req } = chunk.entryModule;
const siteNameDelimiter = 'overrides/';
if (req.includes(siteNameDelimiter)) {
fileData.chunk = null;
fileData.filename = req.slice(
req.indexOf(siteNameDelimiter) + siteNameDelimiter.length
);
return '[path][name].js';
}
return '[name].js';
},
// Emit fresh files every time.
clean: true
},
mode: 'development'
};
module.exports = config;
Firstly, it's important to mention that each item in the entry object will result in a new chunk. That's why at the end of the bundling process, there will be 3 files.
Let's also clarify what a chunk is. A chunk can be thought of as a group of modules. The chunks which are associated with the entry items can be called main chunks. There are other types of chunks, for example when you use the import() function, a new chunk will be created. However, the main chunk differentiates itself from the other chunks by having injected code by webpack, also called runtime code. That additional code is responsible for lazily loading other chunks, handling the loading of a chunk and so forth. Other chunks have some runtime code too, but it's too little compared to the amount which a main chunk has.
Towards the end of the bundling process, the chunks assets are created. By an asset it is meant the resulting file of a chunk, which also contains all the necessary code. This part is also where the final path of a chunk is determined.
It happens in the TemplatedPathPlugin.js file. Here we can also see the available placeholders:
const replacePathVariables = (path, data, assetInfo) => {
/* ... */
// Filename context
//
// Placeholders
//
// for /some/path/file.js?query#fragment:
// [file] - /some/path/file.js
// [query] - ?query
// [fragment] - #fragment
// [base] - file.js
// [path] - /some/path/
// [name] - file
// [ext] - .js
if (typeof data.filename === "string") { /* ... */ }
// Compilation context
/* ... */
// Chunk context
/* ... */
}
For the current situation, the Filename context section is the one of interest.
Here is once again the configuration for the output option:
output: {
path: path.join(__dirname, 'dist'),
filename: fileData => {
const { chunk } = fileData;
if (!chunk.entryModule) {
return crtPath;
}
const { userRequest: req } = chunk.entryModule;
const siteNameDelimiter = 'overrides/';
if (req.includes(siteNameDelimiter)) {
fileData.chunk = null;
fileData.filename = req.slice(
req.indexOf(siteNameDelimiter) + siteNameDelimiter.length
);
return '[path][name].js';
}
return '[name].js';
},
// Emit fresh files every time.
clean: true
},
Notice that output.filename no longer is a string value, but a function. The function is invoked immediately before the TemplatedPathPlugin's logic is factored in.
So, in the case where the initial files are under the overrides directory, the resulting chunk path will be of this form:
/* ... */
// [base] - file.js
// [path] - /some/path/
// [name] - file
/* ... */
const siteNameDelimiter = 'overrides/';
if (req.includes(siteNameDelimiter)) {
fileData.chunk = null;
fileData.filename = req.slice(
req.indexOf(siteNameDelimiter) + siteNameDelimiter.length
);
return '[path][name].js';
}
/* ... *
In our case, path is whatever comes after overrides/ in the entry path and name refers to whatever is between the last / and .js.
For instance, in src/overrides/site2/home.js, path = site2/ and name = home, so '[path][name].js' = 'site2/home'.
And now another important aspect: whatever the returned path is(for instance 'home.js', 'site2/home'), it will be appended to the value of output.path, which in this case refers to:
path.join(__dirname, 'dist'),
So, the final paths for the chunks are:
- home ->
dist/home.js
- home_site2 ->
dist/site2/home.js
- home_site5 ->
dist/site5/home.js