Search code examples
webpackwebpack-hmr

Webpack hmr running in Production


During development of SPA, hmr works great.

However, when published, hmr should not be running. Yet it is, and it produces a stream of 404 errors. Why is this? I don't see what I am doing wrong.

When I package for production, this is the command line (I am running this from the Visual Studio Task Runner):

cmd /c SET NODE_ENV=production&& webpack --config webpack.netcore.config.js

webpack.netcore.config.js

const webpackConfig = require('./webpack.config');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
var originalConfig = webpackConfig({});

module.exports = () => {
  let config = originalConfig;
  // output files without hashes
  config.output.filename = '[name].bundle.js';
  config.plugins.splice(config.plugins.indexOf(HtmlWebpackPlugin));
  config.plugins = [
    // first clean the output directory
    new CleanWebpackPlugin([config.output.path]),
    ...config.plugins
  ];

  return config;
};

webpack.config.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const project = require('./aurelia_project/aurelia.json');
const { AureliaPlugin, ModuleDependenciesPlugin } = require('aurelia-webpack-plugin');
const { optimize: { CommonsChunkPlugin, UglifyJsPlugin }, ProvidePlugin } = require('webpack');
const { TsConfigPathsPlugin, CheckerPlugin } = require('awesome-typescript-loader');

// config helpers:
const ensureArray = (config) => config && (Array.isArray(config) ? config : [config]) || [];
const when = (condition, config, negativeConfig) =>
  condition ? ensureArray(config) : ensureArray(negativeConfig);

// primary config:
const title = 'Aurelia Navigation Skeleton';
const outDir = path.resolve(__dirname, project.platform.output);
const srcDir = path.resolve(__dirname, 'src');
const nodeModulesDir = path.resolve(__dirname, 'node_modules');
const baseUrl = '/';

const cssRules = [
  { loader: 'css-loader' },
];

module.exports = ({production, server, extractCss, coverage} = {}) => ({
  resolve: {
    extensions: ['.ts', '.js'],
    modules: [srcDir, 'node_modules'],
  },
  entry: { 
    app: ['aurelia-bootstrapper'],
    vendor: ['bluebird','aurelia-syncfusion-bridge'],
  },
  output: {
    path: outDir,
    publicPath: baseUrl,
    filename: production ? '[name].[chunkhash].bundle.js' : '[name].[hash].bundle.js',
    sourceMapFilename: production ? '[name].[chunkhash].bundle.map' : '[name].[hash].bundle.map',
    chunkFilename: production ? '[name].[chunkhash].chunk.js' : '[name].[hash].chunk.js'
  },
  devServer: {
    contentBase: outDir,
    // serve index.html for all 404 (required for push-state)
    historyApiFallback: true
  },
  devtool: production ? 'nosources-source-map' : 'cheap-module-eval-source-map',
  module: {
    rules: [
      // CSS required in JS/TS files should use the style-loader that auto-injects it into the website
      // only when the issuer is a .js/.ts file, so the loaders are not applied inside html templates
      {
        test: /\.css$/i,
        issuer: [{ not: [{ test: /\.html$/i }] }],
        use: extractCss ? ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: cssRules
        }) : ['style-loader', ...cssRules],
      },
      {
        test: /\.css$/i,
        issuer: [{ test: /\.html$/i }],
        // CSS required in templates cannot be extracted safely
        // because Aurelia would try to require it again in runtime
        use: cssRules
      },
      { test: /\.html$/i, loader: 'html-loader' },
      { test: /\.ts$/i, loader: 'awesome-typescript-loader', exclude: nodeModulesDir },
      { test: /\.json$/i, loader: 'json-loader' },
      // use Bluebird as the global Promise implementation:
      { test: /[\/\\]node_modules[\/\\]bluebird[\/\\].+\.js$/, loader: 'expose-loader?Promise' },
      // embed small images and fonts as Data Urls and larger ones as files:
      { test: /\.(png|gif|jpg|cur)$/i, loader: 'url-loader', options: { limit: 8192 } },
      { test: /\.woff2(\?v=[0-9]\.[0-9]\.[0-9])?$/i, loader: 'url-loader', options: { limit: 10000, mimetype: 'application/font-woff2' } },
      { test: /\.woff(\?v=[0-9]\.[0-9]\.[0-9])?$/i, loader: 'url-loader', options: { limit: 10000, mimetype: 'application/font-woff' } },
      // load these fonts normally, as files:
      { test: /\.(ttf|eot|svg|otf)(\?v=[0-9]\.[0-9]\.[0-9])?$/i, loader: 'file-loader' },
      ...when(coverage, {
        test: /\.[jt]s$/i, loader: 'istanbul-instrumenter-loader',
        include: srcDir, exclude: [/\.{spec,test}\.[jt]s$/i],
        enforce: 'post', options: { esModules: true },
      })
    ]
  },
    plugins: [

    new AureliaPlugin(),
    new ProvidePlugin({
      'Promise': 'bluebird'
    }),
    new ModuleDependenciesPlugin({
      'aurelia-testing': [ './compile-spy', './view-spy' ]
    }),
    new ModuleDependenciesPlugin({
        "aurelia-orm": [
            "./component/association-select",
            "./component/view/bootstrap/association-select.html",
            "./component/view/bootstrap/paged.html",
            "./component/paged"],
        "aurelia-authentication": ["./authFilterValueConverter"]
    }),
    new TsConfigPathsPlugin(),
    new CheckerPlugin(),
    new HtmlWebpackPlugin({
      template: 'index.ejs',
      metadata: {
        // available in index.ejs //
        title, server, baseUrl
      }
    }),
    ...when(extractCss, new ExtractTextPlugin({
      filename: production ? '[contenthash].css' : '[id].css',
      allChunks: true
    })),
    ...when(production, new CommonsChunkPlugin({
      name: ['common']
    })),
    ...when(production, new CopyWebpackPlugin([
      { from: 'static/favicon.ico', to: 'favicon.ico' }
    ])),
    ...when(production, new UglifyJsPlugin({
      sourceMap: true
    }))
  ]
});

