├── .eslintignore ├── .eslintrc ├── .gitignore ├── .lintstagedrc.js ├── .npmrc ├── .prettierignore ├── README.md ├── package.json ├── prettier.config.js ├── relay.config.js ├── schema.graphql ├── src ├── App.tsx ├── ConferencesQuery.tsx ├── Home.tsx ├── __generated__ │ └── ConferencesQuery.graphql.ts ├── index.html ├── index.tsx └── relay │ └── Environment.ts ├── tsconfig.json ├── webpack.config.common.js ├── webpack.config.development.js ├── webpack.config.production.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | /src/__generated__/* 2 | /webpack*.js 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "react": { 4 | "version": "detect" 5 | } 6 | }, 7 | "parser": "@typescript-eslint/parser", 8 | "plugins": ["@typescript-eslint"], 9 | "extends": [ 10 | "canonical", 11 | "canonical/react", 12 | "plugin:@typescript-eslint/recommended", 13 | "prettier", 14 | "prettier/@typescript-eslint", 15 | "prettier/babel", 16 | "prettier/react", 17 | "prettier/unicorn" 18 | ], 19 | "root": true, 20 | "rules": { 21 | "class-methods-use-this": 0, 22 | "filenames/match-regex": 0, 23 | "no-unused-vars": 0, 24 | "arrow-body-style": 0, 25 | "react/no-multi-comp": 0, 26 | "no-extra-parens": 0, 27 | "import/max-dependencies": 0, 28 | "id-length": 0, 29 | "filenames/match-exported": 0, 30 | "react/jsx-no-bind": 0, 31 | "import/no-namespace": 0, 32 | "import/no-unresolved": 0, 33 | "object-curly-newline": 0, 34 | "no-nested-ternary": 0, 35 | "react/require-default-props": 0, 36 | // Allows us to use .graphql extensions for importing relay artifacts 37 | "import/extensions": 0, 38 | // There are types to be explicit and times to use inference. 39 | // React components are good example of place I like inference for return types. 40 | "@typescript-eslint/explicit-module-boundary-types": 0, 41 | // Disabled because in TS files we usually declare types at the top of the file, and in cases we need to export them. 42 | "import/exports-last": 0, 43 | "react/jsx-boolean-value": 0 44 | }, 45 | "overrides": [ 46 | { 47 | "files": ["**/*.d.ts"], 48 | "rules": { 49 | "@typescript-eslint/ban-types": 0, 50 | "@typescript-eslint/no-explicit-any": 0, 51 | "import/unambiguous": 0 52 | } 53 | }, 54 | { 55 | "files": ["**/*.ts", "**/*.tsx"], 56 | "rules": { 57 | "@typescript-eslint/no-explicit-any": 2, 58 | "@typescript-eslint/no-empty-function": [ 59 | 2, 60 | { "allow": ["arrowFunctions"] } 61 | ], 62 | "import/no-unassigned-import": 0 63 | } 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | *.log 5 | package-lock.json 6 | .* 7 | !*/*.babelrc.js 8 | !.dockerignore 9 | !.editorconfig 10 | !.eslintignore 11 | !.eslintrc 12 | !.gitignore 13 | !.gitlab-ci.yml 14 | !.npmignore 15 | !.storybook 16 | !.npmrc 17 | !.svgrrc.js 18 | !.prettierignore 19 | !.lintstagedrc.js 20 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'], 3 | // jsonlint can process only one file at a time. 4 | '*.json': (filenames) => 5 | filenames.map( 6 | (filename) => `jsonlint --in-place --sort-keys '${filename}'` 7 | ), 8 | }; 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | /src/__generated__/* 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Router V6 + Relay Hooks POC 2 | 3 | **Please note that this repo is merely a work in progress and is currently not yet ready for use. It does not work and should not be taken as a successful example of React Router v6 and Relay hooks working together.** 4 | 5 | ```bash 6 | yarn 7 | 8 | export API_URL='https://api.react-finland.fi/graphql' 9 | 10 | yarn start 11 | 12 | ``` 13 | 14 | - This repo uses the react-finland graphql test project found at https://api.react-finland.fi/graphql to provide a live graphql interface to work with. 15 | 16 | - This repo also defines routes as objects, using the `useRouter` hook. 17 | 18 | - There is one query -- `ConferencesQuery` -- which will serve as the proof of concept query for preloading in this repo. 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "browserslist": { 3 | "development": [ 4 | "last 1 chrome version", 5 | "last 1 firefox version", 6 | "last 1 safari version" 7 | ], 8 | "production": [ 9 | ">0.2%", 10 | "not dead", 11 | "not op_mini all" 12 | ] 13 | }, 14 | "dependencies": { 15 | "@babel/core": "^7.10.3", 16 | "@babel/node": "^7.10.5", 17 | "@babel/plugin-proposal-numeric-separator": "^7.10.1", 18 | "@babel/plugin-proposal-object-rest-spread": "^7.10.3", 19 | "@babel/plugin-proposal-optional-chaining": "^7.10.3", 20 | "@babel/polyfill": "^7.11.5", 21 | "@babel/preset-env": "^7.10.3", 22 | "@babel/preset-react": "^7.10.1", 23 | "@babel/preset-typescript": "^7.10.4", 24 | "@babel/register": "^7.10.4", 25 | "@types/react": "^16.9.49", 26 | "@types/react-dom": "^16.9.8", 27 | "@types/react-relay": "^7.0.10", 28 | "@types/react-router-dom": "^5.1.5", 29 | "@types/webpack-dev-middleware": "^3.7.1", 30 | "@types/webpack-dev-server": "^3.11.0", 31 | "@typescript-eslint/eslint-plugin": "^3.9.0", 32 | "@typescript-eslint/parser": "^3.9.0", 33 | "babel-loader": "^8.1.0", 34 | "babel-plugin-relay": "^10.0.1", 35 | "babel-plugin-styled-components": "^1.11.1", 36 | "clean-webpack-plugin": "^3.0.0", 37 | "eslint": "^7.3.1", 38 | "eslint-config-canonical": "^23.0.1", 39 | "eslint-config-prettier": "^6.11.0", 40 | "get-graphql-schema": "^2.1.2", 41 | "graphql": "^15.1.0", 42 | "history": "^5.0.0", 43 | "html-webpack-plugin": "^4.4.1", 44 | "husky": "^4.2.5", 45 | "jsonlint": "^1.6.3", 46 | "lint-staged": "^10.2.11", 47 | "mini-css-extract-plugin": "^0.10.0", 48 | "optimize-css-assets-webpack-plugin": "^5.0.3", 49 | "prettier": "2.1.0", 50 | "react": "^0.0.0-experimental-94c0244ba", 51 | "react-dom": "^0.0.0-experimental-94c0244ba", 52 | "react-relay": "^0.0.0-experimental-183bdd28", 53 | "react-router": "^0.0.0-experimental-ffd8c7d0", 54 | "react-router-dom": "^0.0.0-experimental-ffd8c7d0", 55 | "relay-compiler": "^10.0.1", 56 | "relay-compiler-language-typescript": "^13.0.1", 57 | "relay-config": "^10.0.1", 58 | "relay-runtime": "^10.0.1", 59 | "terser-webpack-plugin": "^4.1.0", 60 | "typescript": "^4.0.2", 61 | "webpack": "^4.44.1", 62 | "webpack-assets-manifest": "^3.1.1", 63 | "webpack-cli": "^3.3.12", 64 | "webpack-dev-middleware": "^3.7.2", 65 | "webpack-dev-server": "^3.11.0", 66 | "webpack-merge": "^5.1.2" 67 | }, 68 | "husky": { 69 | "hooks": { 70 | "pre-commit": "lint-staged", 71 | "pre-push": "yarn lint && yarn tsc && yarn test" 72 | } 73 | }, 74 | "license": "UNLICENSED", 75 | "private": true, 76 | "scripts": { 77 | "build": "webpack --config=./webpack.config.production.js", 78 | "lint": "eslint src", 79 | "refetch-graphql-schema": "get-graphql-schema $API_URL > ./schema.graphql", 80 | "relay-compile": "relay-compiler --src ./src --schema ./schema.graphql --language typescript --artifactDirectory ./src/__generated__ --extensions js jsx ts tsx", 81 | "start": "yarn relay-compile && NODE_ENV=development webpack-dev-server --config=webpack.config.development.js --open", 82 | "start-server": "yarn refetch-graphql-schema && yarn start", 83 | "tsc": "tsc" 84 | } 85 | } -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-commonjs */ 2 | /* eslint-disable import/unambiguous */ 3 | 4 | /** 5 | * Options for Prettier. 6 | * 7 | * @see https://prettier.io/docs/en/options.html 8 | */ 9 | 10 | module.exports = { 11 | singleQuote: true, 12 | }; 13 | -------------------------------------------------------------------------------- /relay.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-commonjs, import/unambiguous */ 2 | module.exports = { 3 | exclude: [ 4 | '**/node_modules/**', 5 | '**/__mocks__/**', 6 | '**/__generated__/**', 7 | ], 8 | schema: './schema.graphql', 9 | src: './src', 10 | }; 11 | -------------------------------------------------------------------------------- /schema.graphql: -------------------------------------------------------------------------------- 1 | type Colors { 2 | primary: String! 3 | secondary: String! 4 | text: String! 5 | background: String! 6 | } 7 | 8 | type Conference { 9 | id: ID! 10 | series: String! 11 | name: String! 12 | organizer: Contact! 13 | year: String! 14 | startDate: String! 15 | endDate: String! 16 | slogan: String! 17 | websiteUrl: String! 18 | locations: [Location!] 19 | organizers: [Contact!]! 20 | mcs: [Contact!] 21 | partners: [Contact!] 22 | sponsors: [Contact!]! 23 | goldSponsors: [Contact!] 24 | silverSponsors: [Contact!] 25 | bronzeSponsors: [Contact!] 26 | schedules: [Schedule!]! 27 | allSpeakers: [Contact!] 28 | speakers: [Contact!] 29 | keynoteSpeakers: [Contact!] 30 | fullTalkSpeakers: [Contact!] 31 | lightningTalkSpeakers: [Contact!] 32 | workshopInstructors: [Contact!] 33 | talks: [Session!] 34 | workshops: [Session!] 35 | attendees: [Contact!]! 36 | keynotes: [Session!]! 37 | fullTalks: [Session!]! 38 | lightningTalks: [Session!]! 39 | } 40 | 41 | type Contact { 42 | firstName: String! 43 | lastName: String! 44 | name: String! 45 | about: String! 46 | aboutShort: String 47 | company: String 48 | image: Image! 49 | type: [ContactType!]! 50 | social: Social! 51 | keywords: [String!] 52 | location: Location! 53 | talks: [Session!] 54 | workshops: [Session!] 55 | noPhotography: Boolean 56 | country: Country! @deprecated(reason: "Use `location` instead") 57 | } 58 | 59 | """Type of the contact""" 60 | enum ContactType { 61 | PRESS 62 | SPEAKER 63 | TALK 64 | LIGHTNING_TALK 65 | KEYNOTE 66 | WORKSHOP 67 | WORKSHOP_HOST 68 | ORGANIZER 69 | SPONSOR 70 | GOLD_SPONSOR 71 | SILVER_SPONSOR 72 | BRONZE_SPONSOR 73 | PARTNER 74 | ATTENDEE 75 | } 76 | 77 | type Country { 78 | name: String! 79 | code: String! 80 | } 81 | 82 | type Font { 83 | family: String 84 | weight: String 85 | style: String 86 | fileName: String 87 | formats: [String!] 88 | href: String 89 | } 90 | 91 | type Fonts { 92 | primary: String! 93 | secondary: String! 94 | variants: [Font!]! 95 | } 96 | 97 | type Image { 98 | url: String! 99 | title: String 100 | style: Style 101 | } 102 | 103 | type Interval { 104 | begin: String! 105 | end: String! 106 | title: String 107 | sessions: [Session!]! 108 | location: Location 109 | } 110 | 111 | type Location { 112 | name: String 113 | about: String 114 | image: Image 115 | social: Social 116 | country: Country 117 | city: String 118 | address: String 119 | } 120 | 121 | type Logos { 122 | black: WithWithoutText! 123 | colored: WithWithoutText! 124 | white: WithWithoutText! 125 | } 126 | 127 | type Query { 128 | conference(id: ID!): Conference! 129 | conferences: [Conference!]! 130 | allConferences: [Conference!]! @deprecated(reason: "Use `conferences` instead") 131 | contact(conferenceId: ID!, contactName: String!): Contact! 132 | locations: [Contact!]! 133 | people: [Contact!]! 134 | sponsors: [Contact!]! 135 | schedule(day: String!, conferenceId: ID!): Schedule! 136 | series(id: ID!): Series! 137 | allSeries: [Series!]! 138 | themes: [Theme!]! 139 | theme(conferenceId: ID!): Theme! 140 | } 141 | 142 | type Schedule { 143 | day: String! 144 | location: Location 145 | description: String 146 | intervals: [Interval!]! 147 | } 148 | 149 | type Series { 150 | id: ID! 151 | name: String! 152 | conferences: [Conference!]! 153 | } 154 | 155 | type Session { 156 | type: SessionType! 157 | title: String! 158 | hasTitle: Boolean! 159 | description: String 160 | keywords: [String!] 161 | location: Location 162 | people: [Contact!] 163 | urls: SessionUrls 164 | sessions: [Session!] 165 | speakers: [Contact!]! @deprecated(reason: "Use `people` instead") 166 | } 167 | 168 | """Type of the session""" 169 | enum SessionType { 170 | TALK 171 | LIGHTNING_TALK 172 | KEYNOTE 173 | WORKSHOP 174 | PANEL 175 | BREAKFAST 176 | LUNCH 177 | COFFEE_BREAK 178 | ORGANIZATIONAL 179 | PARTY 180 | } 181 | 182 | type SessionUrls { 183 | web: String 184 | slides: String 185 | video: String 186 | } 187 | 188 | type Social { 189 | homepage: String 190 | twitter: String 191 | github: String 192 | facebook: String 193 | googleMaps: String 194 | medium: String 195 | instagram: String 196 | linkedin: String 197 | youtube: String 198 | vk: String 199 | pinterest: String 200 | vimeo: String 201 | dribble: String 202 | devto: String 203 | } 204 | 205 | type Style { 206 | backgroundSize: String! 207 | } 208 | 209 | type Theme { 210 | id: ID! 211 | fonts: Fonts! 212 | textures: [Image!]! 213 | colors: Colors! 214 | logos: Logos! 215 | } 216 | 217 | type WithWithoutText { 218 | withoutText: Image! 219 | withText: Image! 220 | } 221 | 222 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RelayEnvironmentProvider, preloadQuery } from 'react-relay/hooks'; 3 | import { BrowserRouter, useRoutes } from 'react-router-dom'; 4 | import { Home } from './Home'; 5 | import ConferencesQuery from './__generated__/ConferencesQuery.graphql'; 6 | import { relayEnvironment } from './relay/Environment'; 7 | 8 | const conferencesQuery = () => { 9 | const preloadedQuery = preloadQuery( 10 | relayEnvironment, 11 | ConferencesQuery, 12 | {}, 13 | { 14 | fetchPolicy: 'store-or-network', 15 | } 16 | ); 17 | 18 | return { 19 | data: preloadedQuery, 20 | }; 21 | }; 22 | 23 | const Routes = () => { 24 | const element = useRoutes([ 25 | { 26 | element: , 27 | path: '/', 28 | preload: conferencesQuery, 29 | }, 30 | ]); 31 | 32 | return element; 33 | }; 34 | 35 | export const App = () => { 36 | return ( 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/ConferencesQuery.tsx: -------------------------------------------------------------------------------- 1 | import { graphql } from 'react-relay/hooks'; 2 | 3 | export const ConferencesQuery = graphql` 4 | query ConferencesQuery { 5 | allConferences { 6 | id 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Home = () => { 4 | return

Home

; 5 | }; 6 | -------------------------------------------------------------------------------- /src/__generated__/ConferencesQuery.graphql.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @ts-nocheck 4 | 5 | import { ConcreteRequest } from "relay-runtime"; 6 | export type ConferencesQueryVariables = {}; 7 | export type ConferencesQueryResponse = { 8 | readonly allConferences: ReadonlyArray<{ 9 | readonly id: string; 10 | }>; 11 | }; 12 | export type ConferencesQuery = { 13 | readonly response: ConferencesQueryResponse; 14 | readonly variables: ConferencesQueryVariables; 15 | }; 16 | 17 | 18 | 19 | /* 20 | query ConferencesQuery { 21 | allConferences { 22 | id 23 | } 24 | } 25 | */ 26 | 27 | const node: ConcreteRequest = (function () { 28 | var v0 = [ 29 | ({ 30 | "alias": null, 31 | "args": null, 32 | "concreteType": "Conference", 33 | "kind": "LinkedField", 34 | "name": "allConferences", 35 | "plural": true, 36 | "selections": [ 37 | { 38 | "alias": null, 39 | "args": null, 40 | "kind": "ScalarField", 41 | "name": "id", 42 | "storageKey": null 43 | } 44 | ], 45 | "storageKey": null 46 | } as any) 47 | ]; 48 | return { 49 | "fragment": { 50 | "argumentDefinitions": [], 51 | "kind": "Fragment", 52 | "metadata": null, 53 | "name": "ConferencesQuery", 54 | "selections": (v0 /*: any*/), 55 | "type": "Query", 56 | "abstractKey": null 57 | }, 58 | "kind": "Request", 59 | "operation": { 60 | "argumentDefinitions": [], 61 | "kind": "Operation", 62 | "name": "ConferencesQuery", 63 | "selections": (v0 /*: any*/) 64 | }, 65 | "params": { 66 | "cacheID": "17cbf9fb41fdc647cfd87f70780bf16e", 67 | "id": null, 68 | "metadata": {}, 69 | "name": "ConferencesQuery", 70 | "operationKind": "query", 71 | "text": "query ConferencesQuery {\n allConferences {\n id\n }\n}\n" 72 | } 73 | } as any; 74 | })(); 75 | (node as any).hash = '996d53a1013ed2efd2e0a284e04bc8a5'; 76 | export default node; 77 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/prefer-query-selector */ 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import { App } from './App'; 5 | 6 | const rootElement = document.getElementById('root'); 7 | 8 | render(, rootElement); 9 | -------------------------------------------------------------------------------- /src/relay/Environment.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Environment, 3 | Network, 4 | RecordSource, 5 | Store, 6 | QueryResponseCache, 7 | } from 'relay-runtime'; 8 | import { RequestParameters } from 'relay-runtime/lib/util/RelayConcreteNode'; 9 | import { 10 | Variables, 11 | CacheConfig, 12 | } from 'relay-runtime/lib/util/RelayRuntimeTypes'; 13 | 14 | const oneMinute = 60 * 1000; 15 | const cache = new QueryResponseCache({ size: 250, ttl: oneMinute }); 16 | 17 | export const fetchQuery = async ( 18 | operation: RequestParameters, 19 | variables: Variables, 20 | cacheConfig: CacheConfig 21 | ) => { 22 | const queryId = operation.text || ''; 23 | const isMutation = operation.operationKind === 'mutation'; 24 | const isQuery = operation.operationKind === 'query'; 25 | const forceFetch = cacheConfig?.force; 26 | 27 | const fromCache = cache.get(queryId, variables); 28 | 29 | if (isQuery && fromCache !== null && !forceFetch) { 30 | return fromCache; 31 | } 32 | 33 | const response = await fetch(process.env.API_URL || '', { 34 | body: JSON.stringify({ 35 | query: operation.text, 36 | variables, 37 | }), 38 | credentials: 'include', 39 | headers: { 40 | 'Content-Type': 'application/json', 41 | }, 42 | method: 'POST', 43 | }); 44 | 45 | const json = await response.json(); 46 | 47 | if (isQuery && json && queryId !== '') { 48 | cache.set(queryId, variables, json); 49 | } 50 | 51 | if (isMutation) { 52 | cache.clear(); 53 | } 54 | 55 | return json; 56 | }; 57 | 58 | export const relayEnvironment = new Environment({ 59 | network: Network.create(fetchQuery), 60 | store: new Store(new RecordSource()), 61 | }); 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "baseUrl": "src", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "isolatedModules": true, 9 | "jsx": "preserve", 10 | "lib": [ 11 | "dom", 12 | "dom.iterable", 13 | "esnext" 14 | ], 15 | "module": "es2020", 16 | "moduleResolution": "node", 17 | "noEmit": true, 18 | "noImplicitAny": true, 19 | "noImplicitReturns": true, 20 | "noImplicitThis": true, 21 | "noUnusedLocals": true, 22 | "paths": { 23 | "@/*": [ 24 | "*" 25 | ] 26 | }, 27 | "resolveJsonModule": true, 28 | "skipLibCheck": true, 29 | "strict": true, 30 | "target": "es2015" 31 | }, 32 | "include": [ 33 | "src" 34 | ] 35 | } -------------------------------------------------------------------------------- /webpack.config.common.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-commonjs, import/unambiguous */ 2 | const path = require('path'); 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 4 | const HTMLWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: ['@babel/polyfill', path.resolve(__dirname, './src/index')], 8 | module: { 9 | rules: [ 10 | { 11 | exclude: /node_modules/, 12 | loader: 'babel-loader', 13 | test: /\.(ts|js)x?$/, 14 | }, 15 | ], 16 | }, 17 | output: { 18 | chunkFilename: '[name].[contenthash].js', 19 | filename: '[name].[contenthash].js', 20 | path: path.resolve(__dirname, 'dist'), 21 | }, 22 | plugins: [ 23 | new CleanWebpackPlugin(), 24 | new HTMLWebpackPlugin({ 25 | filename: 'index.html', 26 | inject: true, 27 | template: path.resolve(__dirname, 'src', 'index.html'), 28 | }), 29 | ], 30 | resolve: { 31 | extensions: ['*', '.js', '.jsx', '.ts', '.tsx'], 32 | alias: { 33 | '@': path.resolve(__dirname, 'src'), 34 | }, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /webpack.config.development.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-commonjs, import/unambiguous */ 2 | 3 | const { merge } = require('webpack-merge'); 4 | const common = require('./webpack.config.common'); 5 | 6 | module.exports = merge(common, { 7 | devtool: 'inline-source-map', 8 | mode: 'development', 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.css$/, 13 | use: ['style-loader', 'css-loader'], 14 | }, 15 | ], 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /webpack.config.production.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-commonjs, import/unambiguous */ 2 | 3 | const path = require('path'); 4 | const { merge } = require('webpack-merge'); 5 | const AssetsManifestPlugin = require('webpack-assets-manifest'); 6 | const TerserPlugin = require('terser-webpack-plugin'); 7 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 8 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 9 | const common = require('./webpack.config.common'); 10 | 11 | module.exports = merge(common, { 12 | devtool: false, 13 | mode: 'production', 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.css$/, 18 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 19 | }, 20 | ], 21 | }, 22 | optimization: { 23 | minimize: true, 24 | minimizer: [ 25 | new TerserPlugin({ 26 | cache: true, 27 | parallel: true, 28 | sourceMap: true, 29 | }), 30 | new OptimizeCssAssetsPlugin({ 31 | cssProcessorOptions: { 32 | map: { 33 | annotation: true, 34 | inline: false, 35 | }, 36 | }, 37 | }), 38 | ], 39 | usedExports: true, 40 | }, 41 | plugins: [ 42 | new AssetsManifestPlugin(), 43 | new MiniCssExtractPlugin({ 44 | filename: '[name].[contenthash].css', 45 | }), 46 | ], 47 | }); 48 | --------------------------------------------------------------------------------