Webpack multi-compiler example to bundle builds for React, Svelte, Web Components, and normal Express view templates.
1. About
2. Install & Build
3. Extra Features Implemented
3-1. Webpack Dynamic (async) Import
3-2. Use of "animejs" in ES modules
3-3. React: Tailwind + Emotion
3-4. React: Compose Multiple Context Providers
3-5. React (Hooks): Using "useDebounce" of "react-use"
3-6. React: Using "scrollmonitor-react"
3-7. Web Components: Load External CSS for Shadow DOMs
3-8. Web Components: No "class" Syntax
3-9. Web Components: Apply styles to "slot"
3-10. Express: Include Nunjucks Partials
3-11. CORS Errors Fetching from "localhost"
3-12. Using Node Profiler
4. Troubles & Solutions
4-1. Jest Does Not Understand "import()"
4-2. Unexpected "import" (Jest)
4-3. Infinite Loop Using "webpack-hot-middleware"
4-4. Uncaught ReferenceError: regeneratorRuntime is not defined
4-5. No HMR Found For Subdirectory
4-6. Svelte: new App fails
4-7. Svelte: Can't reexport the named export "onMount"
4-8. React: 404 Not Found with React Router using Subdirectory
5. Installed NPM Packages
6. Notes
6-1. Irrelevant Issues
(a) Bundle Only NPM Modules Wanted
(b) Watch Server Template Changes Without Restart
(c) HMR on view templates
7. LICENSE
A working example with similar composition is found at:
Demo
As we enter 2020, many of us deal with monolithic frontends, and our professional hunch tells us something must be done for all the pains and sacrifices made whenever make changes to hundreds of components. We want to casually make changes to the websites without caring too much about the risks to other components. So, it was quite natural for web dev communities to observe last year the growing trends of "micro frontends" as to satisfy the mentioned urge.
One solution would be to have pages with different frameworks. This project configures Webpack multi-compilers to bundle both React and Svelte apps. Another would be to have Web Components for more lightweight tasks. For this one, too, we have Webpack bundles the codes.
However, as simple as it may sound, configuring Webpack to handle multiple frameworks is quite laborious. Especially when we normally want these frameworks running in different subdirectories, and we can easily assume it means never-ending struggles with Webpack configurations. With a sample provided in this project, I hope to lessen the burdens for those who also suffer from the same situations.
We have Express app which serves several routes:
(1) /
and /contact
: Normal Express app
(2) /pizza
: React SPA
(3) /nacho
: Svelte SPA
For (1) normal Express app, as you look into the HTML codes, you see the following tags:
a. <burger-header>
b. <cookie-consent>
c. <message-box>
which are all custom elements
to demonstrate the use of Web Components.
So, there's that.
(4) Web Components
For all the above, we use multi-compiler mode of Webpack,
which is configured in webpack.config.js
,
and basically requires another set of the following configurations:
(1) webpack.config.normal.js
(2) webpack.config.react.js
(3) webpack.config.svelte.js
(4) webpack.config.webcomponent.js
To speed up the build, we use parallel-webpack
instead of normal multi-compiler mode of Webpack.
When we yarn build:dev
, along with the bundling
processes for JS and CSS, we use HTML Webpack Plugin
to generate *.njk
(Nunjucks) from templates,
that are used as view templates for Express app
when we run dist/server.js
.
As you can tell, once the view templates are generated,
they are physically there in the dist/views
,
and neither webpack-dev-middleware
nor webpack-hot-middleware
can detect changes made to the source templates.
So, whenever you make changes to the source templates,
you must run yarn build:dev
to re-generate the view templates,
and reload the browser.
Similar goes for Web Components.
Because Web Components are loaded runtime by nature,
webpack-dev-middleware
and webpack-hot-middleware
cannot detect changes to any of the sources,
and you must perform the build again
just like you need it for Express view templates.
For normal JS and React apps,
webpack-dev-middleware
and webpack-hot-middleware
detect changes, and you don't need to run builds again.
For Svelte apps, it currently does not support HMR,
and you must re-build, and reload the browser.
(while I can make webpack-dev-middleware
to detect changes,
I haven't done it yet)
yarn build:dev
Regardless of the app kinds, *.njk
(Nunjucks) templates are built to dist/views
.
The rest of the chunks are bundled to dist/public
.
All the chunks are bundled right bellow the directory,
but Web Component codes are bundled to dist/public/components
directory,
React codes to dist/public/pizza
,
Svelte codes to dist/public/nacho
.
Given NODE_ENV=development
, codes are bundled for development
.
For all ES6 codes are transpiled in accord with the definitions in babel.config.js
.
yarn start
Starts Express app by runing dist/server.js
.
Uses webpack-dev-middleware
and webpack-hot-middleware
.
You must run yarn build:dev
beforehand, otherwise Express
is not able to read *.njk
(Nunjucks) templates.
The same goes for Web Components.
Whenever you make changes to Web Components,
repeat the build.
Ideas are the same as the ones for development
, but for production
:
yarn build
Some features implemented in this project may help someone who has troubles implementing them.
Sometimes you want certain NPM packages being loaded at runtime,
and Webpack4 offers import()
syntax to achieve this,
and those packages are excluded from the bundled chunks.
(async () => {
const animeES = await import('animejs/lib/anime.es.js');
})();
You need to specify optimization.splitChunks.chunks
in your Webpack configuration.
initial
- Static imports onlyasync
- Dynamic imports onlyall
- Enable both of the aboveoptimization: {
splitChunks: {
chunks: 'all'
}
}
You also need to know import()
syntax is valid only to Webpack4,
and Babel does not understand the syntax.
See
"5-1. Jest Does Not Understand "import()""
for how you need @babel/plugin-syntax-dynamic-import
to teach Babel of the syntax.
animejs
in ES modulesanimejs
provides a build specific for ES codes:
const animeES = await import('animejs/lib/anime.es.js');
const anime = prop('default')(animeES);
anime({
targets: '.square',
keyframes: [
{ translateX: 190 },
{ translateX: 0 },
],
loop: true
})
Or, you could simply do this:
./src/spa/react/components/Cloud.jsx
import anime from 'animejs/lib/anime.min.js';
anime({
targets: svg,
...
});
You want Tailwind + Emotion in your React apps.
./src/spa/react/components/Toppings.jsx
<div key={o.id} css={css`${tw`flex flex-row`}`}>
The topic is pretty much discussed in the following repository cra-ts-emotion-tailwind-solution or mini-react-201910, but if you are using Jest to test your components, the basic idea is to choose "Babel macro" solution over "PostCSS" solution.
You have several React context providers.
Instead of digging the JSX nests too deep,
you want to compose them.
import { composeContextProviders } from './lib/';
import { ProvideScreenSize } from './contexts/';
ReactDOM.render(
<Router basename={basename}>
{composeContextProviders(
[
[ProvideScreenSize, {}]
],
(<App />)
)}
</Router>,
document.querySelector('main')
);
export const composeContextProviders = (contexts, component) => {
return contexts.reduce((acc, [Provider, value]) => {
return (
<Provider value={value}>{acc}</Provider>
);
}, component);
}
useDebounce
of react-use
./src/spa/react/components/App.jsx
import { useDebounce } from 'react-use';
import { useScreenSize } from '../contexts/';
export const App = () => {
const { screenSize } = useScreenSize();
const resize = useCallback(() => {
console.log(`[React.App] ${screenSize.width}x${screenSize.height}`);
}, [screenSize]);
const [, debouncedResize] = useDebounce(
resize,
500,
[screenSize]
);
useEffect(() => {
debouncedResize();
}, [debouncedResize]);
return (
<div>...</div>
);
}
scrollmonitor-react
A sample code to show the use of "scrollmonitor-react".
./src/spa/react/components/Cloud.jsx
<Content1 stateChange={this.receiveStateChange.bind(this)} />
<Content2 stateChange={this.receiveStateChange.bind(this)} />
<Content3 stateChange={this.receiveStateChange.bind(this)} />
to-string-loader
is a handy loader for external CSS files
to your Web Components to apply styles to Shadow DOMs.
./webpack.config.webcomponent.js
module: {
rules: [
{
test: /\.css$/,
exclude: /node_modules/,
use: [
'to-string-loader'
'css-loader',
'postcss-loader'
]
}
]
}
./src/components/burger-header/index.js
./src/components/cookie-consent/index.js
import styles from './style.css';
import template from './template.html';
class BurgerHeader extends HTMLElement {
constructor () {
super();
const text = String.raw`${template}`;
const css = styles.toString();
if (!text) throw new Error('No template');
if (!css) throw new Error('No styles');
const el = document.createElement('template');
el.innerHTML = `<style>${css}</style>${text}`;
this.attachShadow({ mode: 'open' })
.appendChild(el.content.cloneNode(true));
}
class
SyntaxSome of you may prefer not using ES6 class
syntax,
but want to go the old fashion way (using prototype
).
./src/components/message-box/index.js
slot
Applying styles to CSS classes within the custom elements are easy.
However, you want styles for slot
given from the parent.
Say, the parent gives you "message" slot.
<cookie-consent>
<div slot="message">
This website uses cookies to ensure you get the best experience on our website.
</div>
</cookie-consent>
and you have in your custom element the following
./src/components/cookie-consent/template.html
<div id="message-wrapper">
<slot name="message">
This website uses cookies.
</slot>
</div>
and here is the selector syntax you want.
./src/components/cookie-consent/style.css
div#message-wrapper slot[name=message] {
@apply text-gray-800 mb-1;
}
Many Express view templates allow you to include partial templates, and so does Nunjucks.
${require('./partials/footer/template.njk')}
localhost
You run your app locally, and Chrome raises a CORS error
when you attempt to fetch
external resources.
What you probablly want is: https://cors-anywhere.herokuapp.com/
./src/spa/svelte/App.svelte
./src/lib/cors.js
If you have never used it before, it is probably the time.
"profile": "node --inspect-brk node_modules/webpack/bin/webpack.js --config webpack.config.js",
Once execute the above command,
open chrome://inspect
, and start the profiler.
You can see what's going on with your webpack build process.
For More:
https://github.com/webpack/webpack/issues/4550#issuecomment-306750677
Issues I encountered, and possible solutions for them.
import()
(Webpack Dynamic Import)Webpack understands the syntax import()
just fine, but Babel complains.
For Babel, you need plugin-syntax-dynamic-import
.
Once installed, then in your babel.config.js
:
"plugins": [
"@babel/plugin-syntax-dynamic-import"
]
Once a time, we used babel-polyfill
for dynamic module imports.
Yet, it required us to have 2 steps: actual files to import others,
and sort of proxy files for them.
Now, it has become much easier for we only need "node" as one of the build target.
In your babel.config.js
:
"presets": [
[
"@babel/preset-env", {
"modules": false,
"targets": {
"node": "current"
}
}
]
]
import
(Jest)When jest
does not understand import
syntax,
you need @babel/plugin-transform-modules-commonjs
.
"plugins": [
"@babel/plugin-transform-modules-commonjs"
]
By the way, you could use babel-jest
,
but babel-jest
is now integrated into Jest.
https://github.com/vuejs/vue-cli/issues/1584#issuecomment-519482294
webpack-hot-middleware
While the main cause of is still unknown (could be Node version),
sometimes we experience a browser to endlessly reload itself
when using webpack-hot-middleware
.
To stop this from happenning, try to set an actual URL
for publickPath
instead of a relative path:
app.use(webpackDevMiddleware(compiler, {
publicPath: 'http://localhost:5000',
}));
https://github.com/webpack-contrib/webpack-hot-middleware/issues/135#issuecomment-348724624
"plugins": [
"@babel/plugin-transform-runtime"
]
If you output chunks to a subdirectory, having __webpack_hmr
for webpack-hot-middleware path does not resolve,
and you must specify the subdirectory.
For instance, we bunlde React codes to dist/public/react
,
so we explicitly tell Webpack where to look for the HMR loader.
entry: {
pizza: [
'webpack-hot-middleware/client?path=http://localhost:5000/react/__webpack_hmr`,
'src/spa/react/index.jsx'
]
}
webpackHotMiddleware(compiler, {
path: '/react/__webpack_hmr'
})
Although you new App
, it fails.
TypeError: Class constructor SvelteComponent cannot be invoked without 'new'
"browserslist": [
"last 1 chrome versions"
]
Or, in your babel.config.js
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": ["last 1 chrome versions"]
}
}
]
]
onMount
In your Svelte file, you try:
import { onMount } from 'svelte';
and it gives you:
Can't reexport the named export 'onMount' from non EcmaScript module (only default export is available)
Add .mjs
to extension
BEFORE the .js
in your Webpack config:
https://github.com/sveltejs/svelte-loader/issues/82#issuecomment-485830738
resolve: {
extensions: ['.mjs', '.js', '.svelte']
}
Also, don't forget to add it to the babel-loader
as well (optional):
{
test: /\.m?jsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
rootMode: 'upward'
}
}
router.get(['/pizza', '/pizza/*'], (req, res) => res.render('pizza/index'));
Also, make sure you have "basename" set for your React Router.
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { App } from './components';
const basename = process.env.REACT_PUBLIC_URL;
ReactDOM.render(
<Router basename={basename}>
<App />
</Router>,
document.getElementById('root')
);
and use webpack.DefinePlugin
to set process.env.REACT_PUBLIC_URL
.
new webpack.DefinePlugin({
'process.env.REACT_PUBLIC_URL': JSON.stringify(path.resolve('/pizza')),
})
Installed NPM packages follow:
For Babel:
babel-polyfill
alternatively).For Babel (React):
For ESLint:
For Webpack:
No autoprefixer
is needed since tailwind
contains one.
*.njk
view templates at the build. dist/public/images
. *.njk
templates. For Webpack (other goodies):
For CSS:
Make sure to use @emotion/core
instead of using emotion
for React
(which is installed for prod, not dev).
Also, babel-plugin-emotion
is not needed
when we have @emotion/babel-preset-css-prop
,
tw
macro notation, this is what you need.
MoreFor Jest + Enzyme testing:
import
syntax, Jest does not. yarn add --dev @babel/core @babel/preset-env @babel/plugin-syntax-dynamic-import @babel/plugin-transform-runtime @babel/preset-react babel-eslint eslint eslint-loader eslint-plugin-react webpack webpack-cli parallel-webpack webpack-dev-middleware webpack-hot-middleware webpack-merge html-webpack-plugin copy-webpack-plugin babel-loader html-loader nunjucks-loader file-loader svelte-loader csp-html-webpack-plugin license-webpack-plugin webpack-bundle-analyzer css-loader style-loader postcss-loader mini-css-extract-plugin to-string-loader tailwindcss babel-plugin-macros @emotion/babel-preset-css-prop jest jest-emotion @babel/plugin-transform-modules-commonjs enzyme enzyme-adapter-react-16 enzyme-to-json
babel-plugin-macros
). yarn add express nunjucks @emotion/core @emotion/styled tailwind.macro@next react react-dom react-router-dom react-use svelte moment ramda animejs scrollmonitor-react
Troubleshoots from the past for features that were once
implemented in this project but are now gone.
Or, notes on irrelevant topics, but may help you
with understanding the core issues associated.
If you attempt to bundle server.js
using Webpack,
you do not want to include unwanted NPM modules.
Then, use webpack-node-externals
,
and set it to "externals" option in your Webpack config.
You may not.
You could either go with a pure React application or normal server-client application.
If you choose server-client,
consider using services like nodemon
or supervisor
to watch the template changes.
Compared to nodemon
, supervisor
has fewer dependencies.
Install supervisor
, and do this:
"start": "NODE_ENV=development supervisor -e njk dist/server.js"
To detect changes in "*.njk" templates, we need a workaround.
https://github.com/jantimon/html-webpack-plugin/issues/100#issuecomment-368303060
const ReloadPlugin = require('reload-html-webpack-plugin');
Dual-licensed under either of the followings.
Choose at your option.