Stop webpack from instantiating module multiple times across different entry points - javascript

I'm generating a static HTML page with Webpack. I have a custom logging module, and then two other modules which import it. Those imports are in different entry points. The problem is, the logging module is actually being instantiated twice.
sendtolog.js:
'use strict';
import { v4 } from "uuid";
console.log('logging...');
const ssid = v4();
export default function sendToLog(metric) {
console.log(`Sending message with ${ssid}`);
}
webvitals.js:
import { getTTFB } from 'web-vitals/base';
import sendToLog from './sendtolog';
getTTFB(sendToLog);
pageactions.js:
'use strict';
import sendToLog from './sendtolog';
sendToLog({name: 'foo', value: 'bar'});
and then in the browser console:
[Log] logging...
[Log] Sending message with 53f50779-d430-49e1-a1be-5b1bb33db10b
[Log] logging...
[Log] Sending message with 415dd4b9-e089-4feb-a4cf-d29c12a26149
How do I get it to not do that?
The WebPack docs for optimization.runtimeChunk have a giant warning:
Imported modules are initialized for each runtime chunk separately, so
if you include multiple entry points on a page, beware of this
behavior. You will probably want to set it to single or use another
configuration that allows you to only have one runtime instance.
but I'm not using runtimeChunk, I'm just using splitChunks, which I assumed would be "another configuration that allows you to only have one runtime instance."
My WebPack config:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = (env, argv) => {
console.log('Webpack mode: ', argv.mode);
const config = {
entry: {
'main': [
path.resolve(__dirname, './src/pageactions.js'),
],
'inline': [ path.resolve(__dirname,
'./node_modules/web-vitals/dist/polyfill.js'),
path.resolve(__dirname, './src/webvitals.js')
]
},
module: {
rules: [
// JavaScript
{
test: /\.(js)$/,
exclude: /node_modules/,
use: ['babel-loader']
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './views/index.ejs'),
filename: path.resolve(__dirname, 'index.html'),
output: {
path: path.resolve(__dirname, './public'),
filename: '[name].bundle.js'
},
optimization: {
splitChunks: {
chunks: 'all'
},
},
devtool: ('production' === argv.mode) ? false : 'eval',
mode: argv.mode
}
return config;
};

Related

Webpack dynamic import each imported module

This may be an x/y question, so here goes!
Background:
I'm trying to run a comparison of two versions of a JS library to measure the benefits of its side-effect free tree-shaking modules.
My plan was to make two .html pages, one with old.js, and another importing specific modules (i.e. import {mod1, mod2} from "new.js")
Webpack Chunk Names
Ideally, I'd like each individual module to be placed into its own chunk so I can document how much each module "weighs".
I see webpack has an option to add /* webpackChunkName: "my-chunk-name" */ inside of an import.
Question:
Is it possible to dynamically import an individual property/module while specifying its name to generate its own chunk?
I've tried using this code below, but it combines them into a single chunk based on the first mod1 chunkname.
document.getElementById('mod1').onclick = function () {
import(/* webpackChunkName: "mod1" */ 'new.js').then(
(lib) => {
lib.mod1()
}
);
};
document.getElementById('mod2')!.onclick = function () {
import(/* webpackChunkName: "mod2" */ 'new.js').then(
(lib) => {
lib.mod2()
}
);
};
webpack.config.js
// Generated using webpack-cli https://github.com/webpack/webpack-cli
import { Configuration } from 'webpack';
import 'webpack-dev-server';
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const isProduction = process.env.NODE_ENV == 'production';
const config = {
// An entry point is the root JS file associated with a HTML route
entry: {
old: './src/old.ts',
new: './src/new.ts',
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
},
devServer: {
open: false,
host: 'localhost',
},
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
const packageName = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)[1];
// npm package names are URL-safe, but some servers don't like # symbols
return `npm.${packageName.replace('#', '')}`;
},
},
},
},
},
plugins: [
new HtmlWebpackPlugin({
// output name (URL path)
filename: 'old.html',
// the template property to the HTML template
template: path.resolve(__dirname, 'src', 'old.html'),
// associate it with one or more of the entry points with the chunks property.
chunks: ['old'],
}),
new HtmlWebpackPlugin({
// output name (URL path)
filename: 'new.html',
// the template property to the HTML template
template: path.resolve(__dirname, 'src', 'new.html'),
// associate it with one or more of the entry points with the chunks property.
chunks: ['new'],
}),
new HtmlWebpackPlugin({
// output name (URL path)
filename: 'index.html',
// the template property to the HTML template
template: path.resolve(__dirname, 'index.html'),
// associate it with one or more of the entry points with the chunks property.
chunks: [],
}),
// Add your plugins here
// Learn more about plugins from https://webpack.js.org/configuration/plugins/
],
performance: {
hints: false,
},
module: {
rules: [
{
test: /\.(ts|tsx)$/i,
loader: 'ts-loader',
exclude: ['/node_modules/'],
},
{
test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i,
type: 'asset',
},
// Add your rules for custom modules here
// Learn more about loaders from https://webpack.js.org/loaders/
],
},
resolve: {
extensions: ['.ts', '.js'],
},
experiments: {
topLevelAwait: true,
},
};
module.exports = () => {
if (isProduction) {
config.mode = 'production';
} else {
config.mode = 'development';
}
return config;
};
I think the reason webpack combines the same module(new.js) into a single chunk is because MergeDuplicateChunksPlugin is used.
Its functionality is very well described by its name and in this situation it can be seen in action: the mod1 and mod2 chunks are using the same new.js module, so they're fundamentally the same.
Fortunately, this plugin is behind a flag and it can be deactivated by modifying your configuration as follows:
config = {
/* ... */
optimization: {
mergeDuplicateChunks: false,
},
/* ... */
}
With the above configuration, you should now see the new.js module being duplicated in two different chunks - mod1 and mod2.

