Webpack successfully builds, but JavaScript doesn’t execute runtime

Tags: , , , ,



I have migrated Webpack v4 to v5, made changes relative to documentation and deprecation messages from CLI, and got successful build, though during the application testing I noticed that JavaScript doesn’t run and there are no errors. I am getting plain html rendered by SSR.

Have no clue why it doesn’t work, because it’s silent, maybe misconfiguration issue.

Here is my webpack configuration:

const webpack = require('webpack');
const path = require('path');
const glob = require('glob');
// webpack plugins
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const AssetsPlugin = require('assets-webpack-plugin');
// minification plugins
const TerserJSPlugin = require('terser-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
// image optimization plugins
const ImageminPlugin = require("imagemin-webpack-plugin").default;
const imageminGifsicle = require("imagemin-gifsicle");
const imageminPngquant = require("imagemin-pngquant");
const imageminSvgo = require("imagemin-svgo");
const imageminMozjpeg = require('imagemin-mozjpeg');

const ProgressPlugin = webpack.ProgressPlugin;

const env = require('dotenv').config();

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

const environment = {
    NODE_ENV: process.env.NODE_ENV || 'development',
    CONFIG: process.env.CONFIG || 'development',
    DEBUG: process.env.DEBUG || false,
};

const plugins = () => {
    const plugins = [
        new CleanWebpackPlugin({
            verbose: true,
            cleanStaleWebpackAssets: true,
        }),
        new webpack.EnvironmentPlugin(Object.assign(environment, env.parsed)),
        new MiniCssExtractPlugin({
            filename: '[contenthash:5].[name].css',
            chunkFilename: '[chunkhash:5].[name].css',
        }),
        new webpack.ProvidePlugin({
            $: 'jquery',
            jQuery: 'jquery',
            moment: 'moment',
            _: 'lodash',
        }),
        new webpack.ContextReplacementPlugin(/moment[\/]locale$/, /^./(en)$/),
        new AssetsPlugin({
            filename: 'assets.json',
        }),
        new ImageminPlugin({
            cacheFolder: isDev ? path.resolve(__dirname, '../dist/cachedImages') : null,
            externalImages: {
                context: 'src',
                sources: glob.sync('src/assets/img/**/*.*'),
                destination: 'dist/img/',
                fileName: (filepath) => {
                    let name = filepath.split(/img(/|\)/).pop();
                    return name;
                },
            },
            plugins: [
                imageminGifsicle({
                    interlaced: false
                }),
                imageminMozjpeg({
                    progressive: true,
                    arithmetic: false
                }),
                imageminPngquant({
                    floyd: 0.5,
                    speed: 2
                }),
                imageminSvgo({
                    plugins: [
                        { removeTitle: true },
                        { convertPathData: false }
                    ]
                }),
            ],
        }),
    ];

    if (isProd) {
        plugins.push(
            new ProgressPlugin({
                entries: true,
                handler: (percentage, msg, ...args) => {
                    let moduleName = '';
                    if (args[2]) {
                        let sep = args[2].split(path.sep);
                        let dirIndex = sep.indexOf('finstead-client');
                        moduleName = sep.slice(dirIndex).join('/');
                    }

                    const currentWork = args[0] ? ` ${args[0]}` : '';
                    console.log(`[Progress${currentWork}]: (${(percentage * 100).toFixed(3)}%) ${msg} ${moduleName}`)
                }
            }),
            new BundleAnalyzerPlugin({
                analyzerMode: 'static',
                reportFilename: path.resolve(__dirname, 'analysis.html'),
                generateStatsFile: false,
                logLevel: 'info',
            })
        );
    }

    return plugins;
};

const optimization = () => {
    let optimizations = {
        moduleIds: isProd ? 'deterministic' : 'named',
        chunkIds: isProd ? 'deterministic' : 'named',
        splitChunks: {
            chunks: 'all',
            cacheGroups: {
                vendor: {
                    test: /[\/]node_modules[\/](react|react-dom|lodash|moment)[\/]/,
                },
            }
        }
    };

    if (isProd) {
        optimizations.minimizer = [
            new TerserJSPlugin({
                terserOptions: {
                    compress: {
                        pure_funcs: ['console.log'],
                        drop_console: true,
                        drop_debugger: true
                    },
                    warnings: false
                },
                parallel: true
            }),
            new OptimizeCSSAssetsPlugin({})
        ];
    }

    return optimizations;
};

const fontLoaders = [
    {
        test: /.woff(?v=d+.d+.d+)?$/,
        use: {
            loader: 'url?limit=10000&mimetype=application/font-woff',
        }
    },
    {
        test: /.woff2(?v=d+.d+.d+)?$/,
        use: {
            loader: 'url?limit=10000&mimetype=application/font-woff',
        }
    },
    {
        test: /.ttf(?v=d+.d+.d+)?$/,
        use: {
            loader: 'url?limit=10000&mimetype=application/octet-stream',
        }
    },
    {
        test: /.svg(?v=d+.d+.d+)?$/,
        use: {
            loader: 'url?limit=10000&mimetype=image/svg+xml',
        }
    },
    {
        test: /.eot(?v=d+.d+.d+)?$/,
        use: {
            loader: 'file-loader',
        }
    }
];

const config = {
    mode: process.env.NODE_ENV,
    devtool: isProd ?  false : 'eval-source-map',
    context: path.resolve(__dirname, '../src'),
    entry: {
        bundle: {
            import: './app.jsx',
            dependOn: 'vendor',
        },
        styles: './sass/main.scss',
        vendor: {
             runtime: 'runtimecode',
             import: [
            'react',
            'react-dom',
            'redux',
            'redux-saga',
            'react-redux',
            'react-router',
            'react-tap-event-plugin',
            'lodash',
            'moment',
             ]
        }
    },
    output: {
        publicPath: '/build/',
        filename: '[name].[contenthash].js',
        chunkFilename: '[name].[contenthash].js',
        path: path.resolve(__dirname, '../dist/build'),
    },
    resolve: {
        extensions: ['.js', '.jsx', '.json'],
        alias: {
            config: path.resolve(__dirname, '../config.js'),
            utils: path.resolve(__dirname, '../src/utils'),
            shared: path.resolve(__dirname, '../src/components/shared'),
            services: path.resolve(__dirname, '../src/services'),
            store: path.resolve(__dirname, '../src/store'),
            constants: path.resolve(__dirname, '../src/constants'),
            actions: path.resolve(__dirname, '../src/actions'),
            components: path.resolve(__dirname, '../src/components'),
        },
    },
    optimization: optimization(),
    plugins: plugins(),
    module: {
        rules: [
            {
                test: /.m?jsx?$/,
                resolve: {
                    fullySpecified: false
                },
                // ignore transpiling JavaScript from node_modules as they should be ready to go OOTB
                exclude: /node_modules/,
            },
            {
                test: /.jsx?$/,
                exclude: [/node_modules/, /libs/],
                use: {
                    loader: path.join(__dirname, '../helpers/custom-loader.js'),
                    options: {
                        presets: ['@babel/preset-env', '@babel/preset-react'],
                        plugins: [
                            '@babel/plugin-proposal-object-rest-spread',
                            '@babel/plugin-proposal-class-properties',
                            '@babel/plugin-transform-destructuring',
                            '@babel/plugin-syntax-dynamic-import',
                            '@babel/plugin-transform-runtime',
                            'syntax-async-functions'
                        ],
                        cacheDirectory: true
                    }
                }
            },
            {
                test: /.scss$/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader
                    },
                    {
                        loader: 'css-loader',
                        options: {
                            url: false,
                        }
                    },
                    {
                        loader: 'sass-loader',
                        options: {
                            implementation: require('node-sass'),
                        }
                    },
                ]
            },
            {
                test: /.css$/,
                use: ['style-loader', 'css-loader']
            },
            {
                test: /.(jpe?g|png|gif|svg)$/i,
                loader: "url-loader",
                options: {
                    name: "[path][name].[ext]"
                },
            },
            ...fontLoaders
        ]
    },
    watchOptions: {
        ignored: /node_modules/,
    },
};

