├── .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 |
23 |
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 |
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 |
--------------------------------------------------------------------------------