Webpack Hot module replacement, react 18 ReactDOMClient.createRoot() on a container that has already been passed to createRoot() before

I updated React to v18 and my Webpack dev server gives me a console error whenever the hot module replacement fires and injects the new javascript code:
Warning: You are calling ReactDOMClient.createRoot() on a container that has already been passed to createRoot() before. Instead, call root. render() on the existing root instead if you want to update it.
index.js file
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);
root.render(<App />);
if (module.hot) module.hot.accept(function (err) {
console.log('An error occurred while accepting new version');
});
webpack.config.js
const path = require('path');
const HtmlWEbpackPlugin = require('html-webpack-plugin');
module.exports = (env) => {
let cfg = {
mode: 'development',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.[contenthash:6].js',
publicPath: '/',
clean: true
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
]
},
plugins: [new HtmlWEbpackPlugin({ template: './src/index.html' })
],
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
compress: true,
port: 3000,
hot: true,
open: true,
},
performance: {
hints: false
}
}
return cfg;
};
React 18 has native support for hot reloading, this is called Fast Refresh. An excerpt from the react-refresh readme:
Fast Refresh is a feature that lets you edit React components in a running application without losing their state. It is similar to an old feature known as "hot reloading", but Fast Refresh is more reliable and officially supported by React.
To use this with Webpack 5, you will need a plugin called react-refresh-webpack-plugin. To get it to work I would recommend taking a look at the examples included in the git repository, especially the webpack-dev-server example.
NOTE: As of writing this answer, the react-refresh-webpack-plugin is in an experimental state but create-react-app is already using it, so it is probably stable enough to use.
The following is taken straight from react-refresh-webpack-plugin's webpack-dev-server example:
src/index.js
import { createRoot } from 'react-dom/client';
import App from './App';
const container = document.getElementById('app');
const root = createRoot(container);
root.render(<App />);
webpack.config.js
const path = require('path');
const ReactRefreshPlugin = require('#pmmmwh/react-refresh-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const isDevelopment = process.env.NODE_ENV !== 'production';
module.exports = {
mode: isDevelopment ? 'development' : 'production',
devServer: {
client: { overlay: false },
},
entry: {
main: './src/index.js',
},
module: {
rules: [
{
test: /\.jsx?$/,
include: path.join(__dirname, 'src'),
use: 'babel-loader',
},
],
},
plugins: [
isDevelopment && new ReactRefreshPlugin(),
new HtmlWebpackPlugin({
filename: './index.html',
template: './public/index.html',
}),
].filter(Boolean),
resolve: {
extensions: ['.js', '.jsx'],
},
};
babel.config.js
module.exports = (api) => {
// This caches the Babel config
api.cache.using(() => process.env.NODE_ENV);
return {
presets: [
'#babel/preset-env',
// Enable development transform of React with new automatic runtime
['#babel/preset-react', { development: !api.env('production'), runtime: 'automatic' }],
],
// Applies the react-refresh Babel plugin on non-production modes only
...(!api.env('production') && { plugins: ['react-refresh/babel'] }),
};
};
You can remove the following from your Webpack entry point:
src/index.js
// ...
if (module.hot) {
module.hot.accept()
}
This has the small drawback that whenever you modify your entry point (src/index.js) a full reload is necessary. Webpack is very in your face about needing to do a full reload, showing you the following log messages.
This really annoyed me. When looking at how create-react-app solved this, I found that they disabled client side logging for the webpack-dev-server, or at least set the log level to warn or error. You can set the log level by setting the client.logging property in the devServer configuration:
webpack.config.js
// ...
devServer: {
client: {
overlay: false,
logging: 'warn' // Want to set this to 'warn' or 'error'
}
}
// ...
What the odd thing is about the "warning", is that it is not a warning at all, it is just an info message dressed up as a warning.
Hope this helps.

Wrong URL for images with React + Typescript + WebPack

I'm referencing an image inside my .JSX file but the generated URL is wrong.
It looks like this : http://localhost:43124/dist/dist/9ee7eb54c0eb428bb30b599ef121fe25.jpg
The folder "dist" exists with the picture but not "dist/dist". I think the problem comes from my Webpack.config.js. Here are the files :
module.d.ts
I instruct Typescript what to do with image files as mentionned here.
declare module '*.jpg'
declare module '*.svg'
Layout.tsx
I reference my logo inside React so it can be packed by Webpack.
/// <reference path="./module.d.ts"/>
import * as React from 'react';
import logo from '../img/logo.svg';
export class Layout extends React.Component<{}, {}> {
public render() {
return <img src="{logo}" width="220" alt="logo" />
}
}
webpack.config.js
const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin;
const merge = require('webpack-merge');
module.exports = (env) => {
const isDevBuild = !(env && env.prod);
// Configuration in common to both client-side and server-side bundles
const sharedConfig = () => ({
stats: { modules: false },
resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx'] },
output: {
filename: '[name].js',
publicPath: 'dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix
},
module: {
rules: [
{ test: /\.tsx?$/, include: /ClientApp/, use: 'awesome-typescript-loader?silent=true' },
{ test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=25000' }
]
},
plugins: [new CheckerPlugin()]
});
// Configuration for client-side bundle suitable for running in browsers
const clientBundleOutputDir = './wwwroot/dist';
const clientBundleConfig = merge(sharedConfig(), {
entry: { 'main-client': './ClientApp/boot-client.tsx' },
module: {
rules: [
{ test: /\.css$/, use: ExtractTextPlugin.extract({ use: isDevBuild ? 'css-loader' : 'css-loader?minimize' }) }
]
},
output: { path: path.join(__dirname, clientBundleOutputDir) },
plugins: [
new ExtractTextPlugin('style.css'),
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./wwwroot/dist/vendor-manifest.json')
})
].concat(isDevBuild ? [
// Plugins that apply in development builds only
new webpack.SourceMapDevToolPlugin({
filename: '[file].map', // Remove this line if you prefer inline source maps
moduleFilenameTemplate: path.relative(clientBundleOutputDir, '[resourcePath]') // Point sourcemap entries to the original file locations on disk
})
] : [
// Plugins that apply in production builds only
new webpack.optimize.UglifyJsPlugin()
])
});
return [clientBundleConfig];
};
I used the default Visual Studio ASP.NET Core React + Redux template.

