I want to see how much of a speed boost I get from using the non dev version of everything so I built my site using my "production" webconfig. but dev tools still is telling me it is in "development" mode
This page is using the development build of React.
const merge = require("webpack-merge");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const common = require("./webpack.common.js");
module.exports = merge(common, {
// Provides process.env.NODE_ENV with value production.
// Enables FlagDependencyUsagePlugin, FlagIncludedChunksPlugin,
// ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin,
// SideEffectsFlagPlugin and UglifyJsPlugin.
mode: "production",
// see https://webpack.js.org/configuration/optimization/
optimization: {
// minimize default is true
minimizer: [
// Optimize/minimize CSS assets.
// Solves extract-text-webpack-plugin CSS duplication problem
// By default it uses cssnano but a custom CSS processor can be specified
new OptimizeCSSAssetsPlugin({})
]
},
module: {
rules: [
{
test: /\.(sa|sc|c)ss$/,
// only use MiniCssExtractPlugin in production and without style-loader
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"]
}
]
},
plugins: [
// Mini CSS Extract plugin extracts CSS into separate files.
// It creates a CSS file per JS file which contains CSS.
// It supports On-Demand-Loading of CSS and SourceMaps.
// It requires webpack 4 to work.
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css"
}),
new BundleAnalyzerPlugin()
]
});
in my package.json
"scripts": {
"dev": "cross-env NODE_ENV=dev webpack-serve --config webpack.dev.js --open",
"prod": "cross-env NODE_ENV=prod webpack -p --config webpack.prod.js",
"qa": "cross-env NODE_ENV=QA webpack --config webpack.prod.js"
},
My confidence is growing that your problem stems from setting NODE_ENV to prod in your package.json. I think you should instead set it to production, or allow webpack to set it internally with the mode option in your webpack config.
The react-scripts package explicitly sets this value to production for building and searching for NODE_ENV in the facebook/react project in github project shows tons of checks for production as the value instead of prod.
Here's the index.js file for react-is, many other projects in the react source follow the same pattern:
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-is.production.min.js');
} else {
module.exports = require('./cjs/react-is.development.js');
}
I would try changing your build script to:
"prod": "cross-env NODE_ENV=production webpack -p --config webpack.prod.js",
or even let webpack -p set it automatically:
"prod": "cross-env webpack -p --config webpack.prod.js",
Related
Note: I am not using NextJS for my SSR application. I also did not use create-react-app to create my application.
I have a React Server Side Rendered (SSR) application which was hand built (as apposed to using a tool like create-react-app). I use WebPack to bundle up the server side code and the client side code. I followed the excellent Udemy course https://www.udemy.com/course/server-side-rendering-with-react-and-redux/ to understand how to create a React SSR application
My Application
Application structure
webpack.base.config.js
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif)$/i,
use: 'file-loader',
},
{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
exclude: /node_modules/,
query: {
cwd: __dirname,
},
},
],
},
resolve: {
extensions: ['.js', '.jsx'],
},
};
webpack.client.config.js
const path = require('path');
const { merge } = require('webpack-merge');
const CopyPlugin = require('copy-webpack-plugin');
const baseConfig = require('./webpack.base.config.js');
const config = {
entry: './src/client/client.jsx',
output: {
filename: 'client-bundle.js',
path: path.resolve(__dirname, 'public'),
},
plugins: [
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, 'src', 'styles'),
to: path.resolve(__dirname, 'public'),
},
],
}),
],
};
module.exports = merge(baseConfig, config);
webpack.server.config.js
const path = require('path');
const { merge } = require('webpack-merge');
const webpackNodeExternals = require('webpack-node-externals');
const baseConfig = require('./webpack.base.config.js');
const config = {
target: 'node',
entry: './src/server/server.js',
output: {
filename: 'server-bundle.js',
path: path.resolve(__dirname, 'build'),
},
externals: [webpackNodeExternals()],
};
module.exports = merge(baseConfig, config);
Routes
{
...Home,
path: '/',
exact: true,
},
{
...Page1,
path: '/page1',
exact: true,
},
Client Side Routing
<BrowserRouter>
...
</BrowserRouter>
Server Side Routing
<StaticRouter context={context} location={req.path}>
...
</StaticRouter>
Server Side generated HTML template
<html>
<head>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<div id="root">${content}</div>
<script src="client-bundle.js"></script>
</body>
</html>
package.json scripts
"scripts": {
"start": "node build/server-bundle.js",
"build": "npm-run-all --parallel prod:build*",
"prod:build-server-bundle": "webpack --config webpack.server.config.js",
"prod:build-client-bundle": "webpack --config webpack.client.config.js",
"dev": "npm-run-all --parallel dev:*",
"dev:run-server": "nodemon --watch build --exec node build/server-bundle.js",
"dev:build-server-bundle": "webpack --config webpack.server.config.js --watch",
"dev:build-client-bundle": "webpack --config webpack.client.config.js --watch",
"lint": "eslint ./src --ext .js,.jsx"
},
Running my application
I run the application locally using
npm run dev
My application URLs are therefore
http://localhost:/
http://localhost:/page1
My Requirements
I would like my application to have a customizable URL path, for example "/a/b" so that my URLs would be
http://localhost:/a/b
http://localhost:/a/b/page1
or if my path is "xyz" my URLs would be
http://localhost:/xyz
http://localhost:/xyz/page1
How to i enable a custom base path in my React SSR Application.
What i tried
I hardcoded a path in my application in the HTML, and routers, i..e
<html>
<head>
<link rel="stylesheet" type="text/css" href="a/b/styles.css">
</head>
<body>
<div id="root">${content}</div>
<script src="a/b/client-bundle.js"></script>
</body>
</html>
<BrowserRouter basename="a/b/">
...
</BrowserRouter>
<StaticRouter context={context} location={req.path} basename="a/b/">
...
</StaticRouter>
But this does not work, going to either of the following
http://localhost
http://localhost/a/b
renders my home page with no stylesheet applied and no client side bundle. This is because neither of the following can be found and return a 404
http://localhost/a/b/styles.css
http://localhost/a/b/client-bundle.js
Furthermore, if i use a link to invoke the router, the URL for the styles and client-bundle has the path twice, i.e.
client side navigation to
http://localhost:8080/a/b/contact
means styles and client-bundle request urls are
http://localhost/a/b/a/b/styles.css
http://localhost/a/b/a/b/client-bundle.js
You can simply add an env variable basePath and then use that to set your routes.
Routes
{
...Home,
path: `${process.env.basePath}/`,
exact: true,
},
{
...Page1,
path: `${process.env.basePath}/page1`,
exact: true,
},
Now, if your basePath is '/a/b', your index component will be available on yourdomain/a/b/ and page1 will be available on yourdomain/a/b/page1
Hassaan Tauqir's post above which I have marked as the answer helped me refine the solution. Thank you to Hassaan.
package.json
Change the scripts for the PRODUCTION environment to have the BASE_URL specified.
"prod:build-server-bundle": "cross-env BASE_URL=/a/b webpack --config webpack.server.config.js",
"prod:build-client-bundle": "cross-env BASE_URL=/a/b webpack --config webpack.client.config.js",
Note: You have to use "cross-env" otherwise this will not work on every operating system, therefore I had to install "cross-env" first
npm install cross-env
I left the DEVELOPMENT scripts unchanged as I do not need a path when testing locally
"dev:build-server-bundle": "webpack --config webpack.server.config.js --watch",
"dev:build-client-bundle": "webpack --config webpack.client.config.js --watch",
webpack.base.config.js
Reading in BASE_URL
The "BASE_URL" is accessible in "webpack.base.config.js". I added code so that I can handle the "BASE_URL" being specified with a trailing slash or not.
// Set up the BASE_URL parameter, ensuring it does not have a trailing slash
let BASE_URL = '';
if (process.env.BASE_URL) {
BASE_URL = process.env.BASE_URL.toString().trim();
if (BASE_URL.substr(-1) === '/') {
BASE_URL = BASE_URL.substr(0, BASE_URL.length - 1);
}
}
publicPath
In the "module.exports" add the "output" section and add the "publicPath" setting. "publicPath" allows you to specify the base path for all the assets within your app, for example I have images which I reference in my application using the following code.
import myImage from '../images/myImage.png';
....
<img src={myImage } alt="myImage " />
....
"publicPath" must end in a trailing slash, therefore if we have a BASE_URL I append a / otherwise I leave it empty.
output: {
publicPath: (BASE_URL) ? `${BASE_URL}/` : '',
},
For more information on "publicPath" see https://webpack.js.org/guides/public-path/
webpack.DefinePlugin
In the "module.exports" add the "webpack.DefinePlugin" setting the environment variable to be passed through to the reset of the application
plugins: [
new webpack.DefinePlugin({
'process.env.BASE_URL': JSON.stringify(BASE_URL),
}),
],
For more information on "DefaultPlugin" see https://webpack.js.org/plugins/define-plugin/
Server Side Routing
Add the "basename" to the server side router, whose value is the variable specified in the "DefinePlugin" in the webpack config file
<StaticRouter context={context} location={req.path} basename={process.env.BASE_URL}>
...
</StaticRouter>
Client Side Routing
Add the "basename" to the client side router, whose value is the variable specified in the "DefinePlugin" in the webpack config file
<BrowserRouter basename={process.env.BASE_URL}>
...
</BrowserRouter>
I have the following postcss.config.js file:
// postcss.config.js
module.exports = {
plugins: [
require('postcss-import'),
require('tailwindcss'),
require('autoprefixer'),
]
}
and the following tailwind.config.js file:
// tailwind.config.js
module.exports = {
purge: [
'./src/cljs/foo/*.cljs',
'./target/cljs-runtime/*.js',
'./target/cljsbuild/public/js/*',
'./target/cljsbuild/public/js/cljs-runtime/*',
'./target/*'
],
theme: {},
variants: {},
plugins: [],
}
And my goal is to compress the css generated, for which I've added the purge key in tailwind.config.js.
To generate the css from the .src tailwind file, styles.src.css:
#tailwind base;
#tailwind components;
#tailwind utilities;
I'm running the command:
postcss ./resources/public/css/styles.src.css -o ./resources/public/css/styles.css
from the root directory of my project that contains both the tailwind.config.js and the postcss.config.js. Yet after running the command, the generated css is 1.2MB, as big as what I had without the purge key. Why isn't postcss purge working?
You don't need that command with postcss.
Just add enabled:true in purge in tailwind.config.json and wrap your path into list as stated in https://tailwindcss.com/docs/optimizing-for-production#enabling-manually:
purge: {
enabled: true,
content: [
'./src/cljs/foo/*.cljs',
'./target/cljs-runtime/*.js',
'./target/cljsbuild/public/js/*',
'./target/cljsbuild/public/js/cljs-runtime/*',
'./target/*'
],
},
There it is! Now you can run and see the results:
npm run build:css
That's the command I use in package.json:
"scripts": {
"build:css": "tailwind build static/css/tw.css -o static/css/tailwind.css"
},
Your PostCSS configuration is split between tailwind.config.js and postcss.config.js, when it should all be in postcss.config.js.
Why?
Tailwind uses PostCSS behind the scenes. But PostCSS itself doesn't know about your tailwind.config.js file. To use the postcss command, you need to specify the purge option in the postcss.config.js file, not tailwind.config.js. This page on the Tailwind website explains the difference between the two files in detail.
Here is my setup:
// postcss.config.js
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
require('#fullhuman/postcss-purgecss')({
// Specify the paths to all of the template files in your project
content: [
'./src/cljs/foo/*.cljs',
'./target/cljs-runtime/*.js',
'./target/cljsbuild/public/js/*',
'./target/cljsbuild/public/js/cljs-runtime/*',
'./target/*'
],
// This extractor will tell PurgeCSS to ignore all CSS selectors and tags used in your files
defaultExtractor: content => Array.from(content.matchAll(/:?([A-Za-z0-9-_:]+)/g)).map(x => x[1]) || []
}),
]
}
Note my tailwind.config.js file is empty:
// tailwind.config.js
module.exports = {
purge: [],
theme: {
extend: {},
},
variants: {},
plugins: [],
}
Well you can also add purge key in postcss.config.js.
This is my config in
postcss.config.js
const purgecss = require('#fullhuman/postcss-purgecss')({
// Specify the paths to all of the template files in your project
content: ['./src/**/*.js', './public/index.html'],
// make sure css reset isnt removed on html and body
whitelist: ['html', 'body'],
// Include any special characters you're using in this regular expression
defaultExtractor: (content) => content.match(/[A-Za-z0-9-_:/]+/g) || [],
})
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
...(process.env.NODE_ENV === 'production' ? [purgecss] : []),
],
}
Important: The environment variable NODE_ENV is responsible for dev and prod environment. If you are using tailwindcss in dev mode, then you don't want to purge as you want to use all the available styles. By setting it for production mode will inform postcss and thus this will purge unused css.
Please take note that I haven't set any config for tailwindcss in webpack config.
At build time, make sure that you have your NODE_ENV set to specific value for production use case. You can use either 'production' or 'prod' doesn't matter. Same will reflect in postcss.config.js.
Tailwind will purge automatically - from their docs:
Now whenever you compile your CSS with NODE_ENV set to production, Tailwind will automatically purge unused styles from your CSS
https://tailwindcss.com/docs/controlling-file-size#basic-usage
You can run commands for your dev and production environments - development will keep all Tailwind's classes, production will run the purge.
package.json:
"dependencies": {
"autoprefixer": "^9.8.5",
"postcss-cli": "^7.1.1",
"tailwindcss": "^1.5.2"
},
"devDependencies": {
"cross-env": "^7.0.2"
},
"scripts": {
"watch": "cross-env NODE_ENV=development postcss static/css/tailwind.css -o style.css --watch",
"build": "cross-env NODE_ENV=production postcss static/css/tailwind.css -o style.css"
},
postcss.config.js
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer'),
]
}
I have an existing application that I am using "webpack-serve" as it was recommended to me by the developer(at that time he was not going to update webpack-dev-server anymore).
Anyways now it is deprecated and not being used, I got to back to webpack-dev-server but I am thinking if I should just go through the effort and try to use something like "Create React App" as I don't really know if I can use these old wepack.js files I made for webpack-serve and they also don't seem to work 100% as everytime I try to build a production build it gives me a dev build.
webpack.common.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");
const webpack = require('webpack');
module.exports = {
entry: ["#babel/polyfill", "./src/index.js"],
output: {
// filename and path are required
filename: "main.js",
path: path.resolve(__dirname, "dist"),
publicPath: '/'
},
module: {
rules: [
{
// JSX and JS are all .js
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
}
},
{
test: /\.(eot|svg|ttf|woff|woff2)$/,
use: [
{
loader: 'file-loader',
options: {}
}
]
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {}
}
]
}
]
},
plugins: [
new CleanWebpackPlugin(["dist"]),
new HtmlWebpackPlugin({
template: "./src/index.html"
}),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
})
]
};
webpack.dev
const path = require("path");
const merge = require("webpack-merge");
const convert = require("koa-connect");
const proxy = require("http-proxy-middleware");
const historyApiFallback = require("koa2-connect-history-api-fallback");
const common = require("./webpack.common.js");
module.exports = merge(common, {
// Provides process.env.NODE_ENV with value development.
// Enables NamedChunksPlugin and NamedModulesPlugin.
mode: "development",
devtool: "inline-source-map",
// configure `webpack-serve` options here
serve: {
// The path, or array of paths, from which static content will be served.
// Default: process.cwd()
// see https://github.com/webpack-contrib/webpack-serve#options
content: path.resolve(__dirname, "dist"),
add: (app, middleware, options) => {
// SPA are usually served through index.html so when the user refresh from another
// location say /about, the server will fail to GET anything from /about. We use
// HTML5 History API to change the requested location to the index we specified
app.use(historyApiFallback());
app.use(
convert(
// Although we are using HTML History API to redirect any sub-directory requests to index.html,
// the server is still requesting resources like JavaScript in relative paths,
// for example http://localhost:8080/users/main.js, therefore we need proxy to
// redirect all non-html sub-directory requests back to base path too
proxy(
// if pathname matches RegEx and is GET
(pathname, req) => pathname.match("/.*/") && req.method === "GET",
{
// options.target, required
target: "http://localhost:8080",
pathRewrite: {
"^/.*/": "/" // rewrite back to base path
}
}
)
)
);
}
},
module: {
rules: [
{
test: /\.(sa|sc|c)ss$/,
use: ["style-loader", "css-loader", "sass-loader"]
}
]
}
});
webpack.prod
const merge = require("webpack-merge");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const common = require("./webpack.common.js");
module.exports = merge(common, {
// Provides process.env.NODE_ENV with value production.
// Enables FlagDependencyUsagePlugin, FlagIncludedChunksPlugin,
// ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin,
// SideEffectsFlagPlugin and UglifyJsPlugin.
mode: "production",
devtool: "source-map",
// see https://webpack.js.org/configuration/optimization/
optimization: {
// minimize default is true
minimizer: [
// Optimize/minimize CSS assets.
// Solves extract-text-webpack-plugin CSS duplication problem
// By default it uses cssnano but a custom CSS processor can be specified
new OptimizeCSSAssetsPlugin({})
]
},
module: {
rules: [
{
test: /\.(sa|sc|c)ss$/,
// only use MiniCssExtractPlugin in production and without style-loader
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"]
}
]
},
plugins: [
// Mini CSS Extract plugin extracts CSS into separate files.
// It creates a CSS file per JS file which contains CSS.
// It supports On-Demand-Loading of CSS and SourceMaps.
// It requires webpack 4 to work.
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css"
}),
new BundleAnalyzerPlugin()
]
});
Edit
If I where to go over to Create React App how would I handle this stuff?
I have a .babelrc with
"presets": ["#babel/env", "#babel/react"],
"plugins": [
["#babel/plugin-proposal-decorators", { "legacy": true }],
"#babel/plugin-transform-object-assign",
"#babel/plugin-proposal-object-rest-spread",
"transform-class-properties",
"emotion"
]
I think react-app takes care of some of the stuff but not sure if all. I also have if you noticed in webpack.common I am pollying filling everything, would I just need "react-app-polyfill."?
How can I add another "dev mode"
"scripts": {
"dev": "cross-env NODE_ENV=dev webpack-serve --config webpack.dev.js --open",
"prod": "cross-env NODE_ENV=prod webpack -p --config webpack.prod.js",
"qa": "cross-env NODE_ENV=QA webpack --config webpack.prod.js"
},
I need to setup the Node_ENV for QA as I have a check to point to my api that changes in each enviroment.
it's a simple as below today:
npx create-react-app .
I've had to do something like this a couple times. This has been my approach:
create-react-app my-app-cra // clean slate
npm i [list of dependencies] // minus any build, compile, transpile, etc. dependencies
Copy over my src folder, preserving as much of the structure as possible
npm start // and keep fingers crossed! Typically, a bit of manual work is involved
To preserve your git history:
Copy your src to a folder outside your repo
Clean your repo git rm -rf
Perform the above steps (create react app, install deps, copy src back in)
git add // If you preserve your folder structure, git will find the copied over files (and will notice a possible change in path) and handles gracefully, preserving history.
Both create-react-app and webpack 4 are good options and very simple. In my opinion, create-react-app is the most practical.
In order to conserve your git history, I recommend:
create a branch and go to it.
install and save the dependencies and dev-dependencies of create-react-app. you will see them in your package.json file.
do the configuration. use the create-react-app repo as an example.
if it works fine, return to your master branch and merge this branch with the migration.
Execute npm i in order to install the dependencies you added from your branch.
In my app js
app.use(express.static(join(__dirname, '/dist')))
Webpack config:
module.exports = {
entry: "./src/main.js",
output: {
path: join(__dirname, "/dist/js/"),
filename: "bundle.js",
publicPath: "/"
},
mode: "development"
// ...
};
In my footer.hbs
<script src="/js/bundle.js"></script>
In my package.json Scripts
"startw": "webpack-dev-server --open",
"server": "node app.js",
When I make a change in any js file, webpack is compiling but it does not overwrite /js/bundle.js. How can I make webpack recompile my changed bundle js to overwrite dist/js/bundle.js ?
I have tried
https://webpack.js.org/guides/development/#using-webpack-dev-server
and
webpack-dev-middleware
For production, I use
"dev": "./node_modules/.bin/webpack -p --watch",
This compiled as expected to dist/js/bundle.js but this is tedious to run it each time I make a simple change to a js file.
I want to inject the auto compiled output js to use it in my app. I dont mind refreshing the page myself and do not care if I dont get to use HMR.
I am using this react redux starter kit https://github.com/coryhouse/pluralsight-redux-starter and i am getting the following outputs in my console
[HMR] connected
I tried to toggle the following fields in the web-pack config to true and false but it doesn't help
noInfo: true,
debug: false,
How can i turn these logs off?
Tried many solutions, the dev server option clientLogLevel: "none" had the closest meaning. But couldn't get it working.
Then I found a workaround:
Run your app with NODE_ENV=development; for that go to package.json and update scripts:
"scripts": {
"build": "webpack",
"start": "NODE_ENV=production webpack-dev-server --progress --inline --hot",
"dev": "NODE_ENV=development webpack-dev-server --progress --inline --hot"
}
modify webpack.config.js:
plugins: [
new webpack.DefinePlugin({
__DEVELOPMENT__: process.env.NODE_ENV !== 'production',
})
],
The DefinePlugin allows you to create global constants which can be
configured at compile time.
OR
Replace global.console.log with a custom console_log that filters out [HMR] and [WDS] logs. See this Github comment.
Add the below code to your development.js (a file which is conditionally appended to the entry in webpack.config.js based on the value process.env.NODE_ENV !== 'production')
(function(global) {
var console_log = global.console.log
global.console.log = function() {
if (!(
arguments.length == 1 &&
typeof arguments[0] === 'string' &&
arguments[0].match(/^\[(HMR|WDS)\]/)
)) {
console_log.apply(global.console,arguments)
}
}
})(window)
npm run dev
If you've used DefinePlugin, then we now have global constant __DEVELOPMENT__ === true on client side. So on every refresh you can do this:
if (__DEVELOPMENT__) {
console.clear();
}
On version >= 4.0.0 of Webpack You can keep the hot reload feature but limit the logs to errors under devServer like below:
devServer: {
hot: true,
client: {
overlay: false,
logging: 'error',
}
...
}
You can find more info about it on https://webpack.js.org/configuration/dev-server/#logging