Preserving Interim File Extensions in Webpack Output Filenames
In a community I participate in, a developer recently ran into a frustrating hurdle while architecting a micro-frontend: “How do I preserve interim extensions when specifying output.filename? I have files named feature.worker.js and styles.css.js, but Webpack’s [name] template keeps outputting them as feature.js and styles.js.”
This is a common pain point when your build pipeline relies on specific naming conventions for downstream consumption or CDN caching strategies.
Short Answer (For Experienced Devs)
Section titled “Short Answer (For Experienced Devs)”If you are using Webpack 5, the quickest fix is to move away from the simple [name] template and use the [base] template (for assets) or provide a function to output.filename.
// webpack.config.js (Webpack 5.x)module.exports = { output: { // Option A: Use [base] for Asset Modules assetModuleFilename: 'static/[base][ext]',
// Option B: Use a function for full control over Chunks/Entries filename: (pathData) => { // pathData.chunk.name contains the entry key return '[name].bundle.js'; }, },};Deep Dive: Why Webpack Strips Extensions
Section titled “Deep Dive: Why Webpack Strips Extensions”By default, Webpack treats the “name” of a file as the filename minus its final extension. If your entry point is defined as main: './src/index.js', the [name] is main.
However, if you have a file named report.pdf.js, and you want the output to be report.pdf.[contenthash].js, Webpack often sees .pdf.js and assumes the “name” is just report if the entry key is configured that way. The behavior changes depending on whether you are dealing with Entry Points or Asset Modules.
Solution 1: Leveraging Entry Point Mapping
Section titled “Solution 1: Leveraging Entry Point Mapping”Applies to: Webpack 4.x, 5.x
The [name] placeholder refers to the key in your entry object, not the physical filename on disk. If you want to preserve extensions, you must include them in the entry definition.
// webpack.config.js (Illustrative example — Webpack 5.80+)module.exports = { entry: { // Explicitly include the interim extension in the key 'data.schema': './src/scripts/data.schema.js', 'worker.service': './src/workers/worker.service.js' }, output: { filename: '[name].bundle.js', path: __dirname + '/dist' }};Result: This produces data.schema.bundle.js and worker.service.bundle.js.
Solution 2: Using a Naming Function (Most Robust)
Section titled “Solution 2: Using a Naming Function (Most Robust)”Applies to: Webpack 5.x
For complex setups where entry keys are generated dynamically, using a function for output.filename provides the pathData object. This object contains metadata about the chunk, including the original resource path.
// webpack.config.js (Node 18+, Webpack 5.x)const path = require('path');
module.exports = { output: { filename: (pathData) => { const name = pathData.chunk.name; // You can logic-check the filename here if (name.includes('worker')) { return 'workers/[name].js'; } return 'js/[name].[contenthash].js'; }, },};Solution 3: Preserving Extensions in Asset Modules
Section titled “Solution 3: Preserving Extensions in Asset Modules”Applies to: Webpack 5.x
If the “interim extension” problem is occurring with images or CSS files processed via Asset Modules (rather than entry points), the [name] template will indeed strip the extension. Use [base] instead, which includes the full filename and the original extension.
// webpack.config.js (Webpack 5.x)module.exports = { module: { rules: [ { test: /\.css\.js$/, type: 'asset/resource', generator: { // [base] = filename + extension (e.g., "styles.css.js") // [ext] = the output extension (usually determined by the loader) filename: 'assets/[base][ext]' } } ] }};Prevention & Best Practices
Section titled “Prevention & Best Practices”- Define Entry Keys Clearly: Always remember that
[name]is an alias for the key in yourentryobject. If your build script dynamically generates entries, ensure the key generation logic doesn’t strip the dots. - Avoid Double Extensions where possible: While
styles.css.jsis valid, it can confuse some middleware and IDEs. If the file is strictly JavaScript that injects CSS, naming itstyles.inject.jsoften avoids “greedy” extension stripping by simpler regex patterns in loaders. - Check your
resolve.extensions: Ensure that.jsis present in yourresolve.extensionsarray, but be careful with the order. If you havefile.jsandfile.ts, the order determines which one is picked if you import./file.
Follow-up FAQ
Section titled “Follow-up FAQ”Q: Does this affect source maps?
A: Yes. If you change the output.filename logic, Webpack’s devtool (source mapping) will follow those names. If you use a function to return the filename, ensure it returns a unique string to prevent source map collisions.
Q: How does this work with the CleanWebpackPlugin?
A: CleanWebpackPlugin (and the built-in output.clean: true in Webpack 5) looks at the output.path directory. It will correctly identify and remove these files regardless of whether they have interim extensions, as long as they are part of the Webpack manifest.
Q: Can I use [path] to preserve the directory structure too?
A: Absolutely. In the filename string or function, you can use the [path] placeholder. In Webpack 5, [path] will represent the path relative to the context option (usually your project root).