How to webpack .cshtml views as templateUrl in Angular 2

I have setup my Angular 2 project in .NET Core solution and I have a situation where I need to use .cshtml view files to be rendered from server and use them as templates in Angular components. I am using webpack to AOT bundle them.
How can I exclude templateUrl or template to be excluded (not to be compiled into output .js file) but rather resolved on the fly?
My lazy load component (notfound.component.ts):
import { Component } from "#angular/core";
#Component({
templateUrl: "/Home/PageNotFound" // This is my Controller/Action
})
export class NotFoundComponent
{
}
webpack.config.js:
var webpack = require("webpack");
var clean = require("clean-webpack-plugin");
var compression = require("compression-webpack-plugin");
var path = require("path");
var analyzer = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
var HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
"app": "./Client/main-aot.ts" // AoT compilation
},
devtool: "source-map",
output: {
path: __dirname + "/wwwroot/dist/bundle",
//filename: "[name]-[hash:8].bundle.js",
filename: "[name].js",
chunkFilename: "[id]-[hash:8].chunk.js",
publicPath: "/dist/bundle/",
},
resolve: {
extensions: [".ts", ".js"]
},
module: {
rules: [
{
test: /\.ts$/,
use: [
"awesome-typescript-loader",
"angular-router-loader?aot=true&genDir=aot/"
]
}
],
exprContextCritical: false
},
plugins: [
new clean(
[
__dirname + "/wwwroot/dist/bundle"
]
),
new analyzer(),
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
output: {
comments: false
},
sourceMap: true
}),
new compression({
asset: "[path].gz[query]",
algorithm: "gzip",
test: /\.js$|\.html$/,
threshold: 10240,
minRatio: 0.8
})
]
};
When I run following NPM command, its giving me error:
"node_modules/.bin/ngc -p tsconfig-aot.json" && webpack --configebpack.config.js
Error: Compilation failed. Resource file not found:
C:/Users/Saad/Documents/Visual Studio 2017/Projects/HelloAngular/HelloAngular/Client/notfound/dist/template/notfound/notfound.html

