Jey Balachandran
Jey Balachandran

Reputation: 3935

Delete unused webpack chunked files

I'm looking for information on how to delete old webpack chunked files. Here is my current webpack configuration:

var path = require('path');
var webpack = require('webpack');

module.exports = {
  debug: false,
  outputPathinfo: true,
  displayErrorDetails: true,
  context: __dirname,
  entry: {
    common: ['./src/common.coffee'],
    a: './src/a.cjsx',
    b: './src/b.cjsx'
  },
  output: {
    filename: '[name]-[chunkhash].js',
    chunkFileName: '[name].[chunkhash].js',
    path: path.join(__dirname, 'js')
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin('common', 'common-[chunkhash].js'),
    new webpack.optimize.UglifyJsPlugin({
      compress: { warnings: false }
    })
  ],
  module: {
    preLoaders: [
      {
        test: /\.coffee$/,
        exclude: /node_modules/,
        loader: 'coffeelint-loader'
      }
    ],
    loaders: [
      { test: /\.coffee/, loader: 'coffee' },
      { test: /\.cjsx$/, loaders: ['coffee', 'cjsx'] },
      { test: /\.js$/, loader: 'jsx-loader?harmony' }
    ]
  }
}

If I am running $(npm bin)/webpack --config webpack.js --watch and make changes to a.cjsx, it compiles a newer version of that file with a new chunkedhash. However, the old one remains and I'd like it to be deleted right away.

  1. How can I delete the old version of the chunked file?
  2. Is there a way for me to hook into an after callback once watch finishes compiling?

Upvotes: 32

Views: 30013

Answers (12)

Vadim Isakov
Vadim Isakov

Reputation: 51

clean-webpack-plugin is evil. I strongly do not recommend using it

I remove all unused chunks after emit using this script.

const exec = require('child_process').exec;

mix.webpackConfig({
  devtool: 'inline-source-map',
  output: {
    chunkFilename: 'static/ui/js/chunks/[hash][name].js',
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          chunks: 'async',
          idHint: 'vendor'
        }
      }
    }
  },
  plugins: [
    { // Removing unused chunks after emit
      apply: (compiler) => {
        // Called after emitting assets to output directory
        compiler.hooks.afterEmit.tap('AfterEmitPlugin', (compilation) => {
          const CHUNKS_FOLDER = 'static/ui/js/chunks/';
          // List of used chunks
          const chunks = compilation.chunks.map(
            chunk => chunk.files[0]
          ).filter(
            chunk => chunk.startsWith(CHUNKS_FOLDER)
          ).map(
            chunk => chunk.slice(CHUNKS_FOLDER.length)
          )

          // Substring to exclude used chunks
          const chunksExcludingSubstr = chunks.map(
            chunk => `-not -name ${chunk}`
          ).join(' ')

          exec(
            `find ${CHUNKS_FOLDER} -type f ${chunksExcludingSubstr} -delete`,
            (err, stdout, stderr) => 
            {
              if (stderr) process.stderr.write(stderr);
            }
          );
        });
      }
    }
  ]
})

Upvotes: 0

DraKoan
DraKoan

Reputation: 61

my case: webpack 5 + multipage application + themes.css via entry points

solution: https://github.com/webdiscus/webpack-remove-empty-scripts

this plugins don't work with webpack 5 entry points or with MiniCssExtractPlugin:

webpack-fix-style-only-entries, webpack-extraneous-file-cleanup-plugin, webpack-remove-empty-js-chunks-plugin, webpack-delete-no-js-entries-plugin.

my webpack.config.js:

const fs = require('fs');
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts');

const isProd = process.env.NODE_ENV === 'production';
const isDev = !isProd;

const PAGES = ['app', 'help'];

const getThemes = (themePath, alias) => {
  let themes = {};
  const longPath = './' + alias + '/' + themePath;
  fs.readdirSync(longPath).forEach(function(fileName) {
    const fileNameWithPath = path.join(themePath, fileName);
    const fileNameWithLongPath = path.join(longPath, fileName);

    const stat = fs.lstatSync(fileNameWithLongPath);
    if (stat.isDirectory()) return;
    if (!/\.scss$/.test(fileName)) return;

    const nameWithoutExt = path.basename(fileName, '.scss');
    themes[nameWithoutExt] = ['./' + fileNameWithPath];
  });
  console.log(themes);
  return themes;
};

const themes = getThemes('scss/themes', 'src');

const getFilename = (filename, ext) => {
  let name = filename == 'index' ? 'bundle' : filename;
  const isTheme = (ext == 'css' && name.startsWith('theme')) ? true : false;
  const needHash = (isDev || isTheme) ? false : true;
  return needHash ? name +`.[fullhash].` + ext : name+'.'+ext;
};

const getCSSDirname = filename => {
  const isTheme = filename.startsWith('theme');
  return !isTheme? '/css/' : '/css/theme/';
};

const getHTMLWebpackPlugins = arr => {
  // this function config multipages names and add to html-pages
  // inside <head> tag our themes via tag <link rel="stylesheet" href="....css" ...>
  // and return array of HTMLWebpackPlugins
};

