Skip to content

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.

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';
},
},
};

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]'
}
}
]
}
};

  1. Define Entry Keys Clearly: Always remember that [name] is an alias for the key in your entry object. If your build script dynamically generates entries, ensure the key generation logic doesn’t strip the dots.
  2. Avoid Double Extensions where possible: While styles.css.js is valid, it can confuse some middleware and IDEs. If the file is strictly JavaScript that injects CSS, naming it styles.inject.js often avoids “greedy” extension stripping by simpler regex patterns in loaders.
  3. Check your resolve.extensions: Ensure that .js is present in your resolve.extensions array, but be careful with the order. If you have file.js and file.ts, the order determines which one is picked if you import ./file.

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).