module.exports = config;

Here the dev dependencies that I use

"devDependencies": {
    "@babel/cli": "^7.12.10",
    "@babel/core": "^7.12.10",
    "@babel/node": "^7.12.10",
    "@babel/plugin-proposal-class-properties": "^7.12.1",
    "@babel/plugin-proposal-decorators": "^7.12.12",
    "@babel/plugin-proposal-export-namespace-from": "^7.12.1",
    "@babel/plugin-proposal-function-sent": "^7.12.1",
    "@babel/plugin-proposal-json-strings": "^7.12.1",
    "@babel/plugin-proposal-numeric-separator": "^7.12.7",
    "@babel/plugin-proposal-object-rest-spread": "^7.12.1",
    "@babel/plugin-proposal-throw-expressions": "^7.12.1",
    "@babel/plugin-syntax-dynamic-import": "^7.8.3",
    "@babel/plugin-syntax-import-meta": "^7.10.4",
    "@babel/plugin-transform-destructuring": "^7.12.1",
    "@babel/plugin-transform-runtime": "^7.12.10",
    "@babel/preset-env": "^7.12.11",
    "@babel/preset-react": "^7.12.10",
    "@babel/register": "^7.12.10",
    "@babel/runtime": "^7.12.0",
    "@webpack-cli/migrate": "^1.1.2",
    "assets-webpack-plugin": "^7.0.0",
    "babel-eslint": "^10.0.3",
    "babel-loader": "^8.2.2",
    "babel-plugin-dynamic-import-webpack": "1.0.1",
    "babel-plugin-lodash": "^3.3.4",
    "babel-plugin-syntax-async-functions": "^6.13.0",
    "babel-plugin-system-import-transformer": "^3.1.0",
    "babel-plugin-transform-ensure-ignore": "^0.1.0",
    "babel-plugin-webpack-alias": "^2.1.2",
    "babel-watch": "^7.3.0",
    "clean-webpack-plugin": "^3.0.0",
    "cross-env": "^5.2.0",
    "css-loader": "^5.0.1",
    "eslint": "^6.8.0",
    "eslint-config-airbnb": "^18.2.1",
    "eslint-import-resolver-alias": "^1.1.2",
    "eslint-plugin-import": "^2.22.0",
    "eslint-plugin-jsx-a11y": "^6.4.1",
    "eslint-plugin-react": "^7.22.0",
    "eslint-plugin-react-hooks": "^1.7.0",
    "file-loader": "^1.1.11",
    "imagemin-mozjpeg": "^9.0.0",
    "imagemin-pngquant": "^9.0.1",
    "imagemin-svgo": "^8.0.0",
    "imagemin-webpack-plugin": "^2.4.2",
    "imports-loader": "^0.7.0",
    "mini-css-extract-plugin": "^1.3.3",
    "nodemon": "^2.0.7",
    "optimize-css-assets-webpack-plugin": "^5.0.4",
    "redux-devtools": "^3.7.0",
    "redux-devtools-dock-monitor": "^1.2.0",
    "redux-devtools-log-monitor": "^1.2.0",
    "resolve-url-loader": "^2.3.2",
    "sass-loader": "^10.1.0",
    "terser-webpack-plugin": "^5.0.3",
    "webpack": "^5.11.1",
    "webpack-bundle-analyzer": "^4.3.0",
    "webpack-cli": "^4.3.1"
  },