module.exports = {
// ... //
  entry: {
    // mutipage:
    app:  ['./index.js', './scss/app.scss'], 
    help: ['./help.js', './scss/help.scss'],
    // multitheme:
    ...themes,
  },
  optimization: {
    removeEmptyChunks: true, // not work!!!
  },
  // ... //
  plugins: [
    // ... //
    ...getHTMLWebpackPlugins(PAGES),
    new RemoveEmptyScriptsPlugin({
      ignore:  PAGES,
      enabled: isDev === false,
    }),
    new MiniCssExtractPlugin({
      filename: pathdata => {
        return getCSSDirname(pathdata.chunk.name) + getFilename(pathdata.chunk.name, 'css');
      },
      chunkFilename: isDev ? '[id].css' : '[id].[contenthash].css',
    }),
  ],
};

my src files:

[src]:
 - index.js
 - index.html
 - help.js
 - help.html
 - [scss]:
 - - app.scss
 - - help.scss
 - - [themes]:
 - - - light.scss
 - - - dark.scss
 - - - blue.scss

after build:

[dist]:
 - app.js
 - index.html
 - help$hash.js
 - help$hash.html
 - [css]:
 - - app$hash.css
 - - help$hash.css
 - - [themes]:
 - - - light.css
 - - - dark.css
 - - - blue.css

Upvotes: 1

Joshua Swain
Joshua Swain

Reputation: 690

I just had to stop my server and run yarn serve again

Upvotes: 0

GProst
GProst

Reputation: 10237

Since Webpack 5.20.0 you can use output.clean option

Upvotes: 9

JohnnyFun
JohnnyFun

Reputation: 4303

Looks like [email protected]+ has built-in support for this https://webpack.js.org/configuration/output/#outputclean. I use [chunkhash] in my chunk filenames and they get cleared out if I stop comment out dynamic imports and added back in if I uncomment them.

Upvotes: 1

Thrashzone13
Thrashzone13

Reputation: 109

For Windows users

  "scripts": {
    "build": "npm run clean && webpack --mode production",
    "clean": "del /f /s /q dist 1>nul"
  }

Upvotes: 0

plunntic iam
plunntic iam

Reputation: 994

The answer

I've decided to write an answer because others - although trying to answer the question directly - overlooked the most important part in my opinion.

And the most important part is: you shouldn't be doing it this way. Using [hash] placeholders in your development setup cause many headaches with other tooling (phpstorm's path autocomplete in symfony plugin for example). Also it's poor for webpack's incremental compilation performance and thus is not recommended by official webpack docs (reference).

So for future readers: just keep it simple for development config - define your filename as [name].js and move on.

Edit

There seems to be a confusion about what to do with the old chunk-files on the production server. Well, you don't do anything. Once a version is deployed it shouldn't be ever changed. You just keep creating new versions when deploying and keep previous as a backup. Why?

Because you want you're rollback to be reliable and for it to be possible your rollback needs to be extremely simple and atomic. If your rollback procedure is doing anything more than switching a symlink, rerouting to previous container (or similar simple operation) you're probably™ going to end up in trouble.

Rollback isn't a process of "re-deploying" the application again, but now to the previous version. It's a process of "un-doing" the deployment. So doing a git checkout to the previous version followed by a npm build --but-please-be-hurry --and-im-begging-you-dont-fail while your production app is hanging there, completely exploded doesn't cut here.

Rebuilding a previous version of the application - just like the deployment - may fail for many reasons. That's why a rollback should be switching/rerouting back to the exact same version-build that is proven to be working. Not ==-the-same, 100% ===-the-same. That's why you need to keep your previous version around, because that's the ===-same. A "regenerated" one is - in best case scenario - only ==-the-same, and so it is not proven to be working, only assumed.

And no, no amount of CI, staging environments or whatever will give you a guaranteed successful deployment. Part of doing it the right way is to be prepared for when things go wrong. And things will go wrong. Hopefully only from time to time, but still.

Of course once you have 3, 5 or <put-your-number-here> versions backed up you may start to remove the oldest ones as you probably won't ever need more than 3.

Upvotes: 7

Amaimersion
Amaimersion

Reputation: 1015

You can solve the problem № 1 by using remove-files-webpack-plugin.

Use this plugin like this:

plugins: [
  new RemovePlugin({
    watch: {
      test: [
        {
          folder: './js',
          method: (absPath) => new RegExp(/(.*)-([^-\\\/]+)\.js/).test(absPath)
        }
      ]
    }
  })
]

In "watch" mode (not normal compilation!) it grabs all files from ./js folder and tests them with this regular expression /(.*)-([^-\\\/]+)\.js/. Analyze this regular expression on regex101 (unit tests are included) if you have problems with understanding.

Note: i'm the creator of this plugin.

Upvotes: 1

Mark Hong
Mark Hong

Reputation: 11

I have solved that problem by adding below in webpack.config.js

const { CleanWebpackPlugin } = require('clean-webpack-plugin');    
{
  ... your configs ...
  plugins: [new CleanWebpackPlugin()]
}

Upvotes: 1

GProst
GProst

Reputation: 10237

Here is the webpack-clean-obsolete-chunks plugin, which do what you want. It searches for all updated chunks and deletes obsolete files after each webpack compilation.

Upvotes: 15

Arek - Krakiewicz.pl
Arek - Krakiewicz.pl

Reputation: 292

Take a look at this pull request: https://github.com/johnagan/clean-webpack-plugin/pull/32/files

Open raw file view and copy it to index.js of clean webpack plugin. Remember about config flag -> watch: true

Upvotes: 2

arturkin
arturkin

Reputation: 957

There is a clean-webpack-plugin for those purposes, or you can write a simple bash script for npm:

 "scripts": {
    "build": "rm -r dist/* && webpack -p",
    "clean": "rm -r dist/*"
  }

Upvotes: 23

Related Questions