Search code examples
javascriptwebpackwebpack-2html-webpack-plugin

How can I split the JS and CSS into separate HTML files?


I have a very specific requirement where I need to split the JS script tag into one file and the CSS link tag into another file using HtmlWebpackPlugin.

At the moment, both script and link tags are going into both files. Is there a way to do them separately?

Here is my current Webpack file:

import webpack from 'webpack'
import path from 'path'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import ExtractTextPlugin from 'extract-text-webpack-plugin'
import autoprefixer from 'autoprefixer'

const extractCSS = new ExtractTextPlugin({
  filename: 'css/app.bundle.css',
  allChunks: true
})

const createCSSfile = new HtmlWebpackPlugin({
  chunks: ['app'],
  minify: {
    collapseWhitespace: true
  },
  hash: true,
  template: 'src/ejs/css.ejs',
  filename: 'templates/css.php'
})

const createJSfile = new HtmlWebpackPlugin({
  chunks: ['app'],
  minify: {
    collapseWhitespace: true
  },
  hash: true,
  template: 'src/ejs/js.ejs',
  filename: 'templates/js.php'
})

const config = {
  entry: {
    'app': [
      path.resolve(__dirname, 'src/js/app.js'),
      path.resolve(__dirname, 'src/scss/app.scss')
    ]
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/dist',
    filename: 'js/app.bundle.js',
    sourceMapFilename: 'js/app.bundle.map'
  },
  devtool: 'source-map',
  watch: true,
  watchOptions: {
    ignored: /node_modules/,
    aggregateTimeout: 300,
    poll: 1000
  },
  module: {
    rules: [
      {
        test: /\.(png|gif|jpg|jpeg)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '/images/[name].[ext]'
            }
          }
        ]
      },
      {
        test: /\.(eot|ttf|woff|woff2|otf)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '/fonts/[name].[ext]'
            }
          }
        ]
      },
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: [require('@babel/plugin-proposal-object-rest-spread')]
          }
        }
      },
      {
        test: /\.scss$/,
        use: extractCSS.extract([
          {
            loader: 'css-loader'
          },
          {
            loader: 'postcss-loader',
            options: {
              plugins () {
                return [
                  autoprefixer({
                    browsers: [
                      'last 2 versions',
                      'Safari >= 8',
                      'Explorer >= 9',
                      'Android >= 4'
                    ]
                  })
                ]
              }
            }
          },
          {
            loader: 'sass-loader'
          }
        ])
      }
    ]
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        'NODE_ENV': JSON.stringify(process.env.NODE_ENV)
      }
    }),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'js/app.common',
      filename: 'js/app.common.js',
      minChunks: 2
    }),
    createCSSfile,
    createJSfile,
    extractCSS
  ]
}

export default config

Each .ejs file is empty and generates the following inside the .php files:

<head><link href="/dist/css/app.bundle.css?bdba9ec6846a7d92d61f" rel="stylesheet"></head><script type="text/javascript" src="/dist/js/app.bundle.js?bdba9ec6846a7d92d61f"></script>

Is there a way to separate them?

Also, I noticed it is inserting a head tag for the CSS link; is there a way to stop this happening?


Solution

  • With help from @mootrichard I was able to get the answer I needed.

    Steps to take:

    1. Separate JS and CSS into their own entry points.
    2. Set inject: false in HtmlWebpackPlugin configs to stop Webpack doing this.
    3. Reference 'common' in the chunks to make the common JS file available for the templates.
    4. Configure the .ejs templates to loop the files array.

    webpack.config.babel.js

    import webpack from 'webpack'
    import path from 'path'
    import HtmlWebpackPlugin from 'html-webpack-plugin'
    import ExtractTextPlugin from 'extract-text-webpack-plugin'
    import autoprefixer from 'autoprefixer'
    
    const extractCSS = new ExtractTextPlugin({
      filename: 'css/app.bundle.css',
      allChunks: true
    })
    
    const createCSSfile = new HtmlWebpackPlugin({
      chunks: ['css'],
      excludeChunks: ['js', 'common'],
      minify: {
        collapseWhitespace: true,
        preserveLineBreaks: true,
        removeComments: true
      },
      inject: false,
      hash: true,
      template: 'src/ejs/css.ejs',
      filename: 'templates/css.php'
    })
    
    const createJSfile = new HtmlWebpackPlugin({
      chunks: ['js', 'common'],
      excludeChunks: ['css'],
      minify: {
        collapseWhitespace: true,
        preserveLineBreaks: true,
        removeComments: true
      },
      inject: false,
      hash: true,
      template: 'src/ejs/js.ejs',
      filename: 'templates/js.php'
    })
    
    const config = {
      entry: {
        'css': [
          path.resolve(__dirname, 'src/scss/app.scss')
        ],
        'js': [
          path.resolve(__dirname, 'src/js/app.js')
        ]
      },
      output: {
        path: path.resolve(__dirname, 'build'),
        publicPath: '/build',
        filename: 'js/app.bundle.js',
        sourceMapFilename: 'js/app.bundle.map'
      },
      devtool: 'source-map',
      watch: true,
      watchOptions: {
        ignored: /node_modules/,
        aggregateTimeout: 300,
        poll: 1000
      },
      module: {
        rules: [
          {
            test: /\.(png|gif|jpg|jpeg)$/,
            use: [
              {
                loader: 'file-loader',
                options: {
                  name: '/images/[name].[ext]'
                }
              }
            ]
          },
          {
            test: /\.(eot|ttf|woff|woff2|otf)$/,
            use: [
              {
                loader: 'file-loader',
                options: {
                  name: '/fonts/[name].[ext]'
                }
              }
            ]
          },
          {
            test: /\.js$/,
            exclude: /(node_modules|bower_components)/,
            use: {
              loader: 'babel-loader',
              options: {
                presets: ['@babel/preset-env'],
                plugins: [require('@babel/plugin-proposal-object-rest-spread')]
              }
            }
          },
          {
            test: /\.scss$/,
            use: extractCSS.extract([
              {
                loader: 'css-loader'
              },
              {
                loader: 'postcss-loader',
                options: {
                  plugins () {
                    return [
                      autoprefixer({
                        browsers: [
                          'last 2 versions',
                          'Safari >= 8',
                          'Explorer >= 9',
                          'Android >= 4'
                        ]
                      })
                    ]
                  }
                }
              },
              {
                loader: 'sass-loader'
              }
            ])
          }
        ]
      },
      plugins: [
        new webpack.DefinePlugin({
          'process.env': {
            'NODE_ENV': JSON.stringify(process.env.NODE_ENV)
          }
        }),
        new webpack.optimize.CommonsChunkPlugin({
          name: 'common',
          filename: 'js/app.common.js',
          minChunks: 2
        }),
        createCSSfile,
        createJSfile,
        extractCSS
      ]
    }
    
    export default config
    

    js.ejs

    <% for (let i = 0; i < htmlWebpackPlugin.files.js.length; i++) { %>
      <script src="<%= htmlWebpackPlugin.files.js[i] %>"></script>
    <% } %>
    

    css.ejs

    <% for (let i = 0; i < htmlWebpackPlugin.files.css.length; i++) { %>
      <link rel="stylesheet" href="<%= htmlWebpackPlugin.files.css[i] %>">
    <% } %>
    

    Hope this comes to help someone else in future.

    Benefit of this approach

    The reason I needed to separate out JS and CSS into actual separate files was for use in WordPress, where the templates do not have a concept of a 'master' template you inherit from, but instead has basic footer and header includes.

    So if you're using WordPress then this is a pretty good approach to take.