├── .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', () => );
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 |
--------------------------------------------------------------------------------