├── .eslintrc.js
├── .gitignore
├── .prettierrc.js
├── LICENSE
├── README.md
├── package.json
├── scripts
├── build-package.js
├── start-app-dev-server.js
└── utils
│ ├── compile.js
│ ├── get-package-alias.js
│ ├── get-package-path.js
│ ├── get-packages-data.js
│ └── get-src-map.js
├── src
├── apps
│ └── hello-world
│ │ ├── package.json
│ │ ├── src
│ │ ├── App.jsx
│ │ └── index.jsx
│ │ └── webpack.config.js
└── packages
│ ├── typography
│ ├── package.json
│ ├── src
│ │ ├── Text
│ │ │ ├── Text.jsx
│ │ │ ├── Text.story.jsx
│ │ │ └── Text.styles.less
│ │ └── index.js
│ └── webpack.config.js
│ └── ui
│ ├── package.json
│ ├── src
│ ├── Button
│ │ ├── Button.jsx
│ │ ├── Button.story.jsx
│ │ └── Button.styles.less
│ └── index.js
│ └── webpack.config.js
├── storybook
├── main.js
└── start.js
├── webpack
├── .babelrc.js
├── get-app-config.js
├── get-base-paths.js
├── get-package-config.js
└── loaders.js
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const getSrcMap = require('./scripts/utils/get-src-map');
2 |
3 | module.exports = {
4 | parser: 'babel-eslint',
5 | extends: ['airbnb'],
6 | env: {
7 | browser: true,
8 | node: true,
9 | },
10 |
11 | rules: {
12 | // some props require dangle
13 | 'no-underscore-dangle': 'off',
14 |
15 | // sometimes it is better
16 | 'no-nested-ternary': 'off',
17 |
18 | // controlled with prettier
19 | 'arrow-parens': 'off',
20 | 'function-paren-newline': 'off',
21 |
22 | // disabled for condition && someFunc()
23 | 'no-unused-expressions': 'off',
24 |
25 | // backend developers like _, no need to transform data all the time
26 | camelcase: 'off',
27 |
28 | // just the formatting issues, prettier does all the job
29 | 'operator-linebreak': 'off',
30 | 'object-curly-newline': 'off',
31 | 'implicit-arrow-linebreak': 'off',
32 | 'spaced-comment': 'off',
33 | 'comma-dangle': [
34 | 'error',
35 | {
36 | arrays: 'always-multiline',
37 | objects: 'always-multiline',
38 | imports: 'always-multiline',
39 | exports: 'always-multiline',
40 | functions: 'ignore',
41 | },
42 | ],
43 |
44 | // increase max line length to 100
45 | 'max-len': [
46 | 'error',
47 | 100,
48 | {
49 | ignoreTrailingComments: true,
50 | ignoreComments: true,
51 | ignoreUrls: true,
52 | ignoreStrings: true,
53 | ignoreRegExpLiterals: true,
54 | ignoreTemplateLiterals: true,
55 | },
56 | ],
57 |
58 | // these rules are just bullshit
59 | 'import/prefer-default-export': 'off',
60 | 'react/no-danger': 'off',
61 | 'react/state-in-constructor': 'off',
62 | 'react/jsx-props-no-spreading': 'off',
63 | 'react/require-default-props': 'off',
64 | 'react/jsx-one-expression-per-line': 'off',
65 | 'react/destructuring-assignment': 'off',
66 | 'react/sort-comp': 'off',
67 | 'react/forbid-prop-types': 'off',
68 | 'react/jsx-no-bind': 'off',
69 | 'jsx-a11y/control-has-associated-label': 'off',
70 |
71 | // sometimes there is no alternative
72 | 'react/no-array-index-key': 'off',
73 |
74 | // disable dev dependencies for storybook
75 | 'import/no-extraneous-dependencies': [
76 | 'warn',
77 | {
78 | devDependencies: [
79 | '**/*.story.jsx',
80 | '**/*.test.jsx',
81 | '**/storybook/*.js',
82 | '**/webpack.config.js',
83 | '**/*.webpack.config.js',
84 | 'webpack/*',
85 | 'scripts/**',
86 | ],
87 | },
88 | ],
89 |
90 | // rules are broken and provide falsy mistakes
91 | 'jsx-a11y/label-has-for': 'off',
92 | 'jsx-a11y/anchor-is-valid': 'off',
93 | 'react/no-typos': 'off', // https://github.com/yannickcr/eslint-plugin-react/issues/1389
94 |
95 | // it does not spoil anything if used wisely
96 | 'jsx-a11y/no-autofocus': 'off',
97 | },
98 |
99 | settings: {
100 | 'import/resolver': {
101 | node: {},
102 | webpack: {},
103 | alias: {
104 | map: Object.entries(getSrcMap()),
105 | extensions: ['.ts', '.js', '.jsx', '.json'],
106 | },
107 | },
108 | },
109 | };
110 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 100,
3 | singleQuote: true,
4 | trailingComma: 'es5',
5 | };
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Vitaly Rtishchev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Monorepo Starter
2 |
3 | This project includes bare minimum configuration that you can use to work with react in monorepo. It includes workflow to work both with packages and apps (without ssr). All packages included by default are used for testing and showcasing, you can remove them safely after you finish playing around.
4 |
5 | ## Scripts
6 |
7 | - **npm start** script:
8 | - `npm start @application/name` – start any application
9 | - `npm start @application/name -- --port 3000` – start application on port 3000
10 | - **Storybook** script:
11 | - `npm run storybook` – start storybook with all packages and apps
12 | - `npm run storybook @package/name` – start storybook with specific package
13 | - `npm run storybook @package/first @package/second` – start storybook with specified list of packages
14 | - **Build** script:
15 | - `npm run build @package/name` – build single package
16 | - `npm run build @package/first @package/second` – build list of packages
17 | - **Syncpack** scripts:
18 | - `npm run syncpack:list` – list all used dependencies
19 | - `npm run syncpack:format` – format all package.json files
20 | - `npm run syncpack:mismatch` – list dependencies with different versions (e.g. react@16 and react@17 used in different packages) – can be used in precommit hook to check dependencies sync
21 | - `npm run syncpack:mismatch:fix` – update all dependencies listed by `npm run syncpack:mismatch` to highest used version
22 |
23 | ## Technical details and concepts
24 |
25 | Since this is monorepo all building and starting scripts run from root and share the same webpack configuration. Packages and apps can be referred only by name defined in package.json.
26 |
27 | ### Workspcases
28 |
29 | To work with monorepo you are required to use yarn, since it natively supports workspaces. Workspaces are defined in root package.json:
30 |
31 | ```js
32 | // ...
33 | "workspaces": [
34 | "src/packages/*",
35 | "src/apps/*"
36 | ],
37 | // ...
38 | ```
39 |
40 | ### Packages scope
41 |
42 | It is a good idea to use the same scope (`@scope/`) for each package in monorepo to easily identify packages. Although it is not required and packages can use names that include different scopes or no scope at all.
43 |
44 | ### Aliases
45 |
46 | Any package can refer to other package using webpack aliases that are automatically generated. Packages and apps aliases resolve src directory of corresponding package.
47 |
48 | ```js
49 | // example with included @monorepo/hello-world app
50 | import React from 'react';
51 | import { Text } from '@monorepo/typography';
52 | import Button from '@monorepo/ui/Button/Button';
53 |
54 | export default function App() {
55 | return (
56 |
57 | Welcome to monorepo starter
58 |
59 |
60 | );
61 | }
62 | ```
63 |
64 | ### Managing dependencies
65 |
66 | There are two approaches for managing dependencies in monorepo. You can combine both to reach desired behaviour.
67 |
68 | #### Shared dependencies
69 |
70 | - On **core** level: install all globally used dependencies: react, prop-types, classnames, etc. (All dependencies that are expected to use in each package). Install these dependencies as peers on **package** level.
71 | - On **package** level: install package specific dependencies that are used only in this package.
72 |
73 | This approach will allow you to quickly update shared dependencies without need to get through each package and updating versions.
74 |
75 | #### Packages dependencies
76 |
77 | - On **core** level do not install any package related dependencies.
78 | - On **package** level install all package dependencies.
79 |
80 | This approach is pretty heavy and hard to use but will allow you to fully manage versions of dependencies used in each package.
81 |
82 | ### Have questions?
83 |
84 | Let's talk, I'm always open for discussion, please create an issue with your question.
85 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-monorepo-starter",
3 | "version": "1.0.0",
4 | "author": "Vitaly Rtishchev ",
5 | "dependencies": {
6 | "classnames": "^2.2.6",
7 | "open-color": "^1.7.0",
8 | "prop-types": "^15.7.2",
9 | "react": "^16.13.1",
10 | "react-dom": "^16.13.1",
11 | "react-hot-loader": "^4.12.21",
12 | "react-router-dom": "^5.2.0",
13 | "uuid": "^8.3.0"
14 | },
15 | "devDependencies": {
16 | "@babel/core": "^7.11.4",
17 | "@babel/plugin-proposal-class-properties": "^7.10.4",
18 | "@babel/plugin-proposal-export-default-from": "^7.10.4",
19 | "@babel/plugin-proposal-export-namespace-from": "^7.10.4",
20 | "@babel/preset-env": "^7.11.0",
21 | "@babel/preset-react": "^7.10.4",
22 | "@storybook/react": "^6.0.16",
23 | "autoprefixer": "^9.8.6",
24 | "babel-eslint": "^10.1.0",
25 | "babel-loader": "^8.1.0",
26 | "chalk": "^4.1.0",
27 | "cross-env": "^7.0.2",
28 | "css-loader": "^4.2.1",
29 | "eslint": "^7.7.0",
30 | "eslint-config-airbnb": "^18.2.0",
31 | "eslint-import-resolver-alias": "^1.1.2",
32 | "eslint-import-resolver-webpack": "^0.12.2",
33 | "eslint-plugin-import": "^2.22.0",
34 | "eslint-plugin-jsx-a11y": "^6.3.1",
35 | "eslint-plugin-react": "^7.20.6",
36 | "eslint-plugin-react-hooks": "^4.1.0",
37 | "fast-glob": "^3.2.4",
38 | "file-loader": "^6.0.0",
39 | "fs-extra": "^9.0.1",
40 | "get-port": "^5.1.1",
41 | "get-port-sync": "^1.0.1",
42 | "html-webpack-plugin": "^4.3.0",
43 | "less-loader": "^6.2.0",
44 | "mini-css-extract-plugin": "^0.10.0",
45 | "open-browser-webpack-plugin": "^0.0.5",
46 | "optimize-css-assets-webpack-plugin": "^5.0.3",
47 | "postcss-loader": "^3.0.0",
48 | "rimraf": "^3.0.2",
49 | "style-loader": "^1.2.1",
50 | "syncpack": "^5.6.10",
51 | "terser-webpack-plugin": "^4.1.0",
52 | "webpack": "^4.44.1",
53 | "webpack-cli": "^3.3.12",
54 | "webpack-dev-server": "^3.11.0",
55 | "yargs": "^15.4.1"
56 | },
57 | "license": "MIT",
58 | "main": "index.js",
59 | "private": true,
60 | "repository": "https://github.com/rtivital/react-monorepo-starter",
61 | "scripts": {
62 | "build": "node scripts/build-package",
63 | "start": "node scripts/start-app-dev-server",
64 | "storybook": "node storybook/start",
65 | "syncpack:format": "syncpack format",
66 | "syncpack:list": "syncpack list --prod --dev --peer",
67 | "syncpack:mismatch": "syncpack list-mismatches",
68 | "syncpack:mismatch:fix": "syncpack fix-mismatches"
69 | },
70 | "workspaces": [
71 | "src/packages/*",
72 | "src/apps/*"
73 | ]
74 | }
75 |
--------------------------------------------------------------------------------
/scripts/build-package.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const chalk = require('chalk');
4 | const { argv } = require('yargs');
5 | const getPackagePath = require('./utils/get-package-path');
6 | const compile = require('./utils/compile');
7 |
8 | process.env.NODE_ENV = 'production';
9 |
10 | const packages = argv._;
11 |
12 | const helpMessage = `npm run build script usage: ${chalk.cyan('npm run build @package/name')}\n`;
13 |
14 | function throwError(error) {
15 | process.stdout.write(`${error}\n`);
16 | process.stdout.write(helpMessage);
17 | process.exit(1);
18 | }
19 |
20 | if (packages.length === 0) {
21 | throwError(chalk.red`Packages to build are not defined`);
22 | }
23 |
24 | const paths = [];
25 |
26 | packages.forEach(item => {
27 | const packagePath = getPackagePath(item);
28 | if (!packagePath || !fs.existsSync(path.join(packagePath, 'webpack.config.js'))) {
29 | process.stdout.write(chalk.yellow(`Warning: cannot locate package: ${item}, skipping`));
30 | } else {
31 | paths.push({ path: packagePath, name: item });
32 | }
33 | });
34 |
35 | if (paths.length === 0) {
36 | throwError(chalk.red`Packages to build cannot be located`);
37 | }
38 |
39 | process.stdout.write(`Building packages ${chalk.cyan(paths.map(p => p.name).join(', '))}\n`);
40 |
41 | Promise.all(
42 | paths.map(item => {
43 | /* eslint-disable-next-line global-require, import/no-dynamic-require */
44 | const config = require(`${item.path}/webpack.config.js`);
45 |
46 | return compile(config)
47 | .then(stats => {
48 | process.stdout.write(
49 | `${chalk.green`✔`} Package ${chalk.cyan(item.name)} was built in ${chalk.green(
50 | `${(stats.toJson().time / 1000).toFixed(2).toString()}s`
51 | )}\n`
52 | );
53 | })
54 | .catch((err, stats) => {
55 | if (err) {
56 | process.stdout.write(`${chalk.red`✗`} Package ${chalk.cyan(item.name)} build crashed:\n`);
57 | process.stdout.write(`${chalk.red(err)}\n`);
58 | }
59 |
60 | process.stdout.write(`${chalk.red`✗`} Package ${chalk.cyan(item.name)} build crashed:\n`);
61 | process.stdout.write(`${chalk.red(stats.toString('minimal'))}\n`);
62 | process.exit(1);
63 | });
64 | })
65 | ).then(() => {
66 | process.exit(0);
67 | });
68 |
--------------------------------------------------------------------------------
/scripts/start-app-dev-server.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const { argv } = require('yargs');
4 | const chalk = require('chalk');
5 | const WebpackDevServer = require('webpack-dev-server');
6 | const webpack = require('webpack');
7 | const getPackagePath = require('./utils/get-package-path');
8 |
9 | const packageName = argv._[0];
10 |
11 | if (typeof argv.port === 'number') {
12 | process.env.PORT = argv.port;
13 | }
14 |
15 | const helpMessage = `npm start script usage: ${chalk.cyan('npm start @app/name')}\n`;
16 |
17 | function throwError(error) {
18 | process.stdout.write(`${error}\n`);
19 | process.stdout.write(helpMessage);
20 | process.exit(1);
21 | }
22 |
23 | function throwParsingError(error) {
24 | throwError(`${chalk.red`App`} ${packageName} ${chalk.red`cannot be started: ${error}`}`);
25 | }
26 |
27 | if (!packageName) {
28 | throwError(chalk.red`Error: app name was not specified`);
29 | }
30 |
31 | const packagePath = getPackagePath(packageName);
32 |
33 | if (!packagePath) {
34 | throwParsingError('app with this name does not exist');
35 | }
36 |
37 | if (!fs.existsSync(path.join(packagePath, 'webpack.config.js'))) {
38 | throwParsingError('it does not have webpack.config.js');
39 | }
40 |
41 | (async () => {
42 | process.env.NODE_ENV = 'development';
43 |
44 | /* eslint-disable-next-line import/no-dynamic-require, global-require */
45 | const config = await require(`${packagePath}/webpack.config.js`);
46 |
47 | if (!config.devServer) {
48 | throwParsingError('app does not have dev server configuration in webpack config');
49 | }
50 |
51 | new WebpackDevServer(webpack(config), { historyApiFallback: true }).listen(config.devServer.port, 'localhost', error => {
52 | if (error) {
53 | /* eslint-disable-next-line no-console */
54 | console.error(error);
55 | }
56 | });
57 | })();
58 |
--------------------------------------------------------------------------------
/scripts/utils/compile.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 |
3 | module.exports = function compile(config) {
4 | const compiler = webpack(config);
5 |
6 | return new Promise((resolve, reject) => {
7 | compiler.run((err, stats) => {
8 | if (err) {
9 | reject(err, new Error('Invalid webpack configuration'));
10 | }
11 |
12 | if (stats.hasErrors()) {
13 | reject(stats);
14 | }
15 |
16 | resolve(stats);
17 | });
18 | });
19 | };
20 |
--------------------------------------------------------------------------------
/scripts/utils/get-package-alias.js:
--------------------------------------------------------------------------------
1 | const getSrcMap = require('./get-src-map');
2 |
3 | module.exports = function getPackageAlias(packageName) {
4 | const packagesData = getSrcMap();
5 | delete packagesData[packageName];
6 | return packagesData;
7 | };
8 |
--------------------------------------------------------------------------------
/scripts/utils/get-package-path.js:
--------------------------------------------------------------------------------
1 | const getPackagesData = require('./get-packages-data');
2 |
3 | module.exports = function getPackagePath(name) {
4 | return getPackagesData()[name];
5 | };
6 |
--------------------------------------------------------------------------------
/scripts/utils/get-packages-data.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs-extra');
3 | const glob = require('fast-glob');
4 |
5 | const REPO_ROOT = path.join(__dirname, '../../');
6 | const CORE_PACKAGE_JSON = fs.readJsonSync(path.join(REPO_ROOT, './package.json'));
7 |
8 | module.exports = function getPackagesData() {
9 | const packages = glob.sync(
10 | CORE_PACKAGE_JSON.workspaces.map(workspace =>
11 | path.join(__dirname, `../../${workspace}/package.json`)
12 | )
13 | );
14 |
15 | return packages.reduce((acc, item) => {
16 | const packageRoot = item.replace('/package.json', '');
17 | const { name } = fs.readJsonSync(path.join(packageRoot, 'package.json'));
18 | acc[name] = packageRoot;
19 | return acc;
20 | }, {});
21 | };
22 |
--------------------------------------------------------------------------------
/scripts/utils/get-src-map.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const getPackagesData = require('./get-packages-data');
3 |
4 | module.exports = function getSrcMap() {
5 | const packagesData = getPackagesData();
6 |
7 | return Object.keys(packagesData).reduce((acc, name) => {
8 | acc[name] = path.join(packagesData[name], './src');
9 | return acc;
10 | }, {});
11 | };
12 |
--------------------------------------------------------------------------------
/src/apps/hello-world/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@monorepo/hello-world",
3 | "description": "Test application",
4 | "version": "1.0.0",
5 | "author": "Vitaly Rtishchev ",
6 | "dependencies": {},
7 | "devDependencies": {},
8 | "license": "MIT",
9 | "main": "dist/lib.js",
10 | "peerDependencies": {
11 | "classnames": "^2.2.6",
12 | "prop-types": "^15.7.2",
13 | "react": "^16.13.1",
14 | "react-dom": "^16.13.1"
15 | },
16 | "scripts": {}
17 | }
18 |
--------------------------------------------------------------------------------
/src/apps/hello-world/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text } from '@monorepo/typography';
3 | import Button from '@monorepo/ui/Button/Button';
4 |
5 | export default function App() {
6 | return (
7 |
8 | Welcome to monorepo starter
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/apps/hello-world/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import App from './App';
4 |
5 | render(, document.getElementById('app'));
6 |
--------------------------------------------------------------------------------
/src/apps/hello-world/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const getAppConfig = require('../../../webpack/get-app-config');
3 |
4 | module.exports = getAppConfig({
5 | base: path.join(__dirname, './'),
6 | mode: process.env.NODE_ENV || 'production',
7 | });
8 |
--------------------------------------------------------------------------------
/src/packages/typography/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@monorepo/typography",
3 | "description": "Typography components",
4 | "version": "1.0.0",
5 | "author": "Vitaly Rtishchev ",
6 | "dependencies": {},
7 | "devDependencies": {},
8 | "license": "MIT",
9 | "main": "dist/lib.js",
10 | "peerDependencies": {
11 | "classnames": "^2.2.6",
12 | "prop-types": "^15.7.2",
13 | "react": "^16.13.1"
14 | },
15 | "scripts": {}
16 | }
17 |
--------------------------------------------------------------------------------
/src/packages/typography/src/Text/Text.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import classes from './Text.styles.less';
4 |
5 | export default function Text({ children, ...others }) {
6 | return (
7 |
8 | {children}
9 |
10 | );
11 | }
12 |
13 | Text.propTypes = {
14 | children: PropTypes.node,
15 | };
16 |
--------------------------------------------------------------------------------
/src/packages/typography/src/Text/Text.story.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Text from './Text';
3 |
4 | export default {
5 | title: 'Text',
6 | component: Text,
7 | };
8 |
9 | const Template = args => ;
10 |
11 | export const GeneralUsage = Template.bind({});
12 |
13 | GeneralUsage.args = {
14 | children: 'General usage',
15 | };
16 |
--------------------------------------------------------------------------------
/src/packages/typography/src/Text/Text.styles.less:
--------------------------------------------------------------------------------
1 | .text {
2 | color: inherit;
3 | font-family: Helvetica, sans-serif;
4 | -webkit-font-smoothing: antialiased;
5 | }
6 |
--------------------------------------------------------------------------------
/src/packages/typography/src/index.js:
--------------------------------------------------------------------------------
1 | export Text from './Text/Text';
2 |
--------------------------------------------------------------------------------
/src/packages/typography/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const getPackageConfig = require('../../../webpack/get-package-config');
3 |
4 | module.exports = getPackageConfig({ base: path.join(__dirname, './') });
5 |
--------------------------------------------------------------------------------
/src/packages/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@monorepo/ui",
3 | "description": "UI react components to build internal tools",
4 | "version": "1.0.0",
5 | "author": "Vitaly Rtishchev ",
6 | "dependencies": {},
7 | "devDependencies": {},
8 | "license": "MIT",
9 | "main": "dist/lib.js",
10 | "peerDependencies": {
11 | "classnames": "^2.2.6",
12 | "prop-types": "^15.7.2",
13 | "react": "^16.13.1"
14 | },
15 | "scripts": {}
16 | }
17 |
--------------------------------------------------------------------------------
/src/packages/ui/src/Button/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Text } from '@monorepo/typography';
4 | import classes from './Button.styles.less';
5 |
6 | export default function Button({ children, ...others }) {
7 | return (
8 |
11 | );
12 | }
13 |
14 | Button.propTypes = {
15 | children: PropTypes.node,
16 | };
17 |
--------------------------------------------------------------------------------
/src/packages/ui/src/Button/Button.story.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Button from './Button';
3 |
4 | export default {
5 | title: 'Button',
6 | component: Button,
7 | };
8 |
9 | const Template = args => ;
10 |
11 | export const GeneralUsage = Template.bind({});
12 |
13 | GeneralUsage.args = {
14 | children: 'General usage',
15 | };
16 |
--------------------------------------------------------------------------------
/src/packages/ui/src/Button/Button.styles.less:
--------------------------------------------------------------------------------
1 | .button {
2 | background-color: @oc-blue-6;
3 | padding-top: 0;
4 | padding-bottom: 0;
5 | height: 36px;
6 | line-height: 36px;
7 | border: 0;
8 | border-radius: 4px;
9 | padding-left: 20px;
10 | padding-right: 20px;
11 | font-weight: bold;
12 | letter-spacing: 0.5px;
13 | text-transform: uppercase;
14 | text-shadow: 1px 1px 0 @oc-blue-9;
15 | border-bottom: 3px solid @oc-blue-9;
16 | font-size: 12px;
17 | color: @oc-white;
18 | transition: box-shadow 150ms ease, background-color 150ms ease;
19 | cursor: pointer;
20 |
21 | &:focus {
22 | outline: 0;
23 | box-shadow: 0 0 0 3px @oc-blue-1;
24 | }
25 |
26 | &:hover {
27 | background-color: @oc-blue-7;
28 | }
29 |
30 | &:active {
31 | transform: translateY(1px);
32 | border-bottom: 0;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/packages/ui/src/index.js:
--------------------------------------------------------------------------------
1 | export Button from './Button/Button';
2 |
--------------------------------------------------------------------------------
/src/packages/ui/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const getPackageConfig = require('../../../webpack/get-package-config');
3 |
4 | module.exports = getPackageConfig({ base: path.join(__dirname, './') });
5 |
--------------------------------------------------------------------------------
/storybook/main.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const chalk = require('chalk');
3 | const { argv } = require('yargs');
4 | const loaders = require('../webpack/loaders');
5 | const getPackagePath = require('../scripts/utils/get-package-path');
6 | const getSrcMap = require('../scripts/utils/get-src-map');
7 |
8 | const DEFAULT_STORIES = ['../src/**/*.story.@(jsx|mdx)'];
9 | const packages = argv._;
10 | let stories = DEFAULT_STORIES;
11 |
12 | if (packages.length !== 0) {
13 | stories = [];
14 |
15 | packages.forEach(packageName => {
16 | const packagePath = getPackagePath(packageName);
17 | if (packagePath) {
18 | stories.push(path.join(packagePath, 'src/**/*.story.@(jsx|mdx)'));
19 | } else {
20 | process.stdout.write(chalk.yellow(`Warning: Unable to resolve ${packageName}, skipping\n`));
21 | }
22 | });
23 | }
24 |
25 | if (stories.length === 0) {
26 | process.stdout.write(
27 | chalk.yellow('Warning: None of the defined packages can be found, loading default set\n\n')
28 | );
29 | stories = DEFAULT_STORIES;
30 | }
31 |
32 | module.exports = {
33 | stories,
34 | webpackFinal: (config, { configType }) => {
35 | config.module.rules.push(loaders.less({ publicPath: '/', mode: configType.toLowerCase() }));
36 |
37 | // eslint-disable-next-line no-param-reassign
38 | config.resolve.alias = {
39 | ...(config.resolve.alias || null),
40 | ...getSrcMap(),
41 | };
42 |
43 | return config;
44 | },
45 | };
46 |
--------------------------------------------------------------------------------
/storybook/start.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const storybook = require('@storybook/react/standalone');
3 | const getPort = require('get-port');
4 |
5 | getPort({ port: getPort.makeRange(8000, 8100) }).then(port => {
6 | storybook({
7 | port,
8 | mode: 'dev',
9 | configDir: path.join(__dirname, './'),
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/webpack/.babelrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['@babel/preset-react', ['@babel/preset-env', { modules: false }]],
3 |
4 | plugins: [
5 | '@babel/plugin-proposal-export-namespace-from',
6 | '@babel/plugin-proposal-export-default-from',
7 | '@babel/plugin-proposal-class-properties',
8 | ],
9 | };
10 |
--------------------------------------------------------------------------------
/webpack/get-app-config.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra');
2 | const path = require('path');
3 | const webpack = require('webpack');
4 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
5 | const TerserJSPlugin = require('terser-webpack-plugin');
6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
7 | const OpenBrowserPlugin = require('open-browser-webpack-plugin');
8 | const HtmlWebpackPlugin = require('html-webpack-plugin');
9 | const getPort = require('get-port-sync');
10 | const loaders = require('./loaders');
11 | const getPackageAlias = require('../scripts/utils/get-package-alias');
12 | const getBasePaths = require('./get-base-paths');
13 |
14 | module.exports = function getPackageConfig({
15 | base,
16 | publicPath = '/',
17 | mode = 'production',
18 | port: settingsPort,
19 | } = {}) {
20 | const { name } = fs.readJsonSync(path.join(base, './package.json'));
21 | const { entry, output } = getBasePaths(base);
22 | const port = process.env.PORT || settingsPort || getPort();
23 |
24 | return {
25 | mode,
26 |
27 | devtool: mode === 'production' ? false : 'eval',
28 |
29 | entry:
30 | mode === 'production'
31 | ? entry
32 | : [
33 | `webpack-dev-server/client?http://localhost:${port}`,
34 | 'webpack/hot/only-dev-server',
35 | entry,
36 | ],
37 |
38 | output: {
39 | path: output,
40 | filename: '[hash].bundle.js',
41 | publicPath,
42 | },
43 |
44 | devServer: {
45 | port,
46 | compress: true,
47 | contentBase: output,
48 | publicPath,
49 | stats: { colors: true },
50 | hot: true,
51 | historyApiFallback: true,
52 | },
53 |
54 | optimization: {
55 | minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
56 | },
57 |
58 | resolve: {
59 | extensions: ['.js', '.jsx'],
60 | alias: {
61 | ...getPackageAlias(name),
62 | },
63 | },
64 |
65 | module: {
66 | rules: [loaders.babel(), loaders.hot(), loaders.less({ mode, publicPath }), loaders.file()],
67 | },
68 |
69 | plugins: [
70 | new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(mode || 'development') }),
71 | new HtmlWebpackPlugin({
72 | templateContent: ({ htmlWebpackPlugin }) => `
73 |
74 |
75 |
76 | ${htmlWebpackPlugin.tags.headTags}
77 |
78 |
79 |
80 | Application
81 |
82 |
83 |
86 |
87 |
88 | ${htmlWebpackPlugin.tags.bodyTags}
89 |
90 |
91 | `,
92 | }),
93 | ...(mode !== 'production'
94 | ? [
95 | new webpack.HotModuleReplacementPlugin(),
96 | new OpenBrowserPlugin({ url: `http://localhost:${port}` }),
97 | ]
98 | : [new MiniCssExtractPlugin()]),
99 | ],
100 | };
101 | };
102 |
--------------------------------------------------------------------------------
/webpack/get-base-paths.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = function getBasePaths(basePath) {
4 | return {
5 | entry: path.join(basePath, './src/index'),
6 | output: path.join(basePath, './dist'),
7 | };
8 | };
9 |
--------------------------------------------------------------------------------
/webpack/get-package-config.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs-extra');
2 | const path = require('path');
3 | const webpack = require('webpack');
4 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
5 | const TerserJSPlugin = require('terser-webpack-plugin');
6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
7 | const loaders = require('./loaders');
8 | const getPackageAlias = require('../scripts/utils/get-package-alias');
9 |
10 | module.exports = function getPackageConfig({ base, publicPath = '/' } = {}) {
11 | const { name } = fs.readJsonSync(path.join(base, './package.json'));
12 |
13 | return {
14 | mode: 'production',
15 | devtool: false,
16 | entry: path.join(base, './src/index'),
17 |
18 | optimization: {
19 | minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
20 | },
21 |
22 | resolve: {
23 | extensions: ['.js', '.jsx'],
24 | alias: {
25 | ...getPackageAlias(name),
26 | },
27 | },
28 |
29 | module: {
30 | rules: [loaders.babel(), loaders.less({ mode: 'production', publicPath }), loaders.file()],
31 | },
32 |
33 | plugins: [
34 | new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }),
35 | new MiniCssExtractPlugin(),
36 | ],
37 | };
38 | };
39 |
--------------------------------------------------------------------------------
/webpack/loaders.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
3 | const autoprefixer = require('autoprefixer');
4 | const babelrc = require('./.babelrc');
5 |
6 | const babel = () => ({
7 | test: /\.(js|jsx)$/,
8 | exclude: /node_modules/,
9 | include: path.join(__dirname, '../src'),
10 | use: {
11 | loader: 'babel-loader',
12 | options: babelrc,
13 | },
14 | });
15 |
16 | const hot = () => ({
17 | test: /\.(js|jsx)$/,
18 | use: 'react-hot-loader/webpack',
19 | include: /node_modules/,
20 | });
21 |
22 | const file = () => ({
23 | test: /\.(svg|png|jpg|gif|woff|woff2|otf|ttf|eot)$/,
24 | loader: 'file-loader',
25 | });
26 |
27 | const less = settings => ({
28 | test: /\.(less|css)$/,
29 | use: [
30 | settings.mode === 'production'
31 | ? {
32 | loader: MiniCssExtractPlugin.loader,
33 | options: {
34 | publicPath: settings.publicPath,
35 | },
36 | }
37 | : 'style-loader',
38 | {
39 | loader: 'css-loader',
40 | options: {
41 | modules: {
42 | localIdentName:
43 | settings.mode === 'production'
44 | ? '[hash:base64:10]'
45 | : '[path][name]__[local]--[hash:base64:5]',
46 | },
47 | },
48 | },
49 | {
50 | loader: 'less-loader',
51 | options: {
52 | prependData: "@import 'open-color/open-color.less';",
53 | },
54 | },
55 | ...(settings.mode === 'production'
56 | ? [{ loader: 'postcss-loader', options: { plugins: () => [autoprefixer] } }]
57 | : []),
58 | ],
59 | });
60 |
61 | module.exports = {
62 | babel,
63 | hot,
64 | file,
65 | less,
66 | };
67 |
--------------------------------------------------------------------------------