sharing react between microfrontend bundles without CDN - reactjs

I am building an app using a microfrontends architecture with the Single-Spa framework.
I have:
A root config web app: Defines the layout of the apps and includes each microfrontend as an NPM dependency
N Microfrontends: React apps which are loaded into the root config app.
Because each microfrontend is using React, I don't want to bundle it everytime. Instead I want to have the root config app specify React as a dependency and then it can be loaded by each microfrontend.
I have declared react and react-dom as webpack externals so they are not bundled but I think this will only work if I include React from CDN in my index.html of the root config app. Is there a way to configure webpack so that it will include React in the root config and make it available to every microfrontend? I can't use public CDNs in my firm.
Thanks!

In the "Getting Started: Quick start" guide, we cover how to add shared dependencies, and the example specifically shows adding React and ReactDOM to the import map.
You should not try to bundle React into the root-config, since modules bundled into an application by Webpack are namespaced and not shared globally. Instead, per your requirement, you can host those React files in your own CDN or even serve them locally though the root-config because ultimately there are static files. The location of where those files are loaded from is not relevant to SystemJS as long as it can load them. This means you can go as simple as copying those files locally and serving them from the root-config server, to as complicated as setting up your own unpkg server.

Yes, we can do that not just for the React Package but with all the dependencies and share them between all the micro-frontend apps.
Webpack 5, provides the support of ModuleFederationPlugin in which you can define the list of packages. Example:-
Import Package.json in the Webpack configuration and add following plugin
const packageJson = require('../package.json');
{
plugins: [
new ModuleFederationPlugin({
name: <AppName>,
filename: 'remoteEntry.js',
exposes: {
'./App': <PathToFile>,
},
shared: {
...packageJson.dependencies,
}
}),
],
}
You can explicitly define only the packages name which you want to share.
Check out more about ModuleFederationPlugin

If you want to have flexibility in micro frontend, that is, if you want each micro frontend to be deployed independently of each other, you can use client-side intgration.
React
You can use a CDN for static files. (image, fonts, pdf, doc) Webpack module federation can be used for client-side integration.
Configuring an example webpack and module federation.
We divide the webpack config files into 3 parts. for common, development, production
webpack.common.js
module.exports = {
module: {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['#babel/preset-react', '#babel/preset-env'],
plugins: ['#babel/plugin-transform-runtime'],
},
},
},
],
},
};
webpack.dev.js
const { merge } = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const commonConfig = require('./webpack.common');
const packageJson = require('../package.json');
const devConfig = {
mode: 'development',
output: {
publicPath: 'http://localhost:8082/',
},
devServer: {
port: 8082,
historyApiFallback: {
index: 'index.html',
},
},
plugins: [
new ModuleFederationPlugin({
name: 'auth',
filename: 'remoteEntry.js',
exposes: {
'./AuthApp': './src/bootstrap',
},
shared: packageJson.dependencies,
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
module.exports = merge(commonConfig, devConfig);

Related

Module Federation, React, and Apollo 3 warnings

I'm building an app with micro-frontends using webpack 5's module federation plugin. Everything was fine until I started adding react hooks into my remote app. At that point I received errors about "invalid usage of hooks", i.e. I discovered I had TWO versions of react loaded, one from the remote and one from the app consuming the remote.
That problem was solved by adding a shared key to the ModuleFederationPlugin section of my webpack config that marked React as a singleton. Now everything compiles and seems to run just fine.
However, the webpack compiler is throwing some annoying warnings at me now. Its saying:
No required version specified and unable to automatically determine one. Unable to find required version for "react" in description file (/Users/myComputer/Development/myapp/node_modules/#apollo/client/react/context/package.json). It need to be in dependencies, devDependencies or peerDependencies.
Here is what my webpack config (in the remote) looks like currently:
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')
const deps = require('./package.json').dependencies
module.exports = {
mode: 'development',
devServer: { port: 3001 },
entry: './src/index.tsx',
output: {
path: __dirname + '/dist/',
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'],
},
use: 'ts-loader',
},
]
},
devtool: 'source-map',
plugins: [
new ModuleFederationPlugin(
{
name: 'myRemote',
filename: 'remoteEntry.js',
exposes: {
'./App':
'./src/App/App.tsx',
},
shared: {
'react': {
singleton: true,
version: deps['react'],
},
'react-dom': {
singleton: true,
version: deps['react-dom'],
},
},
}
),
new HtmlWebpackPlugin({
template:
'./index.html',
})
]
}
The consuming app's webpack config is almost the same, especially the shared section (there are some slight differences in that it declares the remotes).
What would be the way to tell webpack that the apollo package will be getting its react dependency from somewhere else? Or if thats not the right thing to tell webpack, what is and how can I get rid of these warnings?
Fixed my own problem by changing the key version to requiredVersion

How to use plugin to reduce size when import lodash

Here is my code : import _ from 'lodash';
I want to use babel-plugin-transform-imports to reduce size of folder when "yarn build".
But I don't know how to set up plugin and config in wepack.config.js
Thank you so much
There is three way to reduce:
1. When in development, you can include the folder where you are going to compile by config the loaders, so the file outside this folder won't be compiled. Loader config are like the following code:
{
test: /\.(js|mjs|jsx)$/,
include: path.resolve(__dirname,"src"),//important
use: {
loader: 'babel-loader'
}
}
2、 You can use DDL to pre-compile the third-party library.
e.g.
Firstly create vendor.js, that is to say you need to bundle it by another webpack config.
const webpack = require('webpack')
const library = '[name]_lib'
const path = require('path')
module.exports = {
mode:"production",
entry: {
vendors: ['lodash']
},
output: {
filename: '[name].dll.js',
path: path.join(__dirname,"dist/vendor"),
library
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, 'dist/[name]-manifest.json'),
// This must match the output.library option above
name: library
}),
]
}
And then you need to include mainfest.json in your project webpack config:
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
manifest: path.join(__dirname, 'dist/vendors-manifest.json'),
})
]
3、 You can use externals to exclude it, you can config it like:
externals : {
lodash : {
commonjs: 'lodash',
amd: 'lodash',
root: '_' // indicates global variable
}
}
And don't forget to include lodsh script in HTML, because webpack don't compile or include it in your bundle. If you don't include ,the broswer will throw an error.
You can check more usage at Webpack website:
https://webpack.js.org/configuration/externals/
https://webpack.js.org/plugins/dll-plugin/

