├── .gitignore ├── .prettierrc ├── .stylelintrc ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── README.md ├── app1 ├── .storybook │ ├── addons.js │ ├── config.js │ ├── preview-head.html │ └── webpack.config.js ├── babel.config.js ├── jest.config.js ├── package.json ├── src │ ├── assets │ │ └── favicon.ico │ ├── comps │ │ ├── App │ │ │ ├── App.story.tsx │ │ │ ├── App.test.tsx │ │ │ ├── App.tsx │ │ │ └── index.ts │ │ └── InfoText │ │ │ ├── InfoText.tsx │ │ │ └── index.ts │ ├── index.html │ └── index.tsx ├── tsconfig.json └── webpack.config.js ├── app2 ├── .storybook │ ├── addons.js │ ├── config.js │ ├── preview-head.html │ └── webpack.config.js ├── babel.config.js ├── jest.config.js ├── package.json ├── src │ ├── assets │ │ └── favicon.ico │ ├── comps │ │ ├── App │ │ │ ├── App.story.tsx │ │ │ ├── App.test.tsx │ │ │ ├── App.tsx │ │ │ └── index.ts │ │ └── WarningText │ │ │ ├── WarningText.tsx │ │ │ └── index.ts │ ├── index.html │ └── index.tsx ├── tsconfig.json └── webpack.config.js ├── common ├── .storybook │ ├── addons.js │ ├── config.js │ ├── preview-head.html │ └── webpack.config.js ├── generateDTS.js ├── jest.config.js ├── package.json ├── src │ ├── comps │ │ ├── Header │ │ │ ├── Header.story.tsx │ │ │ ├── Header.test.tsx │ │ │ ├── Header.tsx │ │ │ ├── __snapshots__ │ │ │ │ └── Header.test.tsx.snap │ │ │ └── index.ts │ │ └── index.ts │ └── foo │ │ ├── foo.test.ts │ │ ├── foo.ts │ │ └── index.ts └── tsconfig.json ├── jest.config.js ├── lerna.json ├── package.json ├── tools ├── babel │ └── config.js ├── jest │ ├── config.js │ ├── fileMock.js │ ├── jestFrameworkSetup.js │ └── jestSetup.js └── webpack │ ├── config.js │ ├── storybookConfig.js │ └── styledComponentsTransformer.js ├── tsconfig.json ├── tslint.json ├── wallaby.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/*.log 3 | **/dist 4 | **/build 5 | **/.cache 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all" 8 | } -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "processors": ["stylelint-processor-styled-components"], 3 | "extends": [ 4 | "stylelint-config-recommended", 5 | "stylelint-config-styled-components" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Jest", 9 | "type": "node", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/node_modules/jest-cli/bin/jest.js", 12 | "stopOnEntry": false, 13 | "args": [ 14 | "--config", 15 | "jest.config.js", 16 | "--runInBand", 17 | "--colors", 18 | "${fileBasename}" 19 | ], 20 | "runtimeArgs": ["--nolazy"], 21 | "env": { 22 | "NODE_ENV": "development" 23 | }, 24 | "console": "internalConsole", 25 | "sourceMaps": true 26 | }, 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.useIgnoreFiles": true, 3 | "editor.tabSize": 2, 4 | "files.exclude": { 5 | "**/.git": true, 6 | "**/.svn": true, 7 | "**/.hg": true, 8 | "**/.log": true, 9 | "**/CVS": true, 10 | "**/.DS_Store": true, 11 | "**/dist": true, 12 | "**/build": true, 13 | }, 14 | "search.exclude": { 15 | "**/node_modules": true, 16 | "**/dist": true, 17 | "**/build": true, 18 | "package-lock.json": true, 19 | "*.lock": true, 20 | "*.log": true 21 | }, 22 | "tslint.autoFixOnSave": true, 23 | "material-icon-theme.files.associations": { 24 | "*.story.tsx": "smarty", 25 | "*.story.test.ts": "contributing", 26 | "*.test.tsx.snap": "sublime", 27 | "*.test.ts.snap": "sublime", 28 | "*.snap": "sublime", 29 | "*.storyshot": "sublime" 30 | }, 31 | "files.associations": { 32 | "*.snap": "xml", 33 | "*.storyshot": "xml" 34 | }, 35 | "css.validate": false, 36 | "less.validate": false, 37 | "scss.validate": false, 38 | "react-native-storybooks.port": 7007, 39 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "compile:watch", 9 | "problemMatcher": ["$tsc-watch"], 10 | "isBackground": true, 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + Typescript Mono repo 2 | 3 | Using Lerna, Yarn Workspaces, TS 3.0 project references to make a hopefully useful react monorepo 4 | 5 | Here you could publish the "common" library to npm too so other projects can use it 6 | 7 | Am attempting a nice full example including storybook, styled components, babel 7, hot reloading etc 8 | 9 | Also trying to share build config, tsconfig etc too 10 | 11 | ## Install & Run 12 | 13 | * Install node 14 | * `yarn install` 15 | * `yarn start` : runs apps and opens browser tab for each one 16 | * `yarn storybook` : runs storybook for apps 17 | * `yarn compile:watch` : compiles, type checks and watches all typescript. Can use form IDE. 18 | * `yarn test:ci` : runs all jest tests across projects, lint, style lint 19 | * `yarn build:ci`: creates bundles in the app*/dist directories, creates storybook static html in dist dir, analyses bundle in dist dir etc 20 | 21 | ## TODO 22 | 23 | * Jest not working with code coverage: https://github.com/facebook/jest/issues/5417 24 | * Using babel to transpile typescript would be great, avoids ts-jest and happypack etc, can use babel plugins more easily 25 | * Getting styled components to have nice css class names 26 | * Better sourcemaps, not working on the common library at the mo. 27 | * Cooler webpack stuff like PWA generation etc 28 | * Use typescript in tools directory and for all those configs -------------------------------------------------------------------------------- /app1/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | -------------------------------------------------------------------------------- /app1/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | // automatically import all files ending in *.story.tsx 4 | const req = require.context('../src', true, /.story.ts[x]?$/); 5 | function loadStories() { 6 | req.keys().forEach(filename => req(filename)); 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /app1/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app1/.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { getStorybookWebpackConfig } = require("../../tools/webpack/storybookConfig") 2 | 3 | const config = getStorybookWebpackConfig(__dirname); 4 | 5 | module.exports = config; 6 | -------------------------------------------------------------------------------- /app1/babel.config.js: -------------------------------------------------------------------------------- 1 | const { getBabelConfig } = require("../tools/babel/config") 2 | 3 | const config = getBabelConfig(); 4 | 5 | module.exports = config; 6 | -------------------------------------------------------------------------------- /app1/jest.config.js: -------------------------------------------------------------------------------- 1 | const { getJestConfig } = require("../tools/jest/config") 2 | 3 | const config = getJestConfig(__dirname); 4 | 5 | module.exports = config; 6 | -------------------------------------------------------------------------------- /app1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@monorepo/app1", 3 | "description": "Monorepo Webapp1", 4 | "version": "1.0.0", 5 | "private": true, 6 | "dependencies": { 7 | "@monorepo/common": "^1.0.0" 8 | }, 9 | "scripts": { 10 | "clean:build": "rimraf build && mkdirp build", 11 | "clean:dist": "rimraf dist && mkdirp dist", 12 | "compile": "tsc -b", 13 | "start": "webpack-dev-server --env development --open --port 3001", 14 | "test": "jest", 15 | "build": "webpack --env production", 16 | "lint": "tslint -c ../tslint.json -p tsconfig.json src/**/*.{ts,tsx}", 17 | "lint:fix": "yarn lint --fix", 18 | "lint:style": "stylelint src/**/*.{ts,tsx}", 19 | "storybook": "start-storybook -c .storybook -p 3002", 20 | "storybook:build": "build-storybook -c .storybook -o build/storybook" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app1/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axelnormand/react-typescript-monorepo/c3a1e1770fe5512cf163e3807ef4623bc769073d/app1/src/assets/favicon.ico -------------------------------------------------------------------------------- /app1/src/comps/App/App.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import React from 'react'; 3 | import App from './App'; 4 | 5 | storiesOf('App', module).add('default', () => ); 6 | -------------------------------------------------------------------------------- /app1/src/comps/App/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-testing-library'; 3 | import App from './App'; 4 | 5 | test('renders', () => { 6 | const { getByText } = render(); 7 | const headerText = getByText(/Welcome/); 8 | expect(headerText).toBeDefined(); 9 | }); 10 | -------------------------------------------------------------------------------- /app1/src/comps/App/App.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from '@monorepo/common/comps'; 2 | import { bar } from '@monorepo/common/foo'; 3 | import React from 'react'; 4 | import InfoText from 'src/comps/InfoText'; 5 | 6 | const App = () => { 7 | return ( 8 | <> 9 |
Welcome to Monorepo Webapp 1!
10 | {bar()} 11 | 12 | ); 13 | }; 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /app1/src/comps/App/index.ts: -------------------------------------------------------------------------------- 1 | import App from './App'; 2 | export default App; 3 | -------------------------------------------------------------------------------- /app1/src/comps/InfoText/InfoText.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const InfoText = styled.span` 4 | color: #aa11ff; 5 | `; 6 | 7 | export default InfoText; 8 | -------------------------------------------------------------------------------- /app1/src/comps/InfoText/index.ts: -------------------------------------------------------------------------------- 1 | import InfoText from './InfoText'; 2 | export default InfoText; 3 | -------------------------------------------------------------------------------- /app1/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | App1 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | -------------------------------------------------------------------------------- /app1/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from 'src/comps/App'; 4 | 5 | const rootDOMElement = document.getElementById('app') as HTMLElement; 6 | 7 | const render = (AppComponent = App) => 8 | ReactDOM.render(, rootDOMElement); 9 | 10 | render(App); 11 | 12 | if ((module as any).hot) { 13 | (module as any).hot.accept(['./comps/App'], () => { 14 | ReactDOM.unmountComponentAtNode(rootDOMElement); 15 | const NextApp = require('./comps/App').default; 16 | render(NextApp); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /app1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "rootDir": "src", 6 | "outDir": "build/tsc", 7 | "paths": { 8 | "src/*": ["src/*"], 9 | "@monorepo/common/*": ["../common/src/*"] 10 | } 11 | }, 12 | "include": ["src/**/*"], 13 | "references": [ 14 | { "path": "../common" } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /app1/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { getWebpackConfig } = require("../tools/webpack/config") 2 | 3 | const config = getWebpackConfig(__dirname); 4 | 5 | module.exports = config; 6 | -------------------------------------------------------------------------------- /app2/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | -------------------------------------------------------------------------------- /app2/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | // automatically import all files ending in *.story.tsx 4 | const req = require.context('../src', true, /.story.ts[x]?$/); 5 | function loadStories() { 6 | req.keys().forEach(filename => req(filename)); 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /app2/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app2/.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { getStorybookWebpackConfig } = require("../../tools/webpack/storybookConfig") 2 | 3 | const config = getStorybookWebpackConfig(__dirname); 4 | 5 | module.exports = config; 6 | -------------------------------------------------------------------------------- /app2/babel.config.js: -------------------------------------------------------------------------------- 1 | const { getBabelConfig } = require("../tools/babel/config") 2 | 3 | const config = getBabelConfig(); 4 | 5 | module.exports = config; 6 | -------------------------------------------------------------------------------- /app2/jest.config.js: -------------------------------------------------------------------------------- 1 | const { getJestConfig } = require("../tools/jest/config") 2 | 3 | const config = getJestConfig(__dirname); 4 | 5 | module.exports = config; 6 | -------------------------------------------------------------------------------- /app2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@monorepo/app2", 3 | "description": "Monorepo Webapp 2", 4 | "version": "1.0.0", 5 | "private": true, 6 | "dependencies": { 7 | "@monorepo/common": "^1.0.0" 8 | }, 9 | "scripts": { 10 | "clean:build": "rimraf build && mkdirp build", 11 | "clean:dist": "rimraf dist && mkdirp dist", 12 | "compile": "tsc -b", 13 | "start": "webpack-dev-server --env development --open --port 4001", 14 | "test": "jest", 15 | "build": "webpack --env production", 16 | "lint": "tslint -c ../tslint.json -p tsconfig.json src/**/*.{ts,tsx}", 17 | "lint:fix": "yarn lint --fix", 18 | "lint:style": "stylelint src/**/*.{ts,tsx}", 19 | "storybook": "start-storybook -c .storybook -p 4002", 20 | "storybook:build": "build-storybook -c .storybook -o build/storybook" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app2/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axelnormand/react-typescript-monorepo/c3a1e1770fe5512cf163e3807ef4623bc769073d/app2/src/assets/favicon.ico -------------------------------------------------------------------------------- /app2/src/comps/App/App.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import React from 'react'; 3 | import App from './App'; 4 | 5 | storiesOf('App', module).add('default', () => ); 6 | -------------------------------------------------------------------------------- /app2/src/comps/App/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-testing-library'; 3 | import App from './App'; 4 | 5 | test('renders', () => { 6 | const { getByText } = render(); 7 | const headerText = getByText(/Welcome/); 8 | expect(headerText).toBeDefined(); 9 | }); 10 | -------------------------------------------------------------------------------- /app2/src/comps/App/App.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from '@monorepo/common/comps'; 2 | import { bar } from '@monorepo/common/foo'; 3 | import React from 'react'; 4 | import WarningText from 'src/comps/WarningText'; 5 | 6 | const App = () => { 7 | return ( 8 | <> 9 |
Welcome to Monorepo Webapp 2!
10 | {bar()} 11 | 12 | ); 13 | }; 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /app2/src/comps/App/index.ts: -------------------------------------------------------------------------------- 1 | import App from './App'; 2 | export default App; 3 | -------------------------------------------------------------------------------- /app2/src/comps/WarningText/WarningText.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const WarningText = styled.span` 4 | color: #aa11ff; 5 | `; 6 | 7 | export default WarningText; 8 | -------------------------------------------------------------------------------- /app2/src/comps/WarningText/index.ts: -------------------------------------------------------------------------------- 1 | import WarningText from './WarningText'; 2 | export default WarningText; 3 | -------------------------------------------------------------------------------- /app2/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | Monorepo 2 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | -------------------------------------------------------------------------------- /app2/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from 'src/comps/App'; 4 | 5 | const rootDOMElement = document.getElementById('app') as HTMLElement; 6 | 7 | const render = (AppComponent = App) => 8 | ReactDOM.render(, rootDOMElement); 9 | 10 | render(App); 11 | 12 | if ((module as any).hot) { 13 | (module as any).hot.accept(['./comps/App'], () => { 14 | ReactDOM.unmountComponentAtNode(rootDOMElement); 15 | const NextApp = require('./comps/App').default; 16 | render(NextApp); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /app2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "rootDir": "src", 6 | "outDir": "build/tsc", 7 | "paths": { 8 | "src/*": ["src/*"], 9 | "@monorepo/common/*": ["../common/src/*"] 10 | } 11 | }, 12 | "include": ["src/**/*"], 13 | "references": [ 14 | { "path": "../common" } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /app2/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { getWebpackConfig } = require("../tools/webpack/config") 2 | 3 | const config = getWebpackConfig(__dirname); 4 | 5 | module.exports = config; 6 | -------------------------------------------------------------------------------- /common/.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | -------------------------------------------------------------------------------- /common/.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | // automatically import all files ending in *.story.tsx 4 | const req = require.context('../src', true, /.story.ts[x]?$/); 5 | function loadStories() { 6 | req.keys().forEach(filename => req(filename)); 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /common/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /common/.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { getStorybookWebpackConfig } = require("../../tools/webpack/storybookConfig") 2 | 3 | const config = getStorybookWebpackConfig(__dirname); 4 | 5 | module.exports = config; 6 | -------------------------------------------------------------------------------- /common/generateDTS.js: -------------------------------------------------------------------------------- 1 | // 2 | // This script is to generate each a single combined .d.ts file in the `dist/tsc` directory 3 | // Then you are able to import a submodule like this: import {Header} from '@monorepo/common/comps' 4 | // Typescript `tsc` for now doesn't generate a single d.ts like this for multiple modules 5 | // 6 | 7 | const generator = require('dts-generator').default; 8 | 9 | const OUTPUT_FILE = `dist/tsc/index.d.ts`; 10 | const name = require("./package.json").name; 11 | 12 | console.log(`Generating ${OUTPUT_FILE}`); 13 | generator({ 14 | name: name, 15 | project: `./`, 16 | out: OUTPUT_FILE, 17 | }); 18 | -------------------------------------------------------------------------------- /common/jest.config.js: -------------------------------------------------------------------------------- 1 | const { getJestConfig } = require("../tools/jest/config") 2 | 3 | const config = getJestConfig(__dirname); 4 | 5 | module.exports = config; 6 | -------------------------------------------------------------------------------- /common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@monorepo/common", 3 | "description": "Monorepo Common Library", 4 | "version": "1.0.0", 5 | "private": true, 6 | "main": "build/tsc/index.js", 7 | "typings": "build/tsc/index.d.ts", 8 | "dependencies": {}, 9 | "scripts": { 10 | "clean:build": "rimraf build && mkdirp build", 11 | "clean:dist": "rimraf dist && mkdirp dist", 12 | "compile": "tsc", 13 | "start": "tsc --watch", 14 | "test": "jest", 15 | "lint": "tslint -c ../tslint.json -p tsconfig.json src/**/*.{ts,tsx}", 16 | "lint:fix": "yarn lint --fix", 17 | "lint:style": "stylelint src/**/*.{ts,tsx}", 18 | "storybook": "start-storybook -c .storybook -p 7001", 19 | "storybook:build": "build-storybook -c .storybook -o build/storybook" 20 | }, 21 | "devDependencies": { 22 | "dts-generator": "^2.1.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /common/src/comps/Header/Header.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/react'; 2 | import React from 'react'; 3 | import Header from './Header'; 4 | 5 | storiesOf('Header', module).add('with text', () =>
Hi
); 6 | -------------------------------------------------------------------------------- /common/src/comps/Header/Header.test.tsx: -------------------------------------------------------------------------------- 1 | import 'jest-dom/extend-expect'; 2 | import 'jest-styled-components'; 3 | import React from 'react'; 4 | import { render } from 'react-testing-library'; 5 | import Header, { HeaderProps } from './Header'; 6 | 7 | const props: HeaderProps = { 8 | children: 'Test', 9 | }; 10 | 11 | test('renders', () => { 12 | const { container, getByText } = render(
); 13 | const textNode = getByText(props.children); 14 | expect(textNode).toBeVisible(); 15 | expect(container.firstChild).toHaveStyleRule('font-size', '35px'); 16 | expect(container).toMatchSnapshot(); 17 | }); 18 | -------------------------------------------------------------------------------- /common/src/comps/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | export interface HeaderProps { 5 | children: string; 6 | } 7 | 8 | const Header: React.SFC = ({ children }) => ( 9 | {children} 10 | ); 11 | 12 | const StyledHeader = styled.h1` 13 | font-size: 35px; 14 | color: #fbc121; 15 | `; 16 | 17 | export default Header; 18 | -------------------------------------------------------------------------------- /common/src/comps/Header/__snapshots__/Header.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders 1`] = ` 4 | .c0 { 5 | font-size: 35px; 6 | color: #fbc121; 7 | } 8 | 9 |
10 |

13 | Test 14 |

15 |
16 | `; 17 | -------------------------------------------------------------------------------- /common/src/comps/Header/index.ts: -------------------------------------------------------------------------------- 1 | import Header from './Header'; 2 | export default Header; 3 | -------------------------------------------------------------------------------- /common/src/comps/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Header } from './Header'; 2 | -------------------------------------------------------------------------------- /common/src/foo/foo.test.ts: -------------------------------------------------------------------------------- 1 | import { bar } from './foo'; 2 | 3 | test('bar', () => { 4 | expect(bar()).toContain('common'); 5 | }); 6 | -------------------------------------------------------------------------------- /common/src/foo/foo.ts: -------------------------------------------------------------------------------- 1 | export const bar = () => 'Hi! this is the bar function from common'; 2 | -------------------------------------------------------------------------------- /common/src/foo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './foo'; 2 | -------------------------------------------------------------------------------- /common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "rootDir": "src", 6 | "outDir": "build/tsc", 7 | "paths": { 8 | "src/*": ["src/*"] 9 | } 10 | }, 11 | "include": ["src/**/*"] 12 | } 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const { getJestConfig } = require("./tools/jest/config") 4 | 5 | const config = getJestConfig(__dirname, true); 6 | 7 | module.exports = { 8 | ...config, 9 | projects: ["common", "app1", "app2"], 10 | coverageDirectory: './build/coverage', 11 | }; 12 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.11.0", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "version": "1.0.0" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-typescript-monorepo", 3 | "description": "Testing a React+Typescript monorepo", 4 | "version": "1.0.0", 5 | "private": true, 6 | "workspaces": { 7 | "packages": [ 8 | "common", 9 | "app1", 10 | "app2" 11 | ] 12 | }, 13 | "scripts": { 14 | "start": "lerna run start --parallel", 15 | "test": "jest", 16 | "test:watch": "yarn test --watch", 17 | "test:coverage": "yarn test --coverage --ci --reporters=default --reporters=jest-junit --reporters=jest-html-reporter", 18 | "test:ci": "yarn compile && yarn test:coverage && yarn lint && yarn lint:style", 19 | "lint": "lerna run lint", 20 | "lint:fix": "lerna run lint:fix", 21 | "lint:style": "lerna run lint:style", 22 | "clean:build": "lerna run clean:build && rimraf build && mkdirp build", 23 | "clean:dist": "lerna run clean:dist", 24 | "build": "lerna run build", 25 | "build:ci": "yarn clean:dist && yarn compile && yarn build && yarn storybook:build", 26 | "compile": "tsc -b common app1 app2", 27 | "compile:watch": "yarn compile --watch", 28 | "storybook": "lerna run storybook", 29 | "storybook:build": "lerna run storybook:build" 30 | }, 31 | "dependencies": { 32 | "@storybook/addon-actions": "^4.0.0-alpha.22", 33 | "@storybook/react": "^4.0.0-alpha.22", 34 | "@types/react": "16.4.18", 35 | "@types/react-dom": "^16.0.9", 36 | "react": "^16.5.2", 37 | "react-dom": "^16.6.0", 38 | "react-redux": "^5.0.7", 39 | "redux": "^4.0.1", 40 | "redux-saga": "^0.16.2", 41 | "reselect": "^4.0.0", 42 | "styled-components": "^4.0.2" 43 | }, 44 | "devDependencies": { 45 | "@babel/cli": "^7.1.2", 46 | "@babel/core": "^7.1.2", 47 | "@babel/plugin-proposal-class-properties": "^7.1.0", 48 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 49 | "@babel/preset-env": "^7.1.0", 50 | "@babel/preset-react": "^7.0.0", 51 | "@types/jest": "^23.3.7", 52 | "@types/storybook__addon-actions": "^3.4.1", 53 | "@types/storybook__react": "^3.0.9", 54 | "@types/styled-components": "^4.0.2", 55 | "@types/webpack": "^4.4.17", 56 | "babel-loader": "^8.0.4", 57 | "babel-plugin-styled-components": "^1.8.0", 58 | "friendly-errors-webpack-plugin": "^1.7.0", 59 | "happypack": "^5.0.0", 60 | "html-webpack-plugin": "^3.2.0", 61 | "husky": "^1.1.2", 62 | "jest": "^23.6.0", 63 | "jest-dom": "^2.1.0", 64 | "jest-html-reporter": "^2.4.2", 65 | "jest-junit": "^5.2.0", 66 | "jest-styled-components": "^6.2.2", 67 | "lerna": "^3.4.3", 68 | "mkdirp": "^0.5.1", 69 | "prettier": "^1.14.3", 70 | "react-hot-loader": "^4.3.11", 71 | "react-testing-library": "^5.2.1", 72 | "redux-mock-store": "^1.5.3", 73 | "redux-saga-test-plan": "^3.7.0", 74 | "rimraf": "^2.6.2", 75 | "stylelint": "^9.6.0", 76 | "stylelint-config-recommended": "^2.1.0", 77 | "stylelint-config-styled-components": "^0.1.1", 78 | "stylelint-processor-styled-components": "^1.5.0", 79 | "ts-jest": "^23.10.4", 80 | "ts-loader": "^5.2.2", 81 | "tsconfig-paths-webpack-plugin": "^3.2.0", 82 | "tslib": "^1.9.3", 83 | "tslint": "^5.11.0", 84 | "tslint-config-airbnb": "^5.11.0", 85 | "tslint-config-prettier": "^1.15.0", 86 | "tslint-plugin-prettier": "^2.0.0", 87 | "tslint-react": "^3.6.0", 88 | "typescript": "^3.1.3", 89 | "typescript-plugin-styled-components": "^1.0.0", 90 | "uglifyjs-webpack-plugin": "^2.0.1", 91 | "webpack": "^4.22.0", 92 | "webpack-bundle-analyzer": "^3.0.3", 93 | "webpack-cli": "^3.1.2", 94 | "webpack-dev-server": "^3.1.10", 95 | "webpack-merge": "^4.1.4" 96 | }, 97 | "jest-junit": { 98 | "suiteName": "Jest Tests", 99 | "output": "build/jest/jestTests.xml" 100 | }, 101 | "jest-html-reporter": { 102 | "pageTitle": "Jest Tests", 103 | "outputPath": "build/jest/jestTests.html", 104 | "includeFailureMsg": true, 105 | "executionMode": "reporter" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tools/babel/config.js: -------------------------------------------------------------------------------- 1 | const getBabelConfig = () => { 2 | const presets = [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "browsers": ["last 2 versions"] 8 | } 9 | } 10 | ], 11 | "@babel/react", 12 | ]; 13 | const plugins = [ 14 | "babel-plugin-styled-components", 15 | "@babel/proposal-class-properties", 16 | "@babel/proposal-object-rest-spread" 17 | ]; 18 | 19 | return { 20 | presets, 21 | plugins 22 | }; 23 | } 24 | 25 | module.exports = { 26 | getBabelConfig 27 | } -------------------------------------------------------------------------------- /tools/jest/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { pathsToModuleNameMapper } = require('ts-jest/utils'); 3 | 4 | /** 5 | * generate jest config for your project 6 | * @param {string} projectDir set to __dirname 7 | */ 8 | const getJestConfig = (projectDir, isRoot) => { 9 | const getJestPath = filename => 10 | path.join(projectDir, isRoot ? './' : '../', `tools/jest/${filename}`); 11 | 12 | //generate paths mapping specified in tsconfig.json so jest can follow those lerna imports 13 | const tsConfigFile = path.join(projectDir, 'tsconfig.json'); 14 | const tsConfig = require(tsConfigFile); 15 | let tsPaths = {}; 16 | if (tsConfig.compilerOptions && tsConfig.compilerOptions.paths) { 17 | tsPaths = pathsToModuleNameMapper(tsConfig.compilerOptions.paths, { 18 | prefix: `${projectDir}/`, 19 | }); 20 | } 21 | 22 | return { 23 | globals: { 24 | 'ts-jest': { 25 | isolatedModules: true, 26 | tsConfig: tsConfigFile, 27 | }, 28 | }, 29 | transform: { 30 | '^.+\\.tsx?$': 'ts-jest', 31 | }, 32 | testMatch: ['**/*.test.{ts,tsx}'], 33 | modulePaths: [projectDir], //allow absolute imports eg import Button from 'src/comps/Button' 34 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 35 | moduleNameMapper: { 36 | '\\.(jpg|jpeg|png|gif|webp|svg)$': getJestPath('fileMock.js'), 37 | '\\.(eot|otf|ttf|woff|woff2)$': getJestPath('fileMock.js'), 38 | '\\.(mp4|webm|wav|mp3|m4a|aac|oga)$': getJestPath('fileMock.js'), 39 | '\\.(css|sass|scss)$': getJestPath('fileMock.js'), 40 | ...tsPaths, 41 | }, 42 | setupFiles: [getJestPath('jestSetup.js')], 43 | setupTestFrameworkScriptFile: getJestPath('jestFrameworkSetup.js'), 44 | }; 45 | }; 46 | 47 | module.exports = { 48 | getJestConfig, 49 | }; 50 | -------------------------------------------------------------------------------- /tools/jest/fileMock.js: -------------------------------------------------------------------------------- 1 | // This is the mock output for the src path of a file 2 | export default 'jest-mock-src'; 3 | -------------------------------------------------------------------------------- /tools/jest/jestFrameworkSetup.js: -------------------------------------------------------------------------------- 1 | // Use in jest.config.js under setupTestFrameworkScriptFile 2 | // Configures the testing framework before each test 3 | 4 | // unmount automatically after each test 5 | require('react-testing-library/cleanup-after-each'); -------------------------------------------------------------------------------- /tools/jest/jestSetup.js: -------------------------------------------------------------------------------- 1 | // Setup for use with enzyme 2 | // Patching console.error so test will fail 3 | 4 | // Use in jest.config.js under setupFiles 5 | // Configures the jest environment 6 | 7 | 8 | // Ignore console.log whilst running tests 9 | console.log = () => {}; 10 | 11 | // Throw error upon console.error to fail test 12 | // Ignore a few console.error whilst running tests 13 | const suppressedErrors = /(React\.createElement: type is invalid)/; 14 | const realConsoleError = console.error; 15 | console.error = message => { 16 | if (message.match(suppressedErrors)) { 17 | return; 18 | } 19 | const msg = `Console.Error in Test: "${message}"`; 20 | realConsoleError(msg); 21 | throw new Error(msg); 22 | }; 23 | -------------------------------------------------------------------------------- /tools/webpack/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HTMLWebpackPlugin = require('html-webpack-plugin'); 4 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 5 | const HappyPack = require('happypack'); 6 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 7 | const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin'); 8 | const merge = require('webpack-merge'); 9 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') 10 | .BundleAnalyzerPlugin; 11 | 12 | const getHappyPackPlugin = tsConfigFile => 13 | new HappyPack({ 14 | id: 'ts', 15 | threads: 2, 16 | loaders: [ 17 | { 18 | path: 'ts-loader', 19 | query: { 20 | happyPackMode: true, 21 | transpileOnly: true, 22 | configFile: tsConfigFile, 23 | experimentalFileCaching: true, 24 | getCustomTransformers: path.join( 25 | __dirname, 26 | './styledComponentsTransformer.js', 27 | ), 28 | }, 29 | }, 30 | ], 31 | }); 32 | 33 | /** generate common config then merge with webpack-merge */ 34 | const getCommonConfig = projectDir => { 35 | const tsConfigFile = path.join(projectDir, 'tsconfig.json'); 36 | 37 | const htmlPlugin = new HTMLWebpackPlugin({ 38 | template: path.join(projectDir, 'src/index.html'), 39 | filename: 'index.html', 40 | inject: true, 41 | }); 42 | 43 | const config = { 44 | context: projectDir, // to find tsconfig.json 45 | output: { 46 | path: path.join(projectDir, 'dist'), 47 | publicPath: '/', 48 | }, 49 | devServer: { 50 | hot: true, 51 | overlay: true, 52 | historyApiFallback: true, // for history html5 api 53 | quiet: true, //for friendly errors to work 54 | }, 55 | resolve: { 56 | extensions: ['.tsx', '.ts', '.js'], 57 | // ts config paths plugin enables following those lerna import links 58 | plugins: [new TsconfigPathsPlugin({ configFile: tsConfigFile })], 59 | }, 60 | module: { 61 | rules: [ 62 | { 63 | test: /\.tsx?$/, 64 | exclude: /node_modules/, 65 | loader: 'happypack/loader?id=ts', 66 | }, 67 | { 68 | test: /\.(jpe?g|png|gif|svg)$/i, 69 | loader: 'url-loader', 70 | options: { 71 | limit: 10000, 72 | name: '[name].[hash:4].[ext]', 73 | }, 74 | }, 75 | ], 76 | }, 77 | //common plugins 78 | plugins: [ 79 | getHappyPackPlugin(tsConfigFile), 80 | htmlPlugin, 81 | new FriendlyErrorsWebpackPlugin(), 82 | ], 83 | }; 84 | 85 | return config; 86 | }; 87 | 88 | const getProdConfig = projectDir => { 89 | const commonConfig = getCommonConfig(projectDir); 90 | 91 | const prodDefinePlugin = new webpack.DefinePlugin({ 92 | 'process.env.NODE_ENV': JSON.stringify('production'), 93 | }); 94 | 95 | const bundleAnalyzerPlugin = new BundleAnalyzerPlugin({ 96 | reportFilename: path.join(projectDir, 'build/bundleAnalyzer.html'), 97 | analyzerMode: 'static', 98 | openAnalyzer: false, 99 | }); 100 | 101 | 102 | const prodConfig = { 103 | mode: 'production', 104 | entry: path.join(projectDir, 'src/index.tsx'), 105 | plugins: [prodDefinePlugin, bundleAnalyzerPlugin], 106 | output: { 107 | filename: '[name].[chunkhash:4].js', 108 | }, 109 | devtool: 'source-map', 110 | optimization: { 111 | minimizer: [ 112 | new UglifyJSPlugin({ 113 | sourceMap: true, 114 | cache: true, 115 | parallel: true, 116 | }), 117 | ], 118 | splitChunks: { 119 | cacheGroups: { 120 | vendor: { 121 | test: /[\\/]node_modules[\\/]/, 122 | chunks: 'all', 123 | priority: 1, 124 | }, 125 | }, 126 | }, 127 | }, 128 | }; 129 | 130 | const config = merge(commonConfig, prodConfig); 131 | return config; 132 | }; 133 | 134 | const getDevConfig = projectDir => { 135 | const commonConfig = getCommonConfig(projectDir); 136 | 137 | const devConfig = { 138 | mode: 'development', 139 | entry: ['react-hot-loader/patch', path.join(projectDir, 'src/index.tsx')], 140 | plugins: [ 141 | new webpack.HotModuleReplacementPlugin(), 142 | ], 143 | output: { 144 | filename: 'bundle.js', 145 | }, 146 | devtool: 'eval-source-map', 147 | }; 148 | 149 | const config = merge(commonConfig, devConfig); 150 | return config; 151 | }; 152 | 153 | /** 154 | * pass in --env in package.json: 155 | * `webpack --env production` 156 | * or 157 | * `webpack-dev-server --env development --open --port 3001` 158 | */ 159 | const getWebpackConfig = projectDir => env => { 160 | if (env === 'production') { 161 | return getProdConfig(projectDir); 162 | } 163 | return getDevConfig(projectDir); 164 | }; 165 | 166 | module.exports = { 167 | getHappyPackPlugin, 168 | getWebpackConfig, 169 | }; 170 | -------------------------------------------------------------------------------- /tools/webpack/storybookConfig.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 3 | const { getHappyPackPlugin } = require('./config'); 4 | 5 | /** 6 | * add typescript to storybook webpack 7 | * 8 | * Similar config to the main webpack one 9 | */ 10 | const getStorybookWebpackConfig = storybookDir => baseConfig => { 11 | const tsConfigFile = path.join(storybookDir, '../tsconfig.json'); 12 | 13 | baseConfig.module.rules.push({ 14 | test: /\.(ts|tsx)$/, 15 | exclude: /node_modules/, 16 | loader: 'happypack/loader?id=ts', 17 | }); 18 | 19 | baseConfig.resolve = { 20 | extensions: ['.tsx', '.ts', '.js'], 21 | // ts config paths plugin enables following those lerna import links 22 | plugins: [new TsconfigPathsPlugin({ configFile: tsConfigFile })], 23 | }; 24 | 25 | baseConfig.plugins.push(getHappyPackPlugin(tsConfigFile)); 26 | 27 | return baseConfig; 28 | }; 29 | 30 | module.exports = { 31 | getStorybookWebpackConfig, 32 | }; 33 | -------------------------------------------------------------------------------- /tools/webpack/styledComponentsTransformer.js: -------------------------------------------------------------------------------- 1 | // since using faster happypack with ts-loader, need this custom transformer to 2 | // make debugging styled components nicer. 3 | // once moved to babel typescript can remove this and go back to the normal babel-styled-components plugin 4 | const createStyledComponentsTransformer = require('typescript-plugin-styled-components').default; 5 | 6 | const styledComponentsTransformer = createStyledComponentsTransformer(); 7 | 8 | // create getCustomTransformer function for ts-loader 9 | const getCustomTransformers = () => ({ before: [styledComponentsTransformer] }); 10 | 11 | 12 | module.exports = getCustomTransformers; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "lib": ["dom", "es6", "es7"], 5 | "target": "es5", 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "jsx": "react", 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "noImplicitReturns": true, 12 | "noImplicitAny": true, 13 | "noImplicitThis": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noEmitOnError": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "strictNullChecks": true, 19 | "experimentalDecorators": true, 20 | "preserveConstEnums": true, 21 | "allowJs": false, 22 | "strict": true, 23 | "alwaysStrict": true, 24 | "pretty": true, 25 | "sourceMap": true, 26 | "declaration": true, 27 | "declarationMap": true, 28 | "noEmit": false, 29 | "noEmitHelpers": true, 30 | "importHelpers": true 31 | }, 32 | "exclude": ["**/dist", "**/build", "node_modules"], 33 | "compileOnSave": true 34 | } 35 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:latest", 4 | "tslint-config-airbnb", 5 | "tslint-react", 6 | "tslint-config-prettier" 7 | ], 8 | "rulesDirectory": ["tslint-plugin-prettier"], 9 | "rules": { 10 | "prettier": true, 11 | "ordered-imports": true, 12 | "object-literal-sort-keys": false, 13 | "interface-name": false, 14 | "no-empty-interface": false, 15 | "variable-name": false, 16 | "jsx-no-lambda": false, 17 | "jsx-boolean-value": false, 18 | "import-name": false, 19 | "no-implicit-dependencies": false, 20 | "member-access": false, 21 | "object-shorthand-properties-first": false, 22 | "interface-over-type-literal": false, 23 | "prefer-array-literal": false, 24 | "no-console": false, 25 | "no-submodule-imports": false, 26 | "no-empty": [true, "allow-empty-catch"], 27 | "no-var-requires": false, 28 | "no-angle-bracket-type-assertion": false, 29 | "no-increment-decrement": false, 30 | "jsdoc-format": false, 31 | "max-classes-per-file": false 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | // 2 | // Wallaby config reads tsconfig.json and jest.config.js out of the box 3 | // 4 | 5 | module.exports = function(wallaby) { 6 | return { 7 | files: [ 8 | '**/src/**/*.{ts,tsx}', 9 | { pattern: '**/*.json', instrument: false }, 10 | { pattern: '**/*.js', instrument: false }, 11 | '!**/src/**/*.test.{ts,tsx}', 12 | '!**/src/**/*.story.tsx', 13 | '!**/node_modules/**/*', 14 | '!**/dist/**/*', 15 | '!**/build/**/*', 16 | ], 17 | tests: ['**/src/**/*.test.{ts,tsx}', '!**/node_modules/**/*'], 18 | env: { type: 'node', runner: 'node' }, 19 | testFramework: 'jest', 20 | 21 | // wallaby doesnt support jest multi-projects: https://github.com/wallabyjs/public/issues/1856 22 | // therefore re-specify the tsconfig paths mappings as don't have paths in the root tsconfig.json 23 | // also to fix the "src/" tsconfig path alias appearing in all the projects, tell wallaby about multiple project roots to search 24 | setup: () => { 25 | const jestConfig = require('./jest.config'); 26 | jestConfig.moduleNameMapper = { 27 | ...jestConfig.moduleNameMapper, 28 | '^@monorepo/common/(.*)$': '/common/src/$1', 29 | }; 30 | jestConfig.modulePaths = ['/common', '/app1', '/app2'] 31 | wallaby.testFramework.configure(jestConfig); 32 | }, 33 | 34 | reportConsoleErrorAsError: true, 35 | lowCoverageThreshold: 80, 36 | debug: true, 37 | }; 38 | }; 39 | --------------------------------------------------------------------------------