├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .stylelintrc ├── README.md ├── babel.config.js ├── package.json ├── public └── index_dev.html ├── src ├── App.tsx ├── components │ ├── Footer.scss │ ├── Footer.tsx │ ├── Header.scss │ └── Header.tsx ├── index.tsx ├── pages │ ├── Home.tsx │ └── News.tsx ├── server.tsx ├── store │ ├── actions │ │ └── counterAction.ts │ ├── index.ts │ └── reducers │ │ ├── counterReducer.ts │ │ └── index.ts ├── styles │ └── GlobalStyle.ts └── util │ ├── sum.test.ts │ └── sum.ts ├── tsconfig.json ├── webpack.client.js ├── webpack.config.js ├── webpack.dev.js ├── webpack.server.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack.*.js 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | jest: true, 6 | }, 7 | extends: [ 8 | 'plugin:react/recommended', 9 | 'airbnb', 10 | 'plugin:prettier/recommended', 11 | 'prettier/react', 12 | 'plugin:jest-dom/recommended', 13 | ], 14 | plugins: ['react', '@typescript-eslint', 'jest', 'import'], 15 | globals: { 16 | Atomics: 'readonly', 17 | SharedArrayBuffer: 'readonly', 18 | }, 19 | parser: '@typescript-eslint/parser', 20 | parserOptions: { 21 | ecmaFeatures: { 22 | jsx: true, 23 | }, 24 | ecmaVersion: 2018, 25 | sourceType: 'module', 26 | }, 27 | settings: { 28 | 'import/resolver': { 29 | node: { 30 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 31 | }, 32 | "typescript": { 33 | "alwaysTryTypes": true, 34 | }, 35 | }, 36 | }, 37 | rules: { 38 | 'react/jsx-filename-extension': [ 39 | 1, 40 | { 41 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 42 | }, 43 | ], 44 | 45 | 'import/extensions': [ 46 | 'error', 47 | 'ignorePackages', 48 | { 49 | js: 'never', 50 | jsx: 'never', 51 | ts: 'never', 52 | tsx: 'never', 53 | }, 54 | ], 55 | 56 | 'no-confusing-arrow': 'off', 57 | 'implicit-arrow-linebreak': 'off', 58 | 'import/no-extraneous-dependencies': [ 59 | 'error', 60 | { 61 | devDependencies: [ 62 | 'src/server.tsx', 63 | ], 64 | }, 65 | ], 66 | 'react/prop-types': 'off', 67 | 'max-len': ["error", 100], 68 | }, 69 | 70 | overrides: [ 71 | { 72 | files: ['*.ts', '*.tsx'], 73 | rules: { 74 | '@typescript-eslint/no-unused-vars': [2, { args: 'none' }], 75 | }, 76 | }, 77 | ], 78 | }; 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | yarn-error.log 4 | **/.DS_Store 5 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "trailingComma": "all", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true, 7 | "jsxBracketSameLine": true, 8 | "endOfLine": "lf" 9 | } -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "processors": [ 3 | "stylelint-processor-styled-components" 4 | ], 5 | "extends": [ 6 | "stylelint-config-recommended", 7 | "stylelint-config-styled-components" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-typescript-ssr-codeSplitting 2 | 3 | 코드에 관한 설명은 아래 블로그를 참고해주세요. 4 | https://medium.com/@minoo/next-js-%EC%B2%98%EB%9F%BC-server-side-rendering-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-7608e82a0ab1 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | function isWebTarget(caller) { 2 | return Boolean(caller && caller.target === 'web'); 3 | } 4 | 5 | function isWebpack(caller) { 6 | return Boolean(caller && caller.name === 'babel-loader'); 7 | } 8 | 9 | module.exports = api => { 10 | const web = api.caller(isWebTarget); 11 | const webpack = api.caller(isWebpack); 12 | 13 | return { 14 | presets: [ 15 | '@babel/preset-react', 16 | [ 17 | '@babel/preset-env', 18 | { 19 | useBuiltIns: web ? 'entry' : undefined, 20 | targets: !web ? { node: 'current' } : undefined, 21 | modules: webpack ? false : 'commonjs', 22 | }, 23 | ], 24 | '@babel/preset-typescript', 25 | ], 26 | plugins: [ 27 | '@loadable/babel-plugin', 28 | [ 29 | 'module-resolver', 30 | { 31 | root: ['.'], 32 | extensions: ['.ts', '.tsx'], 33 | alias: { 34 | '@src': './src', 35 | '@components': './src/components', 36 | '@pages': './src/components/pages', 37 | '@store': './src/store', 38 | '@reducers': './src/store/reducers', 39 | '@actions': './src/store/actions', 40 | '@util': './src/util', 41 | '@styles': './src/styles', 42 | }, 43 | }, 44 | ], 45 | ], 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-pure-ssr", 3 | "version": "0.0.1", 4 | "description": "react ssr examples without framework", 5 | "main": "dist/server.js", 6 | "author": "dylanju", 7 | "license": "ISC", 8 | "keywords": [ 9 | "ssr", 10 | "server-side-rendering", 11 | "react", 12 | "typescript", 13 | "react-router", 14 | "react-router-dom" 15 | ], 16 | "homepage": "https://github.com/DylanJu/react-pure-ssr", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/DylanJu/react-pure-ssr.git" 20 | }, 21 | "scripts": { 22 | "start": "yarn build:dev && node ./dist/server.js", 23 | "start:wds": "webpack-dev-server --env=dev --profile --colors", 24 | "build": "rm -rf dist/ && NODE_ENV=production yarn build:client && NODE_ENV=production yarn build:server", 25 | "build:dev": "rm -rf dist/ && NODE_ENV=development yarn build:client && NODE_ENV=development yarn build:server", 26 | "build:server": "webpack --env=server --progress --profile --colors", 27 | "build:client": "webpack --env=client --progress --profile --colors", 28 | "test": "jest --watchAll" 29 | }, 30 | "dependencies": { 31 | "@loadable/component": "^5.12.0", 32 | "@loadable/server": "^5.12.0", 33 | "express": "^4.17.1", 34 | "moment": "^2.24.0", 35 | "react": "^16.12.0", 36 | "react-dom": "^16.12.0", 37 | "react-helmet": "^6.0.0", 38 | "react-redux": "^7.1.3", 39 | "react-router-dom": "^5.1.2", 40 | "redux": "^4.0.5", 41 | "styled-components": "^5.0.0", 42 | "styled-normalize": "^8.0.6", 43 | "typescript": "^3.7.5" 44 | }, 45 | "devDependencies": { 46 | "@babel/core": "^7.6.0", 47 | "@babel/preset-env": "^7.6.0", 48 | "@babel/preset-react": "^7.0.0", 49 | "@babel/preset-typescript": "^7.8.3", 50 | "@loadable/babel-plugin": "^5.12.0", 51 | "@loadable/webpack-plugin": "^5.12.0", 52 | "@types/express": "^4.17.1", 53 | "@types/jest": "^25.1.0", 54 | "@types/loadable__component": "^5.10.0", 55 | "@types/loadable__server": "^5.9.1", 56 | "@types/react": "^16.9.19", 57 | "@types/react-dom": "^16.9.5", 58 | "@types/react-helmet": "^5.0.9", 59 | "@types/react-redux": "^7.1.7", 60 | "@types/react-router-dom": "^5.1.3", 61 | "@types/redux": "^3.6.0", 62 | "@types/styled-components": "^5.1.0", 63 | "@types/webpack-dev-middleware": "^3.7.0", 64 | "@types/webpack-env": "^1.14.0", 65 | "@types/webpack-hot-middleware": "^2.16.5", 66 | "@typescript-eslint/eslint-plugin": "^2.17.0", 67 | "@typescript-eslint/parser": "^2.17.0", 68 | "add": "^2.0.6", 69 | "babel-loader": "^8.0.6", 70 | "babel-plugin-module-resolver": "^4.0.0", 71 | "babel-plugin-styled-components": "^1.10.7", 72 | "core-js": "2", 73 | "css-loader": "^3.4.2", 74 | "eslint": "^6.8.0", 75 | "eslint-config-airbnb": "^18.0.1", 76 | "eslint-config-prettier": "^6.9.0", 77 | "eslint-import-resolver-alias": "^1.1.2", 78 | "eslint-import-resolver-typescript": "^2.0.0", 79 | "eslint-plugin-import": "^2.20.0", 80 | "eslint-plugin-jest": "^23.6.0", 81 | "eslint-plugin-jest-dom": "^2.0.0", 82 | "eslint-plugin-jsx-a11y": "^6.2.3", 83 | "eslint-plugin-prettier": "^3.1.2", 84 | "eslint-plugin-react": "^7.18.0", 85 | "eslint-plugin-react-hooks": "^3.0.0", 86 | "html-webpack-plugin": "^4.2.0", 87 | "jest": "^25.1.0", 88 | "mini-css-extract-plugin": "^0.9.0", 89 | "node-sass": "^4.13.1", 90 | "prettier": "^2.0.4", 91 | "sass-loader": "^8.0.2", 92 | "stylelint": "^13.0.0", 93 | "stylelint-config-recommended": "^3.0.0", 94 | "stylelint-config-styled-components": "^0.1.1", 95 | "stylelint-processor-styled-components": "^1.9.0", 96 | "ts-loader": "^7.0.0", 97 | "typescript-plugin-styled-components": "^1.4.4", 98 | "webpack": "^4.39.3", 99 | "webpack-cli": "^3.3.8", 100 | "webpack-dev-middleware": "^3.7.1", 101 | "webpack-dev-server": "^3.8.0", 102 | "webpack-hot-middleware": "^2.25.0", 103 | "webpack-node-externals": "^1.7.2", 104 | "yarn": "^1.22.4" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /public/index_dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Hello World! 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch } from 'react-router-dom'; 3 | import { Helmet } from 'react-helmet'; 4 | import loadable from '@loadable/component'; 5 | 6 | const Header = loadable(() => import(/* webpackChunkName: "Header" */ './components/Header')); 7 | const Footer = loadable(() => import(/* webpackChunkName: "Footer" */ './components/Footer')); 8 | const Home = loadable(() => import(/* webpackChunkName: "Home" */ './pages/Home')); 9 | const News = loadable(() => import(/* webpackChunkName: "News" */ './pages/News')); 10 | 11 | export default function App() { 12 | return ( 13 |
14 | 15 | App 16 | 17 |
} /> 18 | 19 | } /> 20 | } /> 21 | 22 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | color: palegreen; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Footer.scss'; 3 | 4 | const Footer = () => ; 5 | 6 | export default Footer; 7 | -------------------------------------------------------------------------------- /src/components/Header.scss: -------------------------------------------------------------------------------- 1 | header { 2 | a { 3 | color: rosybrown; 4 | } 5 | } -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import './Header.scss'; 4 | 5 | const Header = () => ( 6 |
7 | Home 8 | News 9 |
10 | ); 11 | 12 | export default Header; 13 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { hydrate } from 'react-dom'; 2 | import React from 'react'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { loadableReady } from '@loadable/component'; 5 | import { Provider } from 'react-redux'; 6 | 7 | import configureStore from '@store'; 8 | import GlobalStyle from '@styles/GlobalStyle'; 9 | import App from './App'; 10 | 11 | const store = configureStore(); 12 | 13 | loadableReady(() => { 14 | const rootElement = document.getElementById('root'); 15 | hydrate( 16 | 17 | 18 | <> 19 | 20 | 21 | 22 | 23 | , 24 | rootElement, 25 | ); 26 | }); 27 | 28 | if (module.hot) { 29 | module.hot.accept(); 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import moment from 'moment'; 4 | import { increase, decrease } from '@actions/counterAction'; 5 | 6 | function Home() { 7 | const number = useSelector((state: any) => state.counterReducer); 8 | const dispatch = useDispatch(); 9 | 10 | return ( 11 |
12 | My Home 13 |

{moment().format('MMMM Do YYYY, h:mm:ss a')}

14 |
counter: {number}
15 | 18 | 21 |
22 | ); 23 | } 24 | 25 | export default Home; 26 | -------------------------------------------------------------------------------- /src/pages/News.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | import styled from 'styled-components'; 4 | 5 | const Title = styled('h1')` 6 | margin: 0; 7 | font-size: 20px; 8 | color: orange; 9 | `; 10 | 11 | const News = () => ( 12 |
13 | 14 | News 15 | 16 | News 17 |
18 | ); 19 | 20 | export default News; 21 | -------------------------------------------------------------------------------- /src/server.tsx: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | import React from 'react'; 4 | import { StaticRouter } from 'react-router-dom'; 5 | import { ChunkExtractor } from '@loadable/server'; 6 | import { Helmet } from 'react-helmet'; 7 | import { Provider } from 'react-redux'; 8 | import { createStore } from 'redux'; 9 | import { renderToString } from 'react-dom/server'; 10 | 11 | import reducers from './store/reducers'; 12 | 13 | const app = express(); 14 | 15 | if (process.env.NODE_ENV !== 'production') { 16 | /* eslint-disable global-require, import/no-extraneous-dependencies */ 17 | /* eslint-disable no-param-reassign */ 18 | const webpack = require('webpack'); 19 | const webpackConfig = require('../webpack.client.js').map((config: any) => { 20 | config.output.path = config.output.path.replace('dist/dist/', 'dist/'); 21 | return config; 22 | }); 23 | 24 | const webpackDevMiddleware = require('webpack-dev-middleware'); 25 | const webpackHotMiddleware = require('webpack-hot-middleware'); 26 | /* eslint-enable global-require, import/no-extraneous-dependencies */ 27 | /* eslint-enable no-param-reassign */ 28 | 29 | const compiler = webpack(webpackConfig); 30 | 31 | app.use( 32 | webpackDevMiddleware(compiler, { 33 | logLevel: 'silent', 34 | publicPath: webpackConfig[0].output.publicPath, 35 | writeToDisk: true, 36 | }), 37 | ); 38 | 39 | app.use(webpackHotMiddleware(compiler)); 40 | } 41 | 42 | app.use(express.static(path.resolve(__dirname))); 43 | 44 | app.get('*', (req, res) => { 45 | const nodeStats = path.resolve(__dirname, './node/loadable-stats.json'); 46 | const webStats = path.resolve(__dirname, './web/loadable-stats.json'); 47 | const nodeExtractor = new ChunkExtractor({ statsFile: nodeStats }); 48 | const { default: App } = nodeExtractor.requireEntrypoint(); 49 | const webExtractor = new ChunkExtractor({ statsFile: webStats }); 50 | 51 | const store = createStore(reducers); 52 | const context = {}; 53 | 54 | const jsx = webExtractor.collectChunks( 55 | 56 | 57 | 58 | 59 | , 60 | ); 61 | 62 | const html = renderToString(jsx); 63 | const helmet = Helmet.renderStatic(); 64 | 65 | res.set('content-type', 'text/html'); 66 | res.send(` 67 | 68 | 69 | 70 | 71 | 72 | ${helmet.title.toString()} 73 | ${webExtractor.getLinkTags()} 74 | ${webExtractor.getStyleTags()} 75 | 76 | 77 |
${html}
78 | ${webExtractor.getScriptTags()} 79 | 80 | 81 | `); 82 | }); 83 | 84 | app.listen(3003, () => console.log('Server started http://localhost:3003')); 85 | -------------------------------------------------------------------------------- /src/store/actions/counterAction.ts: -------------------------------------------------------------------------------- 1 | export const increase = () => { 2 | return { 3 | type: 'INCREMENT', 4 | }; 5 | }; 6 | 7 | export const decrease = () => { 8 | return { 9 | type: 'DECREMENT', 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore, Store } from 'redux'; 2 | import rootReducer, { RootState } from './reducers'; 3 | 4 | const configureStore = (preloadedState?: RootState): Store => 5 | createStore(rootReducer, preloadedState); 6 | 7 | export default configureStore; 8 | -------------------------------------------------------------------------------- /src/store/reducers/counterReducer.ts: -------------------------------------------------------------------------------- 1 | function counter(state = 0, action: any) { 2 | switch (action.type) { 3 | case 'INCREMENT': 4 | return state + 1; 5 | case 'DECREMENT': 6 | return state - 1; 7 | default: 8 | return state; 9 | } 10 | } 11 | 12 | export default counter; 13 | -------------------------------------------------------------------------------- /src/store/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import counterReducer from './counterReducer'; 4 | 5 | const rootReducer = combineReducers({ 6 | counterReducer, 7 | }); 8 | 9 | export default rootReducer; 10 | 11 | export type RootState = ReturnType; 12 | -------------------------------------------------------------------------------- /src/styles/GlobalStyle.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | import { normalize } from 'styled-normalize'; 3 | 4 | export const GlobalStyle = createGlobalStyle` 5 | ${normalize} 6 | 7 | h1, h2, h3, h4, h5, h6, p { 8 | margin: 0; 9 | font-weight: 400; 10 | color: white; 11 | } 12 | 13 | button { 14 | border: none; 15 | background-color: transparent; 16 | outline: none; 17 | } 18 | 19 | select { 20 | appearance: none; 21 | border-radius: 0px; 22 | padding: 6px 9px; 23 | border: none; 24 | } 25 | `; 26 | 27 | export default GlobalStyle; 28 | -------------------------------------------------------------------------------- /src/util/sum.test.ts: -------------------------------------------------------------------------------- 1 | import sum from './sum'; 2 | 3 | test('adds 1 + 2 to equal 3', () => { 4 | expect(sum(1, 2)).toBe(3); 5 | }); 6 | -------------------------------------------------------------------------------- /src/util/sum.ts: -------------------------------------------------------------------------------- 1 | function sum(a: number, b: number): number { 2 | return a + b; 3 | } 4 | 5 | export default sum; 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | "paths": { 45 | "@src/*": ["src/*"], 46 | "@components/*": ["src/components/*"], 47 | "@pages/*": ["src/components/pages/*"], 48 | "@store": ["src/store/index"], 49 | "@actions/*": ["src/store/actions/*"], 50 | "@reducers/*": ["src/store/reducers/*"], 51 | "@util/*": ["src/util/*"], 52 | "@styles/*": ["src/styles/*"], 53 | }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 54 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 55 | // "typeRoots": [], /* List of folders to include type definitions from. */ 56 | // "types": [], /* Type declaration files to be included in compilation. */ 57 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 58 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 59 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 60 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 61 | 62 | /* Source Map Options */ 63 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 64 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 65 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 66 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 67 | 68 | /* Experimental Options */ 69 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 70 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 71 | }, 72 | "exclude": [ 73 | "**/*.test.*", 74 | "**/*.spec.*", 75 | "node_modules" 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /webpack.client.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const nodeExternals = require('webpack-node-externals'); 4 | const LoadablePlugin = require('@loadable/webpack-plugin'); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | const createStyledComponentsTransformer = require('typescript-plugin-styled-components').default; 7 | 8 | const devMode = process.env.NODE_ENV !== 'production'; 9 | const hotMiddlewareScript = `webpack-hot-middleware/client?name=web&path=/__webpack_hmr&timeout=20000&reload=true`; 10 | const styledComponentsTransformer = createStyledComponentsTransformer(); 11 | 12 | const getEntryPoint = target => { 13 | if (target === 'node') { 14 | return ['./src/App.tsx']; 15 | } 16 | return devMode ? [hotMiddlewareScript, './src/index.tsx'] : ['./src/index.tsx']; 17 | }; 18 | 19 | const getConfig = target => ({ 20 | mode: devMode ? 'development' : 'production', 21 | 22 | name: target, 23 | 24 | target, 25 | 26 | entry: getEntryPoint(target), 27 | 28 | output: { 29 | path: path.resolve(__dirname, `dist/${target}`), 30 | filename: '[name].js', 31 | publicPath: '/web/', 32 | libraryTarget: target === 'node' ? 'commonjs2' : undefined, 33 | }, 34 | 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.tsx?$/, 39 | use: [ 40 | 'babel-loader', 41 | { 42 | loader: 'ts-loader', 43 | options: { 44 | getCustomTransformers: () => ({ before: [styledComponentsTransformer] }), 45 | }, 46 | }, 47 | ], 48 | }, 49 | { 50 | test: /\.(scss|css)$/, 51 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], 52 | }, 53 | ], 54 | }, 55 | 56 | resolve: { 57 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 58 | alias: { 59 | pages: path.resolve('src/pages/'), 60 | components: path.resolve('src/components/'), 61 | actions: path.resolve('src/store/actions/'), 62 | reducers: path.resolve('src/store/reducers/'), 63 | util: path.resolve('src/util/'), 64 | }, 65 | }, 66 | 67 | plugins: 68 | target === 'web' 69 | ? [new LoadablePlugin(), new webpack.HotModuleReplacementPlugin(), new MiniCssExtractPlugin()] 70 | : [new LoadablePlugin(), new MiniCssExtractPlugin()], 71 | 72 | externals: target === 'node' ? ['@loadable/component', nodeExternals()] : undefined, 73 | }); 74 | 75 | module.exports = [getConfig('web'), getConfig('node')]; 76 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(env) { 2 | return require(`./webpack.${env}.js`); 3 | }; 4 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | 6 | module.exports = { 7 | mode: 'development', 8 | 9 | entry: './src/index.tsx', 10 | 11 | devServer: { 12 | historyApiFallback: true, 13 | inline: true, 14 | port: 3000, 15 | hot: true, 16 | publicPath: '/', 17 | }, 18 | 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.tsx?$/, 23 | use: ['babel-loader', 'ts-loader'], 24 | }, 25 | { 26 | test: /\.(scss|css)$/, 27 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'], 28 | }, 29 | ], 30 | }, 31 | 32 | resolve: { 33 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 34 | alias: { 35 | pages: path.resolve('src/pages/'), 36 | components: path.resolve('src/components/'), 37 | actions: path.resolve('src/store/actions/'), 38 | reducers: path.resolve('src/store/reducers/'), 39 | util: path.resolve('src/util/'), 40 | }, 41 | }, 42 | 43 | plugins: [ 44 | new webpack.HotModuleReplacementPlugin(), 45 | new MiniCssExtractPlugin(), 46 | new HtmlWebpackPlugin({ 47 | filename: 'index.html', 48 | template: 'public/index_dev.html', 49 | }), 50 | ], 51 | }; 52 | -------------------------------------------------------------------------------- /webpack.server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | 4 | module.exports = { 5 | mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', 6 | 7 | target: 'node', 8 | 9 | node: false, // it enables '__dirname' in files. If is not, '__dirname' always return '/'. 10 | 11 | entry: { 12 | server: './src/server.tsx', 13 | }, 14 | 15 | output: { 16 | path: path.resolve(__dirname, './dist'), 17 | filename: '[name].js', 18 | chunkFilename: '[name].js', 19 | }, 20 | 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.tsx?$/, 25 | use: ['babel-loader', 'ts-loader'], 26 | exclude: /node_modules/, 27 | }, 28 | ], 29 | }, 30 | 31 | resolve: { 32 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 33 | }, 34 | 35 | externals: [nodeExternals()], 36 | }; 37 | --------------------------------------------------------------------------------