How to build unified $templateCache when migrating AngularJS 1.x project from gulp & bower to webpack 3

Have a large AngularJS 1.6 project (~120 JS files), which is currently built through gulp/bower.
Looking to migrate over to using yarn & webpack, and we would prefer not to have to modify our project files just to implement webpack. Each of our AngularJS component JS files are wrapped in a IIFE, so they are already out of the global scope.
One of our gulp steps uses the gulp-angular-templatecache module, which collects all of our HTML files and neatly compacts them into a single $tmplateCache loader module.
angular.module("ourApp").run(["$templateCache", function ($templateCache) {
$templateCache.put("/app/components/ourComponent1.html", "<div id=\"component1-html\"> ... </div>\r\n");
$templateCache.put("/app/components/ourComponent2.html", "<div id=\"component2-html\"> ... </div>\r\n");
// etc...
});
I have managed to figure out how to resolve most of our other build processes using webpack, but this one is giving me a problem.
I have looked at the ngtemplate-loader package, but do not think this would work for our needs because of the following:
Would require us to update all of our existing HTML templates to use 'require("./template.html")'
This would create a separate webpack module for each HTML template, which seems very inefficient.
Probably most important, I haven't been able to get it to work.
The current webpack configuration I have setup is based on a simple demo, and splits the project files out from the vendor files. Our project files are bundled up into a 'app.[hashcode].js' file into the /dist folder.
Ultimately, I would like to be able to inject the compiled $templateCache module, into a specific point in our final 'app.[hashcode].js' bundle file. However, right now I would be satisfied with the $templateCache definition file being created into a separate bundle file.
Is there an existing webpack plugin, or loader, or combination of plugin(s)/loader(s), that I could use to accomplish this build step? Is this even possible with webpack?
This is the base directory structure for the demo project:
/dashboard
/app
/assets
/global
global.less
app.less
/components
/controllers
dashboard.controller.js
/directives
yep-nope.directive.js
/messenger
messenger.directive.js
messenger.html
/services
github-status.service.js
dashbboard.config.js
dashboard.module.js
app.js
/dist
app.4f12bb49f144e559bd9b.js
assets.css
index.html
vendor.b0a30c79aa77e0126a5c.js
index.html
package.json
webpack.config.js
This is the current working webpack.config.js file:
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
context: __dirname,
entry: {
app: path.resolve(__dirname, 'app/app.js'),
vendor: ['angular','angular-sanitize']
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[chunkhash].js'
},
module: {
rules: [
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: ['css-loader', 'less-loader']
})
},
{
test: /\.js$/,
loader: 'ng-annotate-loader'
},
{
test: /\.html$/,
exclude: path.resolve(__dirname, 'app/index.html'),
use: [
{ loader: 'html-loader', options: { minimize: false } }
]
}]
},
plugins: [
new CleanWebpackPlugin(['dist','assets/**/*.css']),
new HtmlWebpackPlugin({
title: 'GitUp',
template: 'index.html',
inject: 'body'
}),
new webpack.optimize.CommonsChunkPlugin({
name:"vendor", filename:"[name].[chunkhash].js"
}),
new ExtractTextPlugin('assets.css')
]
};
This is the webpack 'entry' file app/app.js:
require('./assets/app.less');
require('../node_modules/angular/angular.js');
require('../node_modules/angular-sanitize/angular-sanitize.js');
require('./components/dashboard.module.js'); // Define module
require('./components/dashboard.config.js'); // Setup config
// Load in all components files, except for files already loaded above
var reqCtx = require.context('./components', true, /^(?!.*(?:dashboard\.config|dashboard\.module)).*\.js$/);
reqCtx.keys().forEach(reqCtx);
If needed, I can provide the entire sample project...
This may or may not be the answer you're looking for but in the past I've used html-loader to allow me to require my component template URLs, and it worked brilliantly:
https://www.npmjs.com/package/html-loader
It just inlines the template in your bundled script.