EDIT

I am suing assets-webpack-plugin as you can see, which gives me JSON of entrypoints and chunks. I am manually inserting the bundle.[contenthash].js and vendor.[contenthash].js to my index.html which loads successfully.

But in source code of compiled bundles, I can see that bundle isn’t runtime executable function, but webpack module.

I have tried runtime property on vendor entrypoint, because main bundle point is depended on vendor packages, even though it gave me executable function it doesn’t execute my main bundle code.

Answer

After more investigation I found, that besides entry bundles and runtime file, webpack creates few more files needed for initial execution.

So for making custom html file or using any other preprocessor file we need to use either html-webpack-plugin to generate html file based on optionally provided template or webpack-manifest-plugin to get the list of the needed files in json file.

...
plugins: [
    new WebpackManifestPlugin({
        fileName: path.resolve(__dirname, './manifest.json'), 
        filter: (file) => file.isInitial,
    }),
]
...

Here with available filter option we process files. filter function provides FileDescriptor object as an input for processing which containes isInitial, isChunk and few more fields which you can find in documentation

As you already guessed the files marked as isInitial needed for running your application. Also you can generate custom manifest file using this plugin. For example I have a file where have separated initial css and js bundles.

{
  "css": [
    "/build/c3a7e.commons.css"
  ],
  "js": [
    "/build/bundle.bf972748425cb97699f3.js",
    "/build/styles.09e2bc2ae43dc685a2c5.js",
    "/build/vendor.8e55b287c8b3ffc5c578.js",
    "/build/runtime.d9173e4531e516f18fc8.js",
    "/build/commons-app_jsx.1fc7ae7feba211c7a59d.js",
    "/build/vendor-node_modules_lodash_lodash_js-node_modules_moment_moment_js-node_modules_react-dom_index_js.fc6e3899340455918b6a.js",
    "/build/commons-node_modules_moment_locale_sync_recursive_en_-node_modules_react-redux_es_index_js-no-b2e9a0.a38dfe96c879814994af.js"
  ]
}


Source: stackoverflow