The 404 error looks like this: https://trkn.app/__webpack_hmr


Solution

  • The only two reasons for HMR running in production are:

    1. If you see it on a server log, it means that a dev version of the app is still open in a browser and hitting the server.

    2. If you see it on the client, it means that the production environment was not actually set when building with webpack.

    This is code that I have in webpack.config.js that sets the environment, and then prints the environment to the console that I am sure the correct environment is selected. (Note that this code is specific to my dev set up (VS17 + Taskrunner) so your mileage may vary.

    const path = require("path");
    const webpack = require("webpack");
    const { AureliaPlugin, ModuleDependenciesPlugin, GlobDependenciesPlugin } = require("aurelia-webpack-plugin");
    const bundleOutputDir = "./wwwroot/dist";
    const MiniCssExtractPlugin = require("mini-css-extract-plugin");
    
    module.exports = (env, args) => {
        let isDevBuild = true;  //Assume isDevBuild;
    
        //If being run from NPM, args.mode will be populated
        if (args && args.mode === 'production') {
            isDevBuild = false;
        }
    
        //Not production mode from NPM, check on Production mode from Task Runner
        if (isDevBuild) {
            //If being run from the Webpack Task Runner in VS.
            const node_env = process.env.NODE_ENV
    
            if (node_env) {
                if (node_env === 'production') {
                    isDevBuild = false;
                }
                else {
                }
            }
        }
        //isDevBuild = true;//Uncomment to test the Prod Build
        console.log('isDevBuild=' + isDevBuild);
        const cssLoader = { loader: isDevBuild ? "css-loader" : "css-loader?minimize" };
        return [{
            target: "web",
            mode: isDevBuild ? "development" : "production",
            entry: { "app": ["es6-promise/auto", "aurelia-bootstrapper"] },
            resolve: {
                extensions: [".ts", ".js"],
                modules: ["ClientApp", "node_modules"]
            },
            output: {
                path: path.resolve(bundleOutputDir),
                publicPath: "/dist/",
                filename: "[name].js",
                chunkFilename: "[name].js"
            },
            module: {
                rules: [
                    { test: /\.(woff|woff2)(\?|$)/, loader: "url-loader?limit=1" },
                    { test: /\.(png|eot|ttf|svg|gif|cur)(\?|$)/, loader: "url-loader?limit=100000" },
                    { test: /\.ts$/i, include: [/ClientApp/, /node_modules/], use: "awesome-typescript-loader" },
                    { test: /\.html$/i, use: "html-loader" },
                    {   test: /\.css$/, include: [/node_modules/], use: [
                            {
                                loader: MiniCssExtractPlugin.loader
                            },
                            "css-loader"
                        ]
                    },
                    {
                        test: /\.css$/, exclude: [/node_modules/], use: [
                            {
                                loader: MiniCssExtractPlugin.loader
                            },
                            "css-loader"
                        ]
                    },
                    { test: /\.scss$/i, issuer: /(\.html|empty-entry\.js)$/i, use: [cssLoader, "sass-loader"] },
                    { test: /\.scss$/i, issuer: /\.ts$/i, use: ["style-loader", cssLoader, "sass-loader"] }
                ]
            },
            optimization: {
                splitChunks: {
                    cacheGroups: {
                        commons: {
                            test: /[\\/]node_modules[\\/]/,
                            name: "vendor",
                            chunks: "all"
                        }
                    }
                }
            },
            devtool: isDevBuild ? "source-map" : false,
            performance: {
                hints: false
            },
            plugins: [
                new webpack.DefinePlugin({ IS_DEV_BUILD: JSON.stringify(isDevBuild) }),
                new webpack.ProvidePlugin({ $: "jquery", jQuery: "jquery", "window.jQuery": "jquery" }),
                new webpack.ProvidePlugin({
                    'Promise': 'bluebird'
                }),
    
                new AureliaPlugin({ aureliaApp: "boot" }),
                new GlobDependenciesPlugin({ "boot": ["ClientApp/**/*.{ts,html}"] }),
                new ModuleDependenciesPlugin({}),
                //extractCSS,
                new ModuleDependenciesPlugin({
                    "aurelia-orm": [
                        "./component/association-select",
                        "./component/view/bootstrap/association-select.html",
                        "./component/view/bootstrap/paged.html",
                        "./component/paged"],
                    "aurelia-authentication": ["./authFilterValueConverter"]
                }),
    
                    new MiniCssExtractPlugin({
                        // Options similar to the same options in webpackOptions.output
                        // both options are optional
                        filename: "[name].css",
                        chunkFilename: "[id].css"
                })
    
            ]
        }];
    };
    

    Also note that there are dev/prod features that are set based on the isDevBuild variable. So in addition to setting the environment, you have to set the options accordingly.