├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml └── dependabot.yml ├── .gitignore ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── src ├── components │ ├── App │ │ └── index.js │ └── Spinner │ │ └── index.js ├── config.js ├── hooks │ ├── useCsvData.js │ ├── useJsonData.js │ └── useTopoData.js ├── index.html ├── index.js ├── store │ └── index.js ├── style │ ├── global.js │ ├── normalize.js │ └── theme.js └── utils │ ├── browser-utils.js │ ├── css-utils.js │ └── data-utils.js ├── static └── data │ └── data.json └── webpack ├── webpack.common.js ├── webpack.config.dev.js └── webpack.config.prod.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | webpack/ 3 | build/ -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "plugins": ["prettier"], 4 | "extends": ["airbnb", "prettier"], 5 | "rules": { 6 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 7 | "no-param-reassign": 0, 8 | "react/prop-types": 0, 9 | "prettier/prettier": "error" 10 | }, 11 | "env": { 12 | "browser": true 13 | }, 14 | "settings": { 15 | "import/resolver": { 16 | "webpack": { 17 | "config": "./webpack/webpack.config.dev.js" 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [wbkd] 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: webpack 11 | versions: 12 | - 5.17.0 13 | - 5.19.0 14 | - 5.21.2 15 | - 5.23.0 16 | - 5.24.2 17 | - 5.24.3 18 | - 5.25.1 19 | - 5.27.1 20 | - 5.28.0 21 | - 5.30.0 22 | - 5.31.2 23 | - dependency-name: mini-css-extract-plugin 24 | versions: 25 | - 1.3.4 26 | - 1.3.5 27 | - 1.3.6 28 | - 1.3.8 29 | - 1.3.9 30 | - 1.4.0 31 | - 1.4.1 32 | - dependency-name: eslint 33 | versions: 34 | - 7.18.0 35 | - 7.19.0 36 | - 7.20.0 37 | - 7.21.0 38 | - 7.22.0 39 | - 7.23.0 40 | - 7.24.0 41 | - dependency-name: core-js 42 | versions: 43 | - 3.10.0 44 | - 3.10.1 45 | - 3.8.3 46 | - 3.9.0 47 | - 3.9.1 48 | - dependency-name: postcss-loader 49 | versions: 50 | - 4.2.0 51 | - 5.0.0 52 | - 5.1.0 53 | - dependency-name: eslint-config-prettier 54 | versions: 55 | - 7.2.0 56 | - 8.0.0 57 | - 8.1.0 58 | - dependency-name: webpack-cli 59 | versions: 60 | - 4.4.0 61 | - 4.5.0 62 | - dependency-name: autoprefixer 63 | versions: 64 | - 10.2.3 65 | - 10.2.4 66 | - dependency-name: webpack-bundle-analyzer 67 | versions: 68 | - 4.4.0 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # production 5 | build 6 | bundle.zip 7 | 8 | # misc 9 | .DS_Store 10 | 11 | npm-debug.log 12 | yarn-error.log 13 | yarn.lock 14 | .yarnclean 15 | 16 | .vscode 17 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v12.13.1 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2020 webkid GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Starter 2 | 3 | [![Dependabot badge](https://flat.badgen.net/dependabot/wbkd/react-starter?icon=dependabot)](https://dependabot.com/) 4 | 5 | A minimal starting point for interactive applications that we build at [webkid](https://webkid.io). The boilerplate already includes some basic components as well as hooks and utils that we are often using in our projects. 6 | 7 | The starterkit is based on these libraries: 8 | 9 | - [react](https://facebook.github.io/react/) 10 | - [easy-peasy](https://easy-peasy.now.sh/) 11 | - [emotion](https://emotion.sh/docs/styled) 12 | 13 | ## Getting started 14 | 15 | **[Create a new Github repository with the template](https://github.com/wbkd/react-starter/generate)** or clone the repo: 16 | 17 | ```sh 18 | git clone git@github.com:wbkd/react-starter.git && cd react-starter 19 | ``` 20 | 21 | then install the dependencies via [npm](https://npmjs.org): 22 | 23 | ```sh 24 | npm install 25 | ``` 26 | 27 | you are now ready to develop your app. 28 | 29 | ## Development 30 | 31 | Builds the application and starts a webserver with hot loading. 32 | Runs on [localhost:8080](http://localhost:8080/) 33 | 34 | ```sh 35 | npm run start 36 | ``` 37 | 38 | ## Build 39 | 40 | Builds a minified version of the application in the build folder. 41 | 42 | ```sh 43 | npm run build 44 | ``` 45 | 46 | Additionally, a zipped version of the bundle is added as `build.zip`. We often use this for our clients to upload the application to their own servers. 47 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | api.cache(true); 3 | 4 | const presets = [ 5 | '@babel/preset-react', 6 | [ 7 | '@babel/preset-env', 8 | { 9 | useBuiltIns: 'usage', 10 | corejs: 3, 11 | }, 12 | ], 13 | ]; 14 | 15 | const plugins = []; 16 | 17 | return { 18 | presets, 19 | plugins, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-starter", 3 | "config": { 4 | "appname": "my-app-name" 5 | }, 6 | "version": "7.0.0", 7 | "description": "lightweight react starterkit including webpack, unistore, styled components", 8 | "scripts": { 9 | "start": "webpack serve --config webpack/webpack.config.dev.js", 10 | "build": "cross-env BABEL_ENV=production webpack --config webpack/webpack.config.prod.js && npm run zip", 11 | "zip": "zip -r build/build.zip build" 12 | }, 13 | "repository": { 14 | "url": "git@github.com:wbkd/react-starter.git", 15 | "type": "git" 16 | }, 17 | "browserslist": "> 0.25%, ie 11", 18 | "author": "webkid.io", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@babel/cli": "^7.16.8", 22 | "@babel/core": "^7.16.7", 23 | "@babel/eslint-parser": "^7.16.5", 24 | "@babel/preset-env": "^7.16.8", 25 | "@babel/preset-react": "^7.16.7", 26 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", 27 | "autoprefixer": "^10.4.2", 28 | "babel-loader": "^8.2.3", 29 | "clean-webpack-plugin": "^4.0.0", 30 | "copy-webpack-plugin": "^10.2.0", 31 | "cross-env": "^7.0.3", 32 | "css-loader": "^6.5.1", 33 | "eslint": "^8.7.0", 34 | "eslint-config-airbnb": "^19.0.4", 35 | "eslint-config-prettier": "^8.3.0", 36 | "eslint-import-resolver-webpack": "^0.13.2", 37 | "eslint-plugin-import": "^2.25.4", 38 | "eslint-plugin-jsx-a11y": "^6.5.1", 39 | "eslint-plugin-prettier": "^4.0.0", 40 | "eslint-plugin-react": "^7.28.0", 41 | "eslint-plugin-react-hooks": "^4.3.0", 42 | "eslint-webpack-plugin": "^3.1.1", 43 | "file-loader": "^6.2.0", 44 | "html-webpack-plugin": "^5.5.0", 45 | "mini-css-extract-plugin": "^2.5.1", 46 | "postcss-loader": "^6.2.1", 47 | "postcss-preset-env": "^7.2.3", 48 | "prettier": "^2.5.1", 49 | "react-refresh": "^0.11.0", 50 | "react-svg-loader": "^3.0.3", 51 | "style-loader": "^3.3.1", 52 | "webpack": "^5.66.0", 53 | "webpack-bundle-analyzer": "^4.5.0", 54 | "webpack-cli": "^4.9.1", 55 | "webpack-dev-server": "^4.7.3", 56 | "webpack-merge": "^5.8.0" 57 | }, 58 | "dependencies": { 59 | "@emotion/react": "^11.7.1", 60 | "@emotion/styled": "^11.6.0", 61 | "core-js": "^3.20.3", 62 | "d3-dsv": "^3.0.1", 63 | "easy-peasy": "^5.0.4", 64 | "emotion-normalize": "^11.0.1", 65 | "react": "^17.0.2", 66 | "react-dom": "^17.0.2", 67 | "topojson-client": "^3.1.0", 68 | "unfetch": "^4.2.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/components/App/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { useStoreActions, useStoreState } from 'easy-peasy'; 4 | 5 | import Spinner from 'components/Spinner'; 6 | 7 | const Wrapper = styled.div` 8 | padding: 20px; 9 | `; 10 | 11 | function App() { 12 | const data = useStoreState((state) => state.app.data); 13 | const dataLoading = useStoreState((state) => state.app.dataLoading); 14 | const loadData = useStoreActions((actions) => actions.app.loadData); 15 | 16 | return ( 17 | 18 |

React Starterkit

19 | {dataLoading && } 20 | {data ? ( 21 | data.length 22 | ) : ( 23 | 26 | )} 27 |
28 | ); 29 | } 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /src/components/Spinner/index.js: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { keyframes } from '@emotion/react'; 3 | 4 | const Bounce = keyframes` 5 | 0%, 100% { -webkit-transform: scale(0.0) } 6 | 50% { -webkit-transform: scale(1.0) } 7 | `; 8 | 9 | const Loader = styled.div` 10 | width: ${(p) => p.size || 50}px; 11 | height: ${(p) => p.size || 50}px; 12 | position: relative; 13 | 14 | &:before, 15 | &:after { 16 | content: ''; 17 | width: 100%; 18 | height: 100%; 19 | border-radius: 50%; 20 | opacity: 0.6; 21 | position: absolute; 22 | top: 0; 23 | left: 0; 24 | animation: ${Bounce} ${(p) => p.duration || 2000}ms infinite ease-in-out; 25 | background: ${(p) => p.theme.colors.primary}; 26 | } 27 | 28 | &:after { 29 | animation-delay: -${(p) => (p.duration ? p.duration / 2 : 1000)}ms; 30 | } 31 | `; 32 | 33 | export default Loader; 34 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const config = {}; 2 | 3 | export default config; 4 | -------------------------------------------------------------------------------- /src/hooks/useCsvData.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { dsvFormat } from 'd3-dsv'; 3 | import { fetchCsv } from 'utils/data-utils'; 4 | 5 | const defaultOptions = { delimiter: ',', onEachRow: (d) => d }; 6 | 7 | export default function useCsvData(url, options = defaultOptions) { 8 | const [data, setData] = useState(null); 9 | const mergedOptions = { ...defaultOptions, ...options }; 10 | const { delimiter, onEachRow } = mergedOptions; 11 | const csvParser = dsvFormat(delimiter); 12 | 13 | useEffect(() => { 14 | const loadData = async () => { 15 | const csv = await fetchCsv(url); 16 | const result = csvParser.parse(csv, onEachRow); 17 | setData(result); 18 | }; 19 | 20 | loadData(); 21 | }, [url]); 22 | 23 | return { 24 | data, 25 | isLoading: !data, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/useJsonData.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { fetchJson } from 'utils/data-utils'; 3 | 4 | export default function useJsonData(url) { 5 | const [data, setData] = useState(null); 6 | 7 | useEffect(() => { 8 | const loadData = async () => { 9 | const result = await fetchJson(url); 10 | setData(result); 11 | }; 12 | 13 | loadData(); 14 | }, [url]); 15 | 16 | return { 17 | data, 18 | isLoading: !data, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/hooks/useTopoData.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { feature } from 'topojson-client'; 3 | import { fetchJson } from 'utils/data-utils'; 4 | 5 | export default function useTopoData(url) { 6 | const [data, setData] = useState(null); 7 | 8 | useEffect(() => { 9 | const loadData = async () => { 10 | const topo = await fetchJson(url); 11 | const topoKey = Object.keys(topo.objects)[0]; 12 | const topoData = feature(topo, topo.objects[topoKey]); 13 | setData(topoData); 14 | }; 15 | 16 | loadData(); 17 | }, [url]); 18 | 19 | return { 20 | data, 21 | isLoading: !data, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | react starter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { StoreProvider } from 'easy-peasy'; 4 | import { ThemeProvider } from '@emotion/react'; 5 | 6 | import store from 'store'; 7 | import theme from 'style/theme'; 8 | 9 | import NormalizeStyle from 'style/normalize'; 10 | import GlobalStyle from 'style/global'; 11 | import App from 'components/App'; 12 | 13 | function AppWrapper() { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | ReactDOM.render(, document.getElementById('root')); 26 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, thunk, action } from 'easy-peasy'; 2 | import { fetchJson } from 'utils/data-utils'; 3 | 4 | const model = { 5 | app: { 6 | data: null, 7 | dataLoading: false, 8 | loadData: thunk(async (actions) => { 9 | actions.loadDataStart(); 10 | const data = await fetchJson('static/data/data.json'); 11 | if (data) { 12 | actions.loadDataSuccess(data); 13 | } else { 14 | actions.loadDataFail(); 15 | } 16 | }), 17 | loadDataStart: action((state) => { 18 | state.dataLoading = true; 19 | }), 20 | loadDataSuccess: action((state, payload) => { 21 | state.data = payload; 22 | state.dataLoading = false; 23 | }), 24 | loadDataFail: action((state) => { 25 | state.data = null; 26 | state.dataLoading = false; 27 | }), 28 | }, 29 | }; 30 | 31 | export default createStore(model); 32 | -------------------------------------------------------------------------------- /src/style/global.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTheme, Global, css } from '@emotion/react'; 3 | 4 | function GlobalStyle() { 5 | const theme = useTheme(); 6 | 7 | const globalStyles = css` 8 | html, 9 | body { 10 | font-family: ${theme.fonts.sans}; 11 | font-weight: 400; 12 | letter-spacing: 0.5px; 13 | line-height: 1.5; 14 | font-size: 16px; 15 | padding: 0; 16 | margin: 0; 17 | color: ${theme.colors.text}; 18 | } 19 | 20 | a { 21 | color: ${theme.colors.text}; 22 | text-decoration: none; 23 | } 24 | a:visited, 25 | a:focus, 26 | a:active { 27 | color: ${theme.colors.text}; 28 | text-decoration: none; 29 | } 30 | a:hover { 31 | color: ${theme.colors.text}; 32 | text-decoration: none; 33 | } 34 | code, 35 | pre { 36 | font-family: ${theme.fonts.mono}; 37 | } 38 | `; 39 | 40 | return ; 41 | } 42 | 43 | export default GlobalStyle; 44 | -------------------------------------------------------------------------------- /src/style/normalize.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { Global, css } from '@emotion/react'; 3 | import emotionNormalize from 'emotion-normalize'; 4 | 5 | const NormalizeStyle = memo(() => { 6 | const globalStyles = css` 7 | ${emotionNormalize} 8 | `; 9 | 10 | return ; 11 | }); 12 | 13 | export default NormalizeStyle; 14 | -------------------------------------------------------------------------------- /src/style/theme.js: -------------------------------------------------------------------------------- 1 | import { px, breakpoints } from 'utils/css-utils'; 2 | 3 | const space = [0, 4, 8, 16, 32, 48, 64, 96, 128]; 4 | const spacePx = space.map(px); 5 | 6 | const fontSizes = [12, 16, 20, 24, 36, 48, 54]; 7 | const fontSizesPx = fontSizes.map(px); 8 | const breakpointsPx = Object.values(breakpoints).map(px); 9 | 10 | const theme = { 11 | fonts: { 12 | sans: "-apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Ubuntu, Arial, sans-serif", 13 | mono: 'monospace', 14 | }, 15 | space, 16 | spacePx, 17 | fontSizes, 18 | fontSizesPx, 19 | breakpoints: breakpointsPx, 20 | boxShadow: '0px 16px 64px rgba(26, 25, 43, 0.32);', 21 | colors: { 22 | primary: '#0041D0', 23 | orange: '#FF6700', 24 | blue: '#0041D0', 25 | red: '#FF0072', 26 | purple: '#784BE8', 27 | green: '#00D7CA', 28 | text: '#1A192B', 29 | textLight: '#808080', 30 | lightGrey: '#D9D9D9', 31 | }, 32 | }; 33 | 34 | export default theme; 35 | -------------------------------------------------------------------------------- /src/utils/browser-utils.js: -------------------------------------------------------------------------------- 1 | import { device } from 'utils/css-utils'; 2 | 3 | // detect all IE <= 11 4 | export function isOldIE() { 5 | if (typeof navigator === 'undefined') { 6 | return false; 7 | } 8 | 9 | // https://stackoverflow.com/a/22242528 10 | return ( 11 | navigator.userAgent.indexOf('MSIE') !== -1 || navigator.appVersion.indexOf('Trident/') > -1 12 | ); 13 | } 14 | 15 | export function isMobile() { 16 | return !window.matchMedia(device.tablet); 17 | } 18 | 19 | export default { 20 | isOldIE, 21 | isMobile, 22 | }; 23 | -------------------------------------------------------------------------------- /src/utils/css-utils.js: -------------------------------------------------------------------------------- 1 | export const breakpoints = { 2 | s: 460, 3 | m: 768, 4 | l: 1024, 5 | }; 6 | 7 | export const device = { 8 | phone: `(min-width: ${breakpoints.s}px)`, 9 | tablet: `(min-width: ${breakpoints.m}px)`, 10 | desktop: `(min-width: ${breakpoints.l}px)`, 11 | }; 12 | 13 | export const rgba = (hex, alpha) => { 14 | const [r, g, b] = hex.match(/\w\w/g).map((x) => parseInt(x, 16)); 15 | return `rgba(${r},${g},${b},${alpha})`; 16 | }; 17 | 18 | export const px = (val) => `${val}px`; 19 | 20 | export default { 21 | breakpoints, 22 | device, 23 | rgba, 24 | px, 25 | }; 26 | -------------------------------------------------------------------------------- /src/utils/data-utils.js: -------------------------------------------------------------------------------- 1 | import fetch from 'unfetch'; 2 | 3 | export async function fetchJson(url) { 4 | try { 5 | const response = await fetch(url); 6 | const json = await response.json(); 7 | return json; 8 | } catch (err) { 9 | console.log('Error loading data', err); 10 | return null; 11 | } 12 | } 13 | 14 | export async function fetchCsv(url) { 15 | try { 16 | const response = await fetch(url); 17 | const csv = await response.text(); 18 | return csv; 19 | } catch (err) { 20 | console.log('Error loading data', err); 21 | return null; 22 | } 23 | } 24 | 25 | export default { 26 | fetchJson, 27 | fetchCsv, 28 | }; 29 | -------------------------------------------------------------------------------- /static/data/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "color": "red", 4 | "value": "#f00" 5 | }, 6 | { 7 | "color": "green", 8 | "value": "#0f0" 9 | }, 10 | { 11 | "color": "blue", 12 | "value": "#00f" 13 | }, 14 | { 15 | "color": "cyan", 16 | "value": "#0ff" 17 | }, 18 | { 19 | "color": "magenta", 20 | "value": "#f0f" 21 | }, 22 | { 23 | "color": "yellow", 24 | "value": "#ff0" 25 | }, 26 | { 27 | "color": "black", 28 | "value": "#000" 29 | } 30 | ] 31 | -------------------------------------------------------------------------------- /webpack/webpack.common.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 5 | // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 6 | 7 | module.exports = { 8 | entry: { 9 | app: Path.resolve(__dirname, '../src/index.js'), 10 | }, 11 | output: { 12 | path: Path.join(__dirname, '../build'), 13 | filename: 'js/[name].js', 14 | }, 15 | optimization: { 16 | splitChunks: { 17 | chunks: 'all', 18 | name: false, 19 | }, 20 | }, 21 | plugins: [ 22 | new CleanWebpackPlugin(), 23 | new CopyWebpackPlugin({ 24 | patterns: [{ from: Path.resolve(__dirname, '../static'), to: 'static' }], 25 | }), 26 | new HtmlWebpackPlugin({ 27 | inject: true, 28 | template: Path.resolve(__dirname, '../src/index.html'), 29 | }), 30 | // new BundleAnalyzerPlugin() 31 | ], 32 | resolve: { 33 | modules: [Path.resolve(__dirname, '../src'), Path.resolve(__dirname, '../node_modules')], 34 | }, 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.mjs$/, 39 | include: /node_modules/, 40 | type: 'javascript/auto', 41 | }, 42 | { 43 | test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2)(\?.*)?$/, 44 | use: { 45 | loader: 'file-loader', 46 | options: { 47 | name: '[path][name].[ext]', 48 | }, 49 | }, 50 | }, 51 | { 52 | test: /\.svg$/, 53 | use: [ 54 | 'babel-loader', 55 | { 56 | loader: 'react-svg-loader', 57 | options: { 58 | jsx: true, 59 | }, 60 | }, 61 | ], 62 | }, 63 | ], 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /webpack/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const Path = require('path'); 2 | const Webpack = require('webpack'); 3 | const { merge } = require('webpack-merge'); 4 | const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); 5 | const ESLintPlugin = require('eslint-webpack-plugin'); 6 | 7 | const common = require('./webpack.common.js'); 8 | 9 | module.exports = merge(common, { 10 | mode: 'development', 11 | devtool: 'eval-cheap-source-map', 12 | output: { 13 | chunkFilename: 'js/[name].chunk.js', 14 | }, 15 | devServer: { 16 | historyApiFallback: true, 17 | hot: true, 18 | host: '0.0.0.0', 19 | }, 20 | plugins: [ 21 | new ESLintPlugin({ quiet: true }), 22 | new Webpack.DefinePlugin({ 23 | 'process.env.NODE_ENV': JSON.stringify('development'), 24 | }), 25 | new ReactRefreshWebpackPlugin(), 26 | ], 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.(js|jsx)$/, 31 | include: Path.resolve(__dirname, '../src'), 32 | use: [ 33 | { 34 | loader: 'babel-loader', 35 | options: { 36 | plugins: ['react-refresh/babel'], 37 | }, 38 | }, 39 | ], 40 | }, 41 | { 42 | test: /\.css$/, 43 | use: ['style-loader', 'css-loader'], 44 | }, 45 | ], 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /webpack/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const Webpack = require('webpack'); 2 | const Path = require('path'); 3 | const { merge } = require('webpack-merge'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | const postcssPresetEnv = require('postcss-preset-env'); 6 | 7 | const common = require('./webpack.common.js'); 8 | 9 | module.exports = merge(common, { 10 | mode: 'production', 11 | stats: 'errors-only', 12 | bail: true, 13 | output: { 14 | filename: 'js/[name].[chunkhash:8].js', 15 | chunkFilename: 'js/[name].[chunkhash:8].chunk.js', 16 | }, 17 | plugins: [ 18 | new Webpack.DefinePlugin({ 19 | 'process.env.NODE_ENV': JSON.stringify('production'), 20 | }), 21 | new MiniCssExtractPlugin({ filename: 'bundle.css' }), 22 | ], 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.css$/, 27 | use: [ 28 | MiniCssExtractPlugin.loader, 29 | 'css-loader', 30 | { 31 | loader: 'postcss-loader', 32 | options: { 33 | sourceMap: true, 34 | postcssOptions: { 35 | plugins: [postcssPresetEnv], 36 | }, 37 | }, 38 | }, 39 | ], 40 | }, 41 | { 42 | test: /\.(js|jsx)$/, 43 | include: [ 44 | Path.resolve(__dirname, '../src'), 45 | /** 46 | * add ES6 modules that should be transpiled here. For example: 47 | * Path.resolve(__dirname, '../node_modules/query-string'), 48 | */ 49 | ], 50 | loader: 'babel-loader', 51 | }, 52 | ], 53 | }, 54 | }); 55 | --------------------------------------------------------------------------------