- 1. EditorConfig
- 2. Install Webpack
- 3. Config Webpack
- 4. Scripts
- 5. Create a css file and import it
- 6. Load CSS & SASS
- 7. Extract CSS
- 8. Minimize CSS
- 9. Don't output js comments
- 10. Load HTML
- 11. Cache busting
- 12. Delete previous build files
- 13. Load images & fonts
- 14. Minimize image
- 15. Custom output path
- 16. Add multiple entry points
- 17. Separate main and vendor
- 18. Bundle size
- 19. DON'T Mangle properties' names
- 20. EsLint & Webpack integration
- 21. eslint-plugin-compat
- 22. Prettier install
- 23. Prettier & EsLint integration
- 24. StyleLint install
- 25. Stylelint & Webpack integration
- 26. Solving StyleLint & Prettier conflicts
- 27. Install Stylelint plugin
- 28. stylelint-scss
- 29. husky & lint-staged
- 30. Install babel
- 31. babel config files
- 32. Babel's useBuiltIns
- 33. browserslist & babel
- 34. @babel/eslint-parser
- 35. React
- 36. Config React in Babel
- 37. Config React in EsLint
- 38. Add styled-components
- 39. CSS modules
- 40. Jest
- 41. Jest Babel
- 42. Jest & eslint runner
- 43. Jest handling assets
- 44. Typescript
- 45. Typescript with css modules
- 46. Eslint & Typescript
- 47. Jest & Typescript
- you may need to install a plugin, depending on the editor
.editorconfig
at the root:
# https://EditorConfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
npm init -y
npm i -D webpack webpack-cli webpack-dev-server
npm i -D webpack-merge
webpack.common.js
:
module.exports = {
entry: "./src/index.js",
};
// esm
export default {
entry: "./src/index.js",
};
webpack.dev.js
:
const path = require("path");
const { merge } = require("webpack-merge");
const common = require("./webpack.common");
module.exports = merge(common, {
mode: "development",
devtool: "source-map",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
devServer: {
// contentBase: "./dist", // contentBase was deprecated in favor of the static option at webpack-dev-server v4
static: './dist',
open: true,
// makes server accessible externally via `your-ip:8080`
// tip: get ip with `ip address | grep 192.168.`
// host: "0.0.0.0",
}
});
// esm
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
import { merge } from 'webpack-merge';
import common from './webpack.common.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
export default merge(common, {
mode: "development",
devtool: "source-map",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
devServer: {
// contentBase: "./dist", // contentBase was deprecated in favor of the static option at webpack-dev-server v4
static: './dist',
open: true,
// makes server accessible externally via `yourip:8080` (get ip with `ip address | grep 192.168.`)
host: "0.0.0.0",
},
});
webpack.prod.js
:
const path = require("path");
const { merge } = require("webpack-merge");
const common = require("./webpack.common");
module.exports = merge(common, {
mode: "production",
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
}
});
// esm
import path, { dirname } from "path";
import { fileURLToPath } from 'url';
import { merge } from "webpack-merge";
import common from "./webpack.common.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
export default merge(common, {
mode: "production",
output: {
path: path.resolve(__dirname, "dist"),
filename: "main.[contenthash].js",
},
});
- at
package.json
:
"scripts": {
"start": "webpack serve --config webpack.dev.js",
"build": "webpack --config webpack.prod.js"
},
- create a css/sass file
- in the js file:
import "./style.css"
npm i -D style-loader css-loader sass-loader sass
- in
webpack.common.js
:
module: {
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
{
test: /\.s[ac]ss$/i,
use: ["style-loader", "css-loader", "sass-loader"],
},
],
}
- move
style-loader
inwebpack.common.js
towebpack.dev.js
npm i -D mini-css-extract-plugin
webpack.prod.js
:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module: {
rules: [
{
test: /\.css$/,
use: [{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '', // NOTE: publicPath is required
},
}, "css-loader"],
},
{
test: /\.s[ac]ss$/,
use: [{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: '', // NOTE: publicPath is required
},
}, "css-loader", "sass-loader"],
},
],
},
plugins: [new MiniCssExtractPlugin()],
npm i -D css-minimizer-webpack-plugin
- at
webpack.prod.js
;
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
optimization: {
minimize: true,
minimizer: [
`...`, // // this is the minimizer overridden value that minimizes javascript
new CssMinimizerPlugin(),
],
},
- at
webpack.prod.js
;
const TerserPlugin = require('terser-webpack-plugin');
optimization: {
minimizer: [
new TerserPlugin({
extractComments: false, // don't extract comments to separate file
terserOptions: {
format: {
comments: false, // avoid build with comments
},
},
}),
new CssMinimizerPlugin(),
],
}
npm i -D html-webpack-plugin
- NOTE: delete any script/style tag in the template.html
webpack.common.js
:
const HtmlWebpackPlugin = require("html-webpack-plugin");
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
}),
],
- you need to be using
HtmlWebpackPlugin
, this will automatically insert the script tag in the html - in
webpack.prod.js
:
output: {
path: path.resolve(__dirname, "dist"),
filename: "main-[contenthash].js",
},
// ...
plugins: [new MiniCssExtractPlugin({
filename: "style.[contenthash].css",
})],
npm i -D clean-webpack-plugin
- in
webpack.prod.js
:
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
plugins: [new CleanWebpackPlugin()],
npm i -D html-loader
webpack.common.js
:
{
module: {
rules: [
{
test: /\.html$/i,
loader: 'html-loader', // if not included, webpack won't parse html and import images/fonts
},
{
test: /\.(png|jpe?g|gif)$/,
type: "asset/resource", // emits a separate file (replaced file-loader)
},
{
test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
type: "asset/inline", // inserts inline (replaced url-loader)
},
],
},
}
npm i -D image-minimizer-webpack-plugin
- install format plugins
- lossless probably won't compress much
- lossy will compress much more
import ImageMinimizerPlugin from 'image-minimizer-webpack-plugin';
export default = {
module: {
rules: [
{
test: /\.(jpe?g|png|gif|svg)$/i,
type: 'asset/resource',
},
],
},
plugins: [
new ImageMinimizerPlugin({
minimizerOptions: {
// Lossless plugins
plugins: [
['gifsicle', { interlaced: true }],
['jpegtran', { progressive: true }],
['optipng', { optimizationLevel: 5 }],
[
'svgo',
{
plugins: [
{
removeViewBox: false,
},
],
},
],
],
},
}),
],
};
- image filename hash is incorrect inside html
- webpack-contrib/image-minimizer-webpack-plugin#187
- in
webpack.prod.js
, at theoutput
object:assetModuleFilename: "assets/img/[name].[hash][ext]",
- likewise, in
webpack.dev.js
:assetModuleFilename: '[name][ext]',
- in
webpack.common.js
:
entry: {
// name: path
main: "./src/index.js",
other: "./src/other.js"
},
- in
webpack.prod.js
:
output: {
filename: "[name].[contenthash].js",
path: path.resolve(__dirname, "dist"),
},
- in
webpack.dev.js
:
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
},
- in
webpack.prod.js
:
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: "vendor",
chunks: "all",
// force the creation of this chunk
// by ignoring splitChunks default properties
// (minSize, minChunks, maxAsyncRequests, maxInitialRequests)
// enforce: true,
},
},
},
},
npm i -D webpack-bundle-analyzer
webpack.prod.js
:
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
plugins: [
new BundleAnalyzerPlugin()
]
- terser docs: "THIS WILL BREAK YOUR CODE"
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
mangle: {
properties: true, // DON'T DO THIS
}
}
}),
],
},
npm i -D eslint eslint-webpack-plugin
npx eslint --init
webpack.common.js
:
const ESLintPlugin = require('eslint-webpack-plugin');
module.exports = {
// ...
plugins: [
new ESLintPlugin({
fix: true,
}),
],
// ...
};
.eslintrc.cjs
:
// https://eslint.org/docs/user-guide/configuring/configuration-files#configuration-file-formats
// use .eslintrc.cjs when running ESLint in JavaScript packages that specify `"type": "module "`
// Note that ESLint does not support ESM configuration at this time
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'airbnb-base',
],
plugins: [
],
parserOptions: {
ecmaVersion: 2021, // equivalent to 12
sourceType: 'module',
},
rules: {
// // this rule enforce or disallow the use of certain file extensions
// // when `"type": "module"` in `package.json`, must always contain extension
// 'import/extensions': [
// 'error',
// 'always',
// ],
// // otherwise, if `commonjs`, extensions can be omitted
// 'import/extensions': [
// 'error',
// 'ignorePackages',
// {
// js: 'never',
// jsx: 'never',
// ts: 'never',
// tsx: 'never',
// },
// ],
},
settings: {
// fix 'import/no-unresolved' error
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
},
},
},
overrides: [
{
files: ['webpack.*.js'],
rules: {
'import/no-extraneous-dependencies': [
'error',
{ devDependencies: true },
],
},
},
],
};
- not really necessary when babel's useBuiltIns is enabled
- because polyfills are added automatically
- emit error if target environment (at
browserslistrc
) doesn't support specific feature - only lints high level built-in API (e.g.
Promise
,Set
) - doesn't lint syntactical features (e.g.
class
) or methods (e.g.Array.prototype.includes
)
npm i -D eslint-plugin-compat
eslintrc.cjs
:
{
// ...
"extends": ["plugin:compat/recommended"],
}
npm i -D prettier
.prettierrc.cjs
module.exports = {
singleQuote: true,
};
.prettierignore
:dist/
npm i -D eslint-config-prettier eslint-plugin-prettier
eslint-config-prettier
turns off all rules that conflict with Prettiereslint-plugin-prettier
reports Prettier as Eslint issues
.eslintrc.js
:
{
extends: [
"some-other-config-you-use",
"prettier"
],
// ...
plugins: ["prettier"],
rules: {
'prettier/prettier': 'error',
},
}
npm i -D stylelint stylelint-config-standard
.stylelintrc.cjs
:
{
"extends": "stylelint-config-standard"
}
npm i -D stylelint-webpack-plugin
webpack.common.js
:
const StylelintPlugin = require('stylelint-webpack-plugin');
module.exports = {
// ...
plugins: [new StylelintPlugin({
context: "./src", // directory to search for sass/css files
fix: true,
})],
};
npm i -D stylelint-prettier stylelint-config-prettier
stylelint-prettier
= reports prettier as a stylelint issuesstylelint-config-prettier
= disable rules that conflict with prettier
- NOTE: "stylelint-config-prettier" must be the last at extends array
.stylelintrc.cjs
:
{
"extends": [
"stylelint-config-standard",
"stylelint-config-prettier"
],
"plugins": [
"stylelint-prettier"
],
"rules": {
"prettier/prettier": true
}
}
npm i -D stylelint-order
.stylelintrc.cjs
:
module.exports = {
plugins: [
"stylelint-order".
],
rules: {
"order/properties-alphabetical-order": true,
},
};
npm i -D stylelint-scss
.stylelintrc.js
:
{
"plugins": [
"stylelint-scss"
],
"rules": {
// recommended rules
"at-rule-no-unknown": null,
"scss/at-rule-no-unknown": true,
}
}
npx mrm lint-staged
= install husky & lint-staged; set to always run linters before commitnpx husky add .husky/pre-commit "npm test -- --passWithNoTests --selectProjects jest"
= always run tests before commit- erase
lint-staged
property created atpackage.json
lint-staged.config.cjs
:
// lint-staged will automatically add any fix to the commit
// that's why the linting of test files is done here
module.exports = {
'*.(js|jsx|cjs|ts|tsx)': `eslint --fix`, // including test files
'*.(css|scss|sass)': 'stylelint --fix',
'*.(html|md)': 'prettier --write',
};
Manual install
npm i -D husky lint-staged
npm set-script prepare "husky install" && npm run prepare
npx husky add .husky/pre-commit "npx lint-staged"
npx husky add .husky/pre-commit "npm test -- --passWithNoTests --selectProjects jest"
- add same lint-staged config as above
npm install -D babel-loader @babel/core @babel/preset-env
- in
webpack.prod.js
:
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
}
}
]
}
- NOTE: when no browserslist configs are specified: preset-env will transform all ES2015-ES2020 code to be ES5 compatible
babel.config.json
:
{
"presets": ["@babel/preset-env"]
}
- babel by itself will only transpile syntax (e.g. arrow functions, spread operator)
- new built-in APIs like objects (e.g.
Promise
) and methods (e.g.Array.prototype.includes
) require polyfill
npm i core-js
- at
babel.config.json
:
{
"presets": [
[
"@babel/preset-env",
{
// 'usage' = add the polyfills needed automatically
// 'entry' = requires explicit import of core-js
"useBuiltIns": "usage",
"corejs": {
"version": "3.8", // change it to the last version
"proposals": true
}
}
]
]
}
babel-loader
only atwebpack.prod.js
(for project without react)- file:
.browserslistrc
defaults
not IE 11
babel-loader
must be moved towebpack.common.js
tobrowserslist
settings be applied in both production and dev- in practice, there's usually no need to apply babel in development;
- unless you going to use react that requires babel at development
package.json
, at scripts:"start": "webpack serve --node-env development --config webpack.dev.js"
,"build": "webpack --node-env production --config webpack.prod.js"
.browserslistrc
[development]
last 2 chrome versions
# browserslist's "defaults" is different from
# default behavior of @babel/preset-env in babel v7
#
# if you want to specify targets only for development
# and use babel's default in production,
# you must delete this file and add to package.json:
#
# "browserslist": {
# "development": [
# "last 2 chrome versions"
# ]
# }
[production]
defaults
not IE 11
- NOTE:
- webpack-dev-server is not yet 100% ready for webpack 5
- a bug (automatic reload doesn't happen) when having a
.browserslistrc
or an array attarget
in the webpack config - the workaround is to set
target: web
in the development environment (which overrides default, i.e..browserslistrc
)
- current project's browserslist =
npx browserslist
- browsers list from query =
npx browserslist "last 2 versions, not dead"
- coverage =
npx browserslist --coverage "defaults"
- coverage by countries =
npx browserslist --coverage=global,US,BR "defaults, not ie 11, not op_mini all"
- preset-env's behavior is different than browserslist's "defaults"
defaults
is the same asnpx browserslist "> 0.5%, last 2 versions, Firefox ESR, not dead"
npm i -D @babel/eslint-parser
= enables eslint support for experimental features (e.g. class properties).eslintrc.js
:parser: '@babel/eslint-parser'
.babelrc.cjs
:plugins: [ '@babel/plugin-proposal-class-properties' ],
npm i react react-dom
npm i -D @babel/preset-react
- at
babel.config.json
add"@babel/preset-react"
as the last element in thepresets
array - move the babel loader from
webpack.prod.js
towebpack.common.js
npm i -D eslint-plugin-react
- at
.eslintrcjs
:
extends: [
'airbnb-base',
"prettier",
"plugin:react/recommended", // must always be the last
],
settings: {
"react": {
"version": "detect"
}
},
npm i styled-components
npm i -D babel-plugin-styled-components
= for minification and a better debugging experience- add to
babel.config.json
:"plugins": ["babel-plugin-styled-components"]
- css modules are enabled with a option (
modules
) passed to thecss-loader
- however it defaults to true if file matches
/\.module\.\w+$/
- this means that
*.module.css
and*.module.scss
modules are load by default
npm i -D jest
npx jest --init
or createjest.config.js
- babel-jest is automatically installed when installing Jest
- will automatically transform files if a babel configuration exists in your project
npx jest
will run eslint as a jest test suite
npm i -D eslint-plugin-jest
.eslintrc.cjs
:"plugins": ["jest"]
,"extends": ["plugin:jest/all"]
npm i -D jest-runner-eslint
jest.config.js
:
{
projects: [
{
displayName: 'jest',
},
{
runner: 'jest-runner-eslint',
displayName: 'eslint',
},
],
watchPlugins: ['jest-runner-eslint/watch-fix'],
}
.jest-runner-eslintrc.json
:
{
"cliOptions": {
"fix": true
}
}
- you'll get a error if you import anything that imports any other thing other than a
js
file (jest can only parse js)
- at root, create
__mocks__/fileMock.js
- content:
module.exports = 'test-file-stub';
- content:
npm i -D identity-obj-proxy
jest.config.js
:
// NOTE: if not using jest-runner-eslint
// `moduleNameMapper` goes instead inside the root of the module.exports
projects: [
{
displayName: 'jest',
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|s[ac]ss)$": "identity-obj-proxy", // handles both regular css and css modules
},
},
]
-
npm i -D typescript
-
npx tsc --init
- open the file and edit:
"module": "es2015"
- open the file and edit:
-
npm i -D @types/react @types/jest
-
from here, you have 2 options:
- ts-loader: to compile typescript with ts-loader, not with babel
- babel typescript preset: to compile typescript with babel
npm i -D ts-loader
npx tsc --init
, edit:
{
"compilerOptions": {
"target": "es2015",
"module": "es2015",
"jsx": "react",
"strict": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
webpack.common.js
, add tomodule.rules
:
{
test: /\.tsx?$/,
loader: "ts-loader",
},
npm i -D fork-ts-checker-webpack-plugin
= speed up compilation by moving type checking and (optionally) EsLint to a separate processwebpack.config.js
:
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
// module.rules
{
test: /\.tsx?$/,
loader: 'ts-loader',
exclude: /node_modules/,
options: {
transpileOnly: true
}
}
plugins: [
new ForkTsCheckerWebpackPlugin({
// // optional - move lint to a separate process
// eslint: {
// files: './src/**/*.{ts,tsx,js,jsx}',
// formatter: 'basic', // smaller output
// // formatter: { type: 'codeframe', options: { linesAbove: 0, linesBelow: 0 }},
// },
}),
];
@babel/preset-typescript
can't do type checking, its only job is to compile from ts to js (even if there're errors)- you can type check by:
- run
tsc -w
in a separate terminal tab - or use an editor which supports typescript and will highlight errors (e.g. vscode)
- run
npm i -D @babel/preset-typescript
- at
babel.config.js
add topresets
array:@babel/preset-typescript
- at
webpack.common.js
, add to module.rules:
{
test: /\.(js|ts)x?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
tsconfig.json
:
{
"compilerOptions": {
"target": "es2020", // babel will transpile
"module": "es2015",
"allowJs": true,
"jsx": "react",
"noEmit": true,
"strict": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
// make `tsc` throw an error if an unsupported feature is used
// if set true all implementation files must be modules (import or export something)
"isolatedModules": true
}
}
- while using
webpack 5
and the last versions ofwebpack-dev-server
- warnings related to using features not available in the set
target
&lib
doesn't appear - while using
ts-loader
&@babel/preset-typescript
this behavior may be expected, but this also occurs when usingtsc
src/global.d.ts
:
declare module '*.module.css' {
const styles: { [key: string]: string };
export default styles;
}
declare module '*.module.sass' {
const styles: { [key: string]: string };
export default styles;
}
declare module '*.module.scss' {
const styles: { [key: string]: string };
export default styles;
}
npm install -D typescript-plugin-css-modules
- in vscode, when a js or ts file is opened:
ctrl + shift + p
, selectselect typescript version
- select
use workspace version
tsconfig.json
:
{
"compilerOptions": {
"plugins": [{ "name": "typescript-plugin-css-modules" }]
}
}
npm i -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
- at
webpack.common.js
,ESLintPlugin
options: addextensions: ['js', 'jsx', 'ts', 'tsx'],
.eslintrc.js
:
overrides: [
{
files: ['*.ts', '*.tsx'],
extends: [
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/recommended'
],
parser: '@typescript-eslint/parser',
parserOptions: { project: './tsconfig.json' },
plugins: [ '@typescript-eslint' ],
rules: {
// disable some base rules and enable their typescript-eslint equivalents (Extension Rules) to prevent incorrect errors
// https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/FAQ.md#i-am-using-a-rule-from-eslint-core-and-it-doesnt-work-correctly-with-typescript-code
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': ['error'],
'no-shadow': 'off',
'@typescript-eslint/no-shadow': ['error'],
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error'],
// fix 'missing file extension' error
'import/extensions': [ 'error', 'never' ],
// fix 'no-extraneous-dependencies' error in test files
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: [
'**/__tests__/**',
'**/*{.,_}{test,spec}.{ts,tsx}',
],
},
],
},
},
],
- when using
ts-loader
, you needts-jest
to run tests written in Typescript - otherwise, if using
@babel/preset-typescript
:babel-jest
(which is installed automatically withjest
) will automatically transform files if a babel config exists- https://jestjs.io/docs/next/code-transformation#defaults
jest.config.js
:
module.exports = {
projects: [
{
displayName: 'jest';
// ...
preset: 'ts-jest', // write tests in typescript
globals: {
'ts-jest': {
tsconfig: 'tsconfig.jest.json',
},
},
transform: {
"^.+\\.jsx?$": "babel-jest", // to also be able to write tests in javascript
"^.+\\.tsx?$": "ts-jest",
},
}
// ...
],
// ...
};
tsconfig.jest.json
:
{
"extends": "./tsconfig.json",
"compilerOptions": {
// 'allowSyntheticDefaultImports' is recommended when using webpack
// but 'esModuleInterop' is best suited for Node (e.g. jest)
// https://github.com/typescript-cheatsheets/react#import-react
"esModuleInterop": true
}
}