Resolve module dependencies on remotely loaded React component

TL;DR
I wish if there is a right way to resolve common module dependencies with Webpack on remotely loaded React component as shown at the following Gist: https://gist.github.com/tonytonyjan/616022dce75f8e6c1603bbeb94ec46a4
Details
It may seem uncommon, but we won't have access to all React components at build time.
We gonna have plugins that would be a Webpack bundled React components, deployed in some accessible endpoint.
And our webapp will display a page of available plugins, loaded at runtime.
The above Gist shows how to remotely load React component, but it resolves external dependencies by manually mapping modules on require function:
function require(name){
if(name == 'react') return React
else throw `You can't use modules other than "react" in remote component.`
}
I wish to know if there is a better way to graciously resolve all common external dependencies of remote components (asynchronously or not), without doing this manual mapping trick (some Webpack config).
For instance, plugin webpack.config.js would be something like below
module.exports = {
entry: {
PluginView: 'src/PluginView.js'
},
target: 'web',
output: {
path: path.join(__dirname, outputPath),
filename: 'PluginView.bundle.js',
libraryTarget: 'commonjs2'
},
module: {
loaders: [
{
loader: 'babel-loader',
query: {
presets: ['es2015', 'react']
}
}
]
},
externals: {
'react': 'React',
'otherThirdPartyLib': 'OtherThirdPartyLib' // any other common lib already available at our webapp
}
};
Any thoughts will be appreciated.
Thanks in advance.

How to migrate from imports using <script> to module bundler?

I have a project which uses Angular. It does not use a task manager nor dependency manager and all libs are stored in repo and included using plain old <script> tag like <script src="libs/angular/angular.min.js"></script>.
I want to modernize the application by introducing a module bundler and automating the build process. The original idea was gulp + bower, but I see that webpack + npm3 are a trend now.
There is no issue with gulp + bower because of things like gulp-inject, but I can’t find anything similar which works with webpack.
Are there any tools which would allow me to use the existing code as it is and write only new modules using webpack?
Here's the solution i've got so far. Since the webpack requires a single entry point per bundle, i still have to import all those legacy js in a single file. So i decided to use ES6 import (or webpack's require) in that entrypoint called index.js
here's the how webpack config looks like
var webpack = require('webpack');
var path = require('path');
var basePath = __dirname + "/target/app";
module.exports = {
context: basePath,
entry: {
app: './index.js',
vendor: './vendor.js'
},
output: {
path: basePath + "/build",
filename: '[name].js'
},
module: {
loaders: [
{test: /\.js$/, loader: 'babel?presets=es2015', exclude: /node_modules/},
]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
}),
new webpack.ProvidePlugin({
"$": "jquery",
"jQuery": "jquery",
"window.jQuery": "jquery",
"_": "lodash"
}),
],
};
and so in index.js i did following
import './app/component1.js'
import './app/component2.js'
//all the rest imports from index.html
as for the third part libraries like jquery, angular, etc... i decided to describe that imports in separate bundle and so there's another bundle called vendor.js
import './libs/lodash.js'
import './libs/jquery.js'
//and all the rest libraries
//note that here we can use now libs from npm node_modules like
import 'angular' //will take angular from node_modules/angular
Important thing which i faced while doing this, is that legacy modules are not really compatible with webpack because of using global variables like $, _, etc... ProvidePlugin helps here and add require({module}) to the modules where the provided {variable} met as it's described in config 'variable': 'module'
Note that i used CommonsChunkPlugin to avoid having dependent modules in both bundlers, like angular is required in index.js and vendor.js but with this plugin webpack will handle that and add js code only in vendor.js

Resources