The project is actually only built with the latest Babel-features + Webpack4 + jest for unit-tests.
This documentation is not intended to show how to build a React application with this configuration, but to explain each unit and their meaning of the used configurations. I will try to document every config-line used for this setup.
see the example in the
src
-folder, in principle each feature is illustrated by a mini example.
git clone https://github.com/PutziSan/TS-JS-React-Webpack-babel7-setup.git [NEW_PROJECT_NAME]
cd [NEW_PROJECT_NAME]
git remote remove origin
yarn
- mix TypeScript and JavaScript with ES6-Imports via babel7
- Tree-Shaking and Code-Splitting via dynamic
import()
- jest-test using quasi-same babel-config
- Hot-Module-Replacement for your React-Components via react-hot-loader on development
- (...) => Basically everything you know from Create-React-App (CRA), only significantly slimmer implemented via babel7
- babel
- webpack
- jest
- react-hot-loader
- TypeScript
- npm-/yarn-scripts
- development-utilities
- overwiev dependencies and devDependencies
- To Observe
babel7 is used equally for all build+test+develop.
Babel is configured via the babel.config.js-file. The process.env.NODE_ENV
-variable is used to determine which plugins and presets should be added.
Presets are a predefined set of plugins, see babel-dependencies for the individual presets/plugins we use.
- Plugins run before Presets
- Plugin ordering is first to last
- Preset ordering is reversed (last to first)
- add @babel/plugin-transform-runtime, see babel-helper-functions via babel-runtime for smaller bundle-size
- turns off the
development
-option in @babel/preset-react
- do not include the @babel/preset-env, you will use a new browser for dev, so no need to waste compile-time with this preset during dev
- add react-hot-loader/babel, look at react-hot-loader for more information.
Customizations for jest, since jest ES6 cannot import
/export
and does not understand dynamic imports, see jest-documentation:
- ES6-exports to commonjs:
@babel/preset-env
will be adapted from"modules": false
to"modules": "commonjs"
- dynamic imports: use babel-plugin-dynamic-import-node by AirBnb instead of @babel/plugin-syntax-dynamic-import
Babel injects small helper-functions like _extend
, when needed. With @babel/plugin-transform-runtime the code is not copied in every file, but the transform-runtime-plugin will inject referneces to the @babel/runtime-package, which holds the implementations of the helper-functions ("Helper aliasing"). This will result in a smaller bundle. The "useESModules": true
-option will use ES6-modules (import
/export
) instead of the implementations with commonjs (require
/module.exports
).
Please note that the @babel/plugin-transform-runtime can also perform other transformations:
corejs
will polyfill new built-ins (Promise, Map, ...) via the core-js-libraryregenerator
will transform generator-functions (function*
) into ES5-Code with a local regenerator-runtime
In my opinion it is not a good idea to use these options, because the inserted transformations can take up a lot of space and it is very likely that others also use polyfills, so it may be that a feature is polyfilled by several different libraries which bloats your bundle. If you are developing a library, it is best not to use features that require polyfills at all. If really necessary, use ponyfills and document the use.
In src/index.tsx
the first line loads a polyfill-script (import './bootstrap/polyfills';
), so that the app also runs under Internet Explorer 11 (IE11). Following polyfills are included:
Promise
via this promise-library (Promises are required for code-splitting via dynamic imports)Object.assign
via this object-assign-library (Object.assign
is needed for the @babel/plugin-proposal-object-rest-spread with theuseBuiltIns
-option and in some libraries (e.g. react-loadable) but not included in IE11)
Both polyfills together increase the bundle-size by ~ 5kb. If you think you do not need this polyfills you can remove them. If you need other polyfills, because you use new features or have to support very old browsers, you should attach them in src/bootstrap/polyfills.js
.
To stop thinking about polyfills you can automate this process with babel-polyfill. Similar to @babel/plugin-transform-runtime with the corejs
-option, polyfills via core-js are added for older browsers. In contrast to runtime, the polyfills are loaded globally into the application (which is not recommended for libraries).
You can then additionaly use the useBuiltIns
-option of the babel-preset-env:
useBuiltIns: 'usage'
: Adds specific imports for polyfills when they are used in each file. We take advantage of the fact that a bundler will load the same polyfill only once. Be aware that this will not polyfill usages innode_modules
useBuiltIns: 'entry'
: You need to import@babel/polyfill
in your entry-file once, babel will transform this import to only include imports needed by the specified preset-envtarget
-option; At the moment (as of 13.08.2018) this is forbrowsers: ['>0.25%']
still over 80 kb
I would not recommend the use of babel-polyfill since:
- either significantly too many polyfills are imported (library standalone or with
useBuiltIns: 'entry'
) or - using
useBuiltIns: 'usage'
the polyfills are incosistent (they are included locally per file but change the global namespace) and only functions used in your code are analyzed (since usednode_modules
s are not examined), außerdem ist auch mit dieser Methode- Also with this method the resulting package is bigger than if you install the polyfills yourself as described above
- For the example app only the two polyfills mentioned above are needed to run under IE11, the bundle size was still ~ 14 kb bigger and I had to install the required imports manually into
index.ts
, because preset-env did not recognize that for the dynamicimport()
Promise must polyfill and for React-LoadableObject.assign
package | description |
---|---|
@babel/core | peer-dependency for everything else |
@babel/plugin-proposal-class-properties | see proposal, so that ES6 class fields can not only be set in the constructor; the "loose": true -option will assign the properties via assignment expressions instead of Object.defineProperty which results in less code |
@babel/plugin-proposal-object-rest-spread | The Object-React-Operator (... ) is not natively supported by preset-env.The rest operator works in TypeScript files without this transformation because the TypeScript compiler has been compiling it to ES5 code since TypeScript 2.1. The useBuiltIns: true -Option transforms it to Object.assign -calls, make sure to include a polyfill for older browsers (IE 11) |
@babel/plugin-syntax-dynamic-import | only Syntax-Plugin! so babel understands dynamic imports, which webpack uses for code-splitting |
@babel/plugin-transform-runtime | Babel-helpers and -polyfills will use @babel/runtime, without this babel copies the needed helper into every single file when needed, for more information visit their documentation; the "useESModules": true -option will use ES6-modules with import /export |
@babel/runtime | Babel will inject this dependency in your code when needed via @babel/plugin-transform-runtime, see upper line |
@babel/preset-env | ES>5 to ES5, should always run as the last transformation, so it should always remain the first presets -entry |
@babel/preset-react | JSX to ES6;useBuiltIns -option will passed through the useBuiltIns -option in @babel/plugin-transform-react-jsx => "When spreading props, use Object.assign directly instead of Babel's extend helper."development -option will add debug-informations, turned off in production |
@babel/preset-typescript | TS/TSX to ES6/JSX |
babel-plugin-dynamic-import-node | the only one not by babel itself but by airbnb, only for jest-tests, see jest-declaration |
package | description |
---|---|
babel-core@7.0.0-bridge.0 | for jest-test, siehe jest-doku |
babel-jest | so that jest also uses the babel-transformations |
babel-loader@8.0.0-beta.4 | to transform files via webpack, new babel-load-v8 must be used with new babel7 |
react-hot-loader/babel | babel extension for hot-loading to work with react, see react-hot-loader |
webpack is our bundler and development-server. It is configured via webpack.config.js
.
config | description |
---|---|
entry | An object where the key represents the output-name of the generated js-file (this will applied to [name] in output.filename ) and the value is the location of your entry-point, see Entry Points - object-syntax (webpack-documentation) |
output.filename |
js/ is used to collect all js-files in the js -sub-dir, see this discussion see caching of your assets for information about [contenthash:8] |
output.path |
The folder specified here is used as the target where it should output your bundles, assets and anything else you bundle or load with webpack. If you want to collect your js-files in a single directory use output.filename (see above) |
output.chunkFilename |
chunks are generated when using JavaScripts dynamic imports, see webpack-documentation |
output.publicPath |
It is important that you set publicPath with a trailing slash so that the paths are not set relative to the current route (note that the default value is an empty string ("" )). For example, the (html-webpack-plugin) would then insert <script src="main.js"> and your browser would search for main.js relative to the current route, but we want <script src="/main.js"> (note the leading slash) so that the JS file is always searched in the root. |
mode | sets default-plugins (siehe link) and replaces NODE_ENV to production/development, see webpack#optimization to check what the production -mode does |
devtool | defines how source-maps are written, eval gives best performance, but incorrect line numbers, for our project eval-source-map is the best compromise between correct line numbers and performance |
resolve.symlinks | false , cause it can cause to unexpected behavior (e.g. it could apply a module-rule even you excluded it if the symlinked path is outside of the exclusion) |
module | 1. runs babel for every file (see more babel-dependencies) 2. we can import static files (img/pdf), which are converted to an url and added to the bundle as an external file see webpack-dependencies#file-loader |
devServer | hot: true see react-hot-loader; contentBase: 'public' so the dev-server recognizes the static assets which are in /public |
optimization.minimizer | set explicit, cause the default-config via mode does not remove comments also added collapse_vars: false , as it has caused considerable performance losses (uglifyjs-issue, terser-issue, cra-issue), but only 3KiB savings in a > 5 MiB (output-bundle-size) project (Apoly) |
plugins | webpack.EnvironmentPlugin : see Environment-variableswebpack.HotModuleReplacementPlugin see react-hot-loadersee webpack-dependencies for other plugins |
setup:
- css-loader with a seconds plugin to add the css to the dom (see webpack-dependencies style-loader/mini-css-extract-plugin) as
module.rules
-entry - optimize-css-assets-webpack-plugin as
optimization.minimizer
-entry to minimize css for production - mini-css-extract-plugin for production to emit the computed css-bundles as CSS-files
make sure to include common file-formats (eg
woff
,woff2
, ...) in your file-loader so that the css-files can load them, if not this will raise errors
Hash functions (to put it simply) always output the same result on repeated calls with a string of any length (they are deterministic). I use a
:8
-suffix (like in[name].[contenthash:8].js
) so that the names don't get too long and even at this length it is unlikely to get the same result for 2 files. If this is too uncertain, you can adjust or remove the number
Hashes are used to ensure that unmodified bundles keep the same name for successive builds and changed files get a new bundle name. Thus browsers can effectively cache files by file name. Caching is configured via different plugins and config-entries:
- for your bundled JS-files:
output.filename
([name].[contenthash:8].js
) andoutput.chunkFilename
([id].[contenthash:8].js
) - for your bundled CSS-files:
filename
([name].[contenthash:8].js
) andchunkFilename
([id].[contenthash:8].js
) via mini-css-extract-plugin - for other static assets:
options.name
([name].[hash:8].[ext]
) in file-loader-options
during development you should not enable any hashing of your filenames (This can be neglected for CSS-files via mini-css-extract-plugin as it is only used in production anyway)
Have a look at webpacks caching-guide for more information.
If you have build-performance concerns, you should start with webpacks build-performance-article on github and the build-performance-section on their website to learn what you can optimize.
The webpack.EnvironmentPlugin
replaces the specified keys, so that process.env.[KEY]
is replaced with the actual JSON
-representation of the current process.env.[KEY]
in your Environment.
It is configured so that every key from process.env
in the current node-process (via CLI, .env
-file, CI, ...) will be available in your bundle.
Make sure to not use sensitive environment-variables in your frontend-project!
Additional all environment variables that you have defined in a .env
-file will be available in process.env
using the dotenv-library.
package | description |
---|---|
webpack | core-bundler with node api |
webpack-cli | webpack via cli |
webpack-dev-server | webpack-dev-server for development |
file-loader | import static assets in js |
css-loader | with this it is possible to import css-files in js-files, a further plugin is needed to append the imported css to the dom (inline or via external css), see below |
style-loader | this injects the styles inline to the DOM, it is not recommended for production, but for development cause this plugin supports HMR, in production use mini-css-extract-plugin |
mini-css-extract-plugin | this will grab the css-files from css-loader (see above) and writes them in an external css-file per bundle, which is bether for production cause of smaller js-bundles and caching-abilities for the css-files |
optimize-css-assets-webpack-plugin | minifies your bundled CSS-files via nano |
html-webpack-plugin | This will use your src/index.html -template and injects a <script> -tag with src to your generated entry-bundle |
uglifyjs-webpack-plugin | enables us to use this plugin with other options then defaults, see webpack.config.js ("optimization.minimizer") |
dotenv | "Loads environment variables from .env " |
babel-loader@8.0.0-beta.4 | see further babel-dependencies |
Jest is translated via babel-jest via babel to es5 and made usable, for special babel settings see babels test
-specific options (NODE_ENV
to test
is set by jest).
config | description |
---|---|
setupTestFrameworkScriptFile | dev/setupTests.js configures jest-enzyme |
testRegex | all tests have to lay inside tests |
moduleFileExtensions | test these extensions for import or require , corresponds to resolve.extensions in webpack.config.js |
moduleNameMapper | mock static assets (img, CSS) see jest-doku - Handling Static Assets |
transform | pass every file through babel |
jest-enzyme by AirBnB is integrated via dev/setupTests.js
, which is linked to jest in setupTestFrameworkScriptFile
(see jest.config.js
).
jest-enzyme adds more matcher
functions for jests expect
. For an overview of these functions, see enzyme-docu for enzyme-matchers.
package | description |
---|---|
jest | test-module for js/react by facebook |
jest-enzyme | adds enzyme-matchers to jests expect |
enzyme | Test-utils for rendering and mounting React-Components |
enzyme-adapter-react-16 | adapter for enzyme that it can mount and shallow-render React-16-Components |
react-hot-loader is a tool by Gaeron (Dan Abramov) to enable Hot Module Replacement (HMR).
HMR means that the page is not completely reloaded when changes are made, but that the JS modules are "replaced" internally.rden
Since Babel7 the integration with TypeScript is much easier, because Babel understands TypeScript and therefore the babel-extension of react-hot-loader can be used easily.
currently the usage is not documented here yet in react-hot-loader, but the PR that explains this already exists
- Root-Component (
src/components/App.tsx
) is wrapped with thehot
-HOC. - for development the
react-hot-loader/babel
-plugin is enabled (see babelsdevelopment
-specific options) - in
webpack.config.js
, thedevServer.hot
-prop is set totrue
and thewebpack.HotModuleReplacementPlugin
is enabled, see webpacks HMR-guide
TypeScript (TypeScript GitHub-repo) is build via babels @babel/preset-typescript.
See TS-doku#compiler-options, below only things worth explaining are mentioned:
config | value | description |
---|---|---|
moduleResolution | node |
TLDR: node is nowadays default and bether |
module | esnext |
since we also use ES6-import /export js, it makes sense to keep this also for TS (esnext so that TS-compiler does not complain about dynamic imports) |
target | es6 |
since TS is going through babel again anyway, the ES6 |
jsx | preserve |
preserve means that JSX is not converted to React.createElement , this is done by the babel compiler. |
lib (search for --lib ) |
["es6", "dom"] |
"List of library files to be included in the compilation." |
sourceMap | false |
since webpack writes the sourcemaps for us it can be neglected by TS, see webpack.config.js |
allowJs | true |
allows import and export of JS without compiler-errors |
Scripts defined in package.json
and executed via yarn
or npm run
:
cross-env NODE_ENV=development webpack-dev-server --open
Sets NODE_ENV
to development
(OS-agnostic via cross-env), and starts the webpack-dev-server, which uses your webpack.config.js
(more information under #webpack). In addition, your default browser will open with http://localhost:3000
per default we are using
devtool: 'eval'
for thestart
-command. according to webpack the performance can be improved considerably, but the line-number is not correct in case of errors. If you need to debug with correct line-numbers you can useyarn start:debug
Sets the --devtool eval-source-map
-flag, see webpacks documentation, the eval-source-map
-flag is the one with the best performance while sending correct line-numbers (the other flags does not work correct cause we use typescript)
"rimraf dist && ncp public dist && cross-env NODE_ENV=production webpack"
rimraf dist
: clear current distncp public dist
copies current content of yourpublic
-folder todist
cross-env NODE_ENV=production webpack
setsNODE_ENV
toproduction
(platform-agnostic via cross-env) and build your project via #webpack
executes your defined tests in /tests
, using your config in /jest.config.js
, look at the jest-part for more information.
package | description |
---|---|
cross-env | set environment-variables platform-agnostic |
ncp | copy files platform-agnostic via node |
rimraf | remove files platform-agnostic via node |
ESLint is a linter for your JS-Code. You can use it via CLI or integrate it into your development cycle.
TODO
package | description |
---|---|
eslint | see ESLint |
eslint-config-airbnb | most commonly used lint-rule-set by Airbnb, this automatically includes following plugins: eslint-plugin-import, eslint-plugin-jsx-a11y, eslint-plugin-react |
eslint-plugin-import | "ESLint plugin with rules that help validate proper imports." |
eslint-import-resolver-node | addition toeslint-plugin-import to resolve file extensions other than .js (e.g. necessary to import .ts /.tsx files) |
eslint-plugin-jsx-a11y | "Static AST checker for accessibility rules on JSX elements." |
eslint-plugin-react | "React specific linting rules for ESLint" |
eslint-plugin-prettier | used via the Recommended Configuration, so it will also extend the rules via eslint-config-prettier |
eslint-config-prettier | Turns off rules that might conflict with Prettier. |
TSLint is a linting tool that checks your TypeScript-Code via command-line. However, the integration into your development cycle is more comfortable (for example as integration into your IDE like WebStorm or VS code) so that the code is checked quasi "as-you-type", see TSLints "Third-Party Tools".
TODO
package | description |
---|---|
tslint | see TSLint |
tslint-config-prettier | "Use tslint with prettier without any conflict" |
tslint-react | "Lint rules related to React & JSX for TSLint." |
Prettier (prettier github-repo) is a strict code-formatter that supports many different languages. Prettier always tries to guarantee the same code style and does not take initial formatting into account. This guarantees that even if several people work on a project, the code style remains consistent. The options to customize the output are limited.
Via husky andlint-staged every changed *.{ts,tsx,js,js,jsx,json,css,md}
file is formatted uniformly before a commit using Prettier, also a tslint --fix
runs for *.{ts,tsx}
and a eslint --fix
for *.{js,jsx}
before the changed files are re-added.
The setup is according to lint-staged's documentation, but husky is set up via /husky.config.js
and lint via dev/.lintstagedrc
. (package.json
should not be stuffed senselessly).
If you ever have trouble understanding why a dependency is in package.json
you can find an overview here, which refers to the corresponding documentation section.
package | link to documenation |
---|---|
@babel/runtime | babel-helper-functions via babel-runtime for smaller bundle-size |
object-assign | polyfills |
promise | polyfills |
prop-types | So you can use prop-types in your *.js -files |
react" | A React-App without the react-package doesn't make that much sense ;) |
react-dom | "This package serves as the entry point of the DOM-related rendering paths. It is intended to be paired with the isomorphic React, which will be shipped as react to npm." |
react-loadable | A nice library for code splitting via webpack and dynamic import() calls.This library also automatically supports HMR for your dynamic imports (see react-hot-loader). |
The @types/...
-devDependencies are omitted to explain, have a look at the DefinitelyTyped-project for more information.
- webpack-serve
- seems to be the succesor of webpack-dev-server, which is only in maintenance mode
- => but currently there are no reasonable examples or the like. why webpack-dev-server is the better way up-to-date (08/2018)
- reduce moment-js-bundle-size via the []webpacks
IgnorePlugin
](https://webpack.js.org/plugins/ignore-plugin/), see "How to optimize moment.js with webpack" - performance-regression with
collapse_vars
(uglify-js2) checken: uglifyjs-issue, terser-issue, cra-issue- currently added
collapse_vars: false
indev/webpack/ugilfyJsPluginOptions.js
- zum beispiel für komplettes apoly-projekt (ca 5MiB bundled) war der Unterschied ~3KiB => VERNACHLÄSSIGBAR
- CRA nutzt terser + webpack-terser, für apoly testweise code eingebaut: terser ist langsamer (112s vs 81s) und schlechter (20188KiB vs 19846KiB, diff 342KiB) als uglify mit
collapse_vars: false
- currently added
- Ist babel-preset-env wirklich notwendig, bzw vllt mit einer neueren browserslist-config in preset-env (zb ">5%"), um sicherzugehen dass nicht jeder müll für IE11 kompiliert wird, in dem fall muss auch wider auf terser anstatt uglifyj umgestiegen werden da uglifyjs nur es5 kann
- das auslagern mit all den dependencies und doku in ein projekt was übe cmd-line angesteuert werden kann
- node-js-api anbieten sodass die webpack/babel-config verfügbar ist und man leicht Anpassungen vornehmen kann mit eigener config
- webpack-cli und webpack-dev-server muss dann selbst hinzugefügt werden um es entsprechend zu nutzen
- mono-repo dafür erstellen,
- single packages für (erster spontan-entwurf):
- babel (create-script für standard-babel-objekt)
- webpack (create-script für config)
- eslint
- tslint
- zu überlegen:
- jest (config auslagern?)
- TypeScript (tsconfig)
- die einzelnen projekte exportieren jeweils die configs, wenn man etwas anpassen möchte sollte das einzelne projekt als dependency mit genutzt werden sodass man das script selbst anpassen kann auf grundlage des config-objektes
- single packages für (erster spontan-entwurf):
check + document:
- optimization.splitChunks => sollte man machen habe aktuell mit paar werten rumgespielt, zb ist aktuell "all" anscheinend nicht so geil, maxSize sinnvoll mit http/2
- runtimechunk sollte true sein da sonst bei eine änderung quasi jedes chunk einen neuen hash bekommt und nichts gecached werden kann, mit runtimeChunk werdend ie hash werte hier rein geschrieben sodass nicht geänderte files gleich bleiben
- opt-in useESModules:"auto" => in bezug auf babel-runtime-plugin, sollte ich checken wenn documentation besser ist
- supportsStaticESM : ist vielleicht auch nur ne interne option, sollte ich prüfen wenn die documentation bissel stabiler ist
gut info auch der Medium-Artikel webpack 4: Code Splitting, chunk graph and the splitChunks optimization