What is the best practice for importing angularjs using webpack?

How do you use Webpack and AngularJS together, and how about template loading and on demand fetching of resources?
An example of a well written webpack.config.js file for this purpose would be very much appreciated.
All code snippets displayed here can be accessed at this github repo. Code has been generously adapted from this packetloop git repo.
webpack.config.json
var path = require('path');
var ResolverPlugin = require("webpack/lib/ResolverPlugin");
var config = {
context: __dirname,
entry: ['webpack/hot/dev-server', './app/app.js'],
output: {
path: './build',
filename: 'bundle.js'
},
module: {
loaders: [{
test: /\.css$/,
loader: "style!css-loader"
}, {
test: /\.scss$/,
loader: "style!css!sass?outputStyle=expanded"
}, {
test: /\.jpe?g$|\.gif$|\.png$|\.svg$|\.woff$|\.ttf$/,
loader: "file"
}, {
test: /\.html$/,
loader: "ngtemplate?relativeTo=" + path.join(__dirname, 'app/') + "!raw"
}]
},
// Let webpack know where the module folders are for bower and node_modules
// This lets you write things like - require('bower/<plugin>') anywhere in your code base
resolve: {
modulesDirectories: ['node_modules', 'lib/bower_components'],
alias: {
'npm': __dirname + '/node_modules',
'vendor': __dirname + '/app/vendor/',
'bower': __dirname + '/lib/bower_components'
}
},
plugins: [
// This is to help webpack know that it has to load the js file in bower.json#main
new ResolverPlugin(
new ResolverPlugin.DirectoryDescriptionFilePlugin("bower.json", ["main"])
)
]
};
module.exports = config;
To import AngularJS into the main app.js you do the following:
app/vendor/angular.js
'use strict';
if (!global.window.angular) {
require('bower/angular/angular');
}
var angular = global.window.angular;
module.exports = angular;
And then use it in app.js like so,
app.js
...
var angular = require('vendor/angular');
// Declare app level module
var app = angular.module('myApp', []);
...
Is the following correct? Is there an easier way to do this? I've seen a few (not a lot by any standards) posts which listed another method.
From this reddit post comment
// Add to webpack.config.js#module#loaders array
{
test: /[\/]angular\.js$/,
loader: "exports?angular"
}
There is also another plugin which is in development right now, at stackfull/angular-seed. It seems to be in the right direction, but is really really hard to use right now.
Webpack is way awesome, but the lack of documentation and samples are killing it.
You can just require angular in all modules (files) where you need it. I have a github repository with example how to do that (also using webpack for build). In the example ES6 import syntax is used but it shouldnt matter, you can use standard require() instead.
Example:
import 'bootstrap/dist/css/bootstrap.min.css';
import './app.css';
import bootstrap from 'bootstrap';
import angular from 'angular';
import uirouter from 'angular-ui-router';
import { routing} from './app.config';
import common from './common/common.module';
import featureA from './feature-a/feature-a.module';
import featureB from './feature-b/feature-b.module';
const app = angular
.module('app', [uirouter, common, featureA, featureB])
.config(routing);
I am starting with Angular + Flux with Webpack so may be I can help you with some things.
Basically I am installing everything with NPM, it has module export system, so it works like nothing. (You can use export-loader, but why if you do not need to.)
My webpack.config.js looks like this:
var webpack = require('webpack');
var path = require('path');
var HtmlWebpackPlugin = require("html-webpack-plugin");
var nodeModulesDir = path.resolve(__dirname, './node_modules');
// Some of my dependencies that I want
// to skip from building in DEV environment
var deps = [
'angular/angular.min.js',
...
];
var config = {
context: path.resolve(__dirname, './app'),
entry: ['webpack/hot/dev-server', './main.js'],
resolve: {
alias: {}
},
output: {
path: path.resolve(__dirname, './build'),
filename: 'bundle.js'
},
// This one I am using to define test dependencies
// directly in the modules
plugins: [
new webpack.DefinePlugin({
ON_TEST: process.env.NODE_ENV === 'test'
})
],
module: {
preLoaders: [
{test: /\.coffee$/, loader: "coffeelint", exclude: [nodeModulesDir]}
],
loaders: [
{test: /\.js$/, loader: 'ng-annotate', exclude: [nodeModulesDir]},
{test: /\.coffee$/, loader: 'coffee', exclude: [nodeModulesDir]},
...
],
noParse: []
},
devtool: 'source-map'
};
if (process.env.NODE_ENV === 'production') {
config.entry = {
app: path.resolve(__dirname, './app/main.js'),
vendors: ['angular']
};
// config.output.path = path.resolve(__dirname, './dist');
config.output = {
path: path.resolve(__dirname, "./dist"),
filename: "app.[hash].js",
hash: true
};
config.plugins.push(new webpack.optimize.UglifyJsPlugin());
config.plugins.push(new webpack.optimize.CommonsChunkPlugin('vendors', 'vendors.[hash].js'));
config.plugins.push(new HtmlWebpackPlugin({
title: 'myApp',
template: path.resolve(__dirname, './app/index.html'),
inject: 'body'
}));
delete config.devtool;
}
else {
deps.forEach(function (dep) {
var depPath = path.resolve(nodeModulesDir, dep);
config.resolve.alias[dep.split(path.sep)[0]] = depPath;
config.module.noParse.push(depPath);
});
}
module.exports = config;
My main.js looks like this:
var angular = require('angular');
if(ON_TEST) {
require('angular-mocks/angular-mocks');
}
require('./index.coffee');
And index.coffee containt main angular module:
ngModule = angular.module 'myApp', []
require('./directive/example.coffee')(ngModule)

Categories