This repository show you how to create mono repository with Ts.ED and Vite/React. It tries to show step by step, how to install the different techno to obtain an integrated build chain.
The technologies presented are switchable. If you want to make an application on Vue/Svelte, it's possible because Vite support it. You can also change Ts.ED to another backend framework.
The idea is essentially to see how the mono repository is structured to put a front and back and tools like storybook!
- Node.js 16+
- TypeScript
- Ts.ED
- React
- Tailwind CSS 3
- Vite
- Nx and Yarn 3 workspaces
- Jest 28+
- Eslint & Prettier
- Lint-staged
- Husky
- Storybook and tailwind css viewer
To begin we need to configure yarn:
corepack enable
yarn init -2
Add nodeLinker: node-modules
in .yarnrc.yml
.
Note: PNP support is not covered at this step.
Edit package.json
and add:
{
"workspaces": [
"packages/*",
"packages/**/*"
]
}
mkdir packages/web/components && cd packages/web/components && yarn init -y
mkdir packages/config && cd packages/config && yarn init -y
Edit the packages/web/components
and add the following line:
{
"main": "src/index.ts",
}
This line is necessary for other packages that consumes the right entrypoint from
@project/components
.
For the app:
mkdir packages/web/app && cd packages/web/app && yarn create vite .
Then select react-ts option.
Note: Edit all
package.json
and add"version": "1.0.0"
.
Then install NX:
yarn dlx add-nx-to-monorepo
Create the root tsconfig.json
and add the following scripts:
{
"files": [],
"references": [
{
"path": "./packages/web/app"
},
{
"path": "./packages/web/components"
}
]
}
Add a tsconfig.web.json
in packages/config
with the following content:
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
}
}
Add a tsconfig.node.json
in packages/config
with the following content:
{
"compilerOptions": {
"module": "commonjs",
"target": "esnext",
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"moduleResolution": "node",
"isolatedModules": false,
"preserveConstEnums": true,
"suppressImplicitAnyIndexErrors": false,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"allowSyntheticDefaultImports": true,
"importHelpers": true,
"newLine": "LF",
"noEmit": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"composite": true,
"lib": [
"es7",
"dom",
"ESNext.AsyncIterable"
]
}
}
Finally for each front-end package you'll need to create a tsconfig.json
and tsconfig.node.json
with the following content:
tsconfig.json:
{
"extends": "@project/config/tsconfig.web.json",
"compilerOptions": {
"rootDir": "src"
},
"include": ["src"],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}
tsconfig.node.json:
{
"extends": "@project/config/tsconfig.web.json",
"compilerOptions": {
"rootDir": "src"
},
"include": ["src"],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}
yarn workspace @project/config add -D eslint prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-plugin-workspaces eslint-config-prettier eslint-plugin-import eslint-plugin-simple-import-sort
yarn workspace @project/config add -D eslint-config-react-app eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-testing-library eslint-plugin-jsx-a11y
yarn workspace @project/config add -D vite-plugin-eslint
In packages/config/eslint
:
- Create a
packages/config/eslint/node.js
file from this example, - Create a
packages/config/eslint/web.js
file from this example.
Then create .eslintrc.js
for each packages in packages/config
.
Add the following configuration if the packages is for a web
(front) env:
module.exports = {
extends: [require.resolve("@project/config/eslint/web")]
};
Edit also the vite.config.ts
in packages/web/app
directory and the lines related to eslint:
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
+ import eslint from "vite-plugin-eslint";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
+ eslint()
]
});
Add the following configuration if the packages is for a node.js
(back) env:
module.exports = {
extends: [require.resolve("@project/config/eslint/node")]
};
Then, add for each packages/**/*/package.json
:
{
"scripts": {
"lint": "eslint \"./src/**/*.{js,jsx,ts,tsx}\"",
"lint:fix": "yarn lint --fix"
}
}
Finally, add the following scripts in the root package.json
:
{
"scripts": {
"lint": "nx run-many --target=lint",
"lint:fix": "nx run-many --target=lint:fix"
}
}
yarn add -D lint-staged
Edit root package.json
and add the following configuration:
{
"lint-staged": {
"**/*.{ts,tsx,js,jsx}": [
"eslint --fix",
"git add"
],
"**/*.{json,md,yml,yaml}": [
"prettier --write",
"git add"
]
}
}
yarn add -D @commitlint/cli @commitlint/config-conventional
echo "module.exports = {extends: ['@commitlint/config-angular']};" > commitlint.config.js
yarn dlx husky-init --yarn2 && yarn
yarn add is-ci
yarn husky add .husky/commit-msg 'yarn commitlint --edit $1'
yarn husky add .husky/post-commit 'git update-index --again'
yarn husky add .husky/pre-commit 'npx lint-staged $1'
Edit package.json
and replace "postinstall" step by:
"scripts": {
- "postinstall": "husky install",
+ "prepare": "is-ci || husky install",
}
yarn add -D cross-env jest jest-environment-jsdom jest-watch-typeahead @swc/core @swc/jest @types/jest @testing-library/dom @testing-library/jest-dom @testing-library/react @testing-library/user-event
yarn workspace @project/config add -D camelcase
In packages/config/jest
, create the following files:
- Create
jest.web.config.js
file from this example, - Create
cssTransform.js
file from this example, - Create
fileTransform.js
file from this example, - Create
setupTest.js
file from this example, - Create
swc.web.json
file from this example.
In packages/web/app
and packages/web/components
, create a jest.config.js
with the following code:
module.exports = require("@project/config/jest/jest.web.config.js");
Edit packages/web/app/package.json
and packages/web/components/package.json
and add the following scripts:
{
"scripts": {
"test": "cross-env NODE_ENV=test jest --coverage"
}
}
And finally, edit the root package.json
and add the following scripts:
{
"scripts": {
"test": "nx run-many --target=test --all"
}
}
yarn workspace @project/config add -D tailwindcss tailwindcss-cli postcss autoprefixer postcss-flexbugs-fixes postcss-preset-env postcss-nested
In packages/config
:
- Create
postcss.config.js
file from this example, - Create
tailwind.config.js
file from this example.
In packages/web/app
, create a postcss.config.js
file with the following content:
module.exports = require("@project/config/postcss.config.js");
In packages/web/app
, create a tailwind.config.js
file with the following content:
module.exports = require("@project/config/tailwind.config.js");
In packages/web/components/styles/tailwind
, create an index.css
file with the following content:
@tailwind base;
@tailwind components;
@tailwind utilities;
In packages/web/components/styles
, create an index.css
file with the following content:
@import "./tailwind/index.css";
Then, in packages/web/components
, create an index.ts
file with the following content:
import "./styles/index.css";
export * from "./components/button/Button";
Now, when a component is used in app or any other web package, the tailwind configuration will be loaded automatically.
Create the new package with:
mkdir packages/web/storybook && cd packages/web/storybook && yarn init -y
Add version in the generated package.json
:
{
"name": "@project/storybook",
"version": "1.0.0"
}
Run the following command under packages/web/storybook
:
yarn dlx sb init --builder @storybook/builder-vite --type react
yarn workspace @project/storybook add -D @storybook/addon-postcss
Edit package.json
in packages/web/storybook
and change the following lines:
{
"scripts": {
+ "start:storybook": "start-storybook -p 6006",
+ "build:storybook": "build-storybook -o dist"
- "storybook": "start-storybook -p 6006",
- "build-storybook": "build-storybook -o dist"
}
}
Edit the root package.json
and add the following scripts:
{
"scripts": {
"start:storybook": "nx start:storybook @project/storybook",
"build:storybook": "nx build:storybook @project/storybook"
}
}
Edit main.js
located in packages/web/storybook/.storybook
and add the following code:
const { map } = require('@project/config/packages/index.js');
module.exports = {
"stories": [
...map("web/components", [
"**/*.stories.mdx",
"**/*.stories.@(js|jsx|ts|tsx)"
]),
...map("web/app", [
"**/*.stories.mdx",
"**/*.stories.@(js|jsx|ts|tsx)"
]),
"../stories/**/*.stories.mdx",
"../stories/**/*.stories.@(js|jsx|ts|tsx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
{
name: '@storybook/addon-postcss',
options: {
cssLoaderOptions: {
importLoaders: 1,
},
postcssLoaderOptions: {
// When using postCSS 8
implementation: require('postcss'),
},
},
},
"framework": "@storybook/react",
"core": {
"builder": "@storybook/builder-vite"
},
"features": {
"storyStoreV7": true
}
]
}
Edit preview.js
located in packages/web/storybook/.storybook
and add the following code:
import "@project/components";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
}
}
In packages/web/storybook
, create a postcss.config.js
file with the following content:
module.exports = require("@project/config/postcss.config.js");
In packages/web/storybook
, create a tailwind.config.js
file with the following content:
module.exports = require("@project/config/tailwind.config.js");
Run:
yarn workspace @project/config add -D tailwindcss-cli tailwind-config-viewer rimraf
Then add in packages/config/package.json
the following scripts:
{
"scripts": {
"start:tailwind": "tailwind-config-viewer -o",
"build:tailwind": "tailwind-config-viewer export ../web/storybook/public && cp ../web/storybook/public/index.html ../web/storybook/public/tailwind.html && yarn clean:tailwind",
"clean:tailwind": "rimraf ../web/storybook/public/index.html ../web/storybook/public/favicon.ico"
}
}
Edit the root package.json
and change the following scripts:
{
"scripts": {
- "start:storybook": "nx start:storybook @project/storybook",
- "build:storybook": "nx build:storybook @project/storybook",
+ "start:storybook": "nx build:tailwind @project/config && nx start:storybook @project/storybook",
+ "build:storybook": "nx build:tailwind @project/config && nx build:storybook @project/storybook",
}
}
Edit main.js
located in packages/web/storybook/.storybook
and add the following code:
module.exports = {
staticDirs: ["../public"]
}
Finally, create a new story tailwind.stories.mdx
in packages/web/storybook/stories
with the following code:
import { Meta } from '@storybook/addon-docs/blocks'
<Meta title="Tailwind"/>
<style>{`
import { Meta } from '@storybook/addon-docs/blocks'
<Meta title="Tailwind"/>
<style>{`
.sbdocs-wrapper {
padding: 0 !important;
}
.sbdocs .sbdocs-content {
max-width: 100%;
}
`}</style>
<iframe src="./tailwind.html" style={{height: '100vh', width: '100vw'}}/>
Run the following commands:
mkdir packages/back/server
cd packages/back/server
yarn dlx @tsed/cli init .
Select the following options:
? Choose the target platform: Express.js
? Choose the architecture for your project: Ts.ED
? Choose the convention file styling: Ts.ED
? Check the features needed for your project Database, Swagger, Testing
? Choose a ORM manager Mongoose
? Choose unit framework Jest
Edit the packages/back/server/package.json
and apply changes:
{
+ "name": "@project/server",
"scripts": {
+ "clean": "rimraf dist tsconfig.tsbuildinfo",
- "build": "yarn run barrels && tsc --project tsconfig.compile.json",
+ "build": "yarn run barrels && tsc --build",
- "test": "yarn run test:lint && yarn run test:coverage",
+ "test": "yarn run lint && yarn run test:coverage",
"test:unit": "cross-env NODE_ENV=test jest",
"test:coverage": "yarn run test:unit",
- "test:lint": "eslint '**/*.{ts,js}'",
- "test:lint:fix": "eslint '**/*.{ts,js}' --fix"
+ "lint": "eslint '**/*.{ts,js}'",
+ "lint:fix": "eslint '**/*.{ts,js}' --fix"
},
"devDependencies": {
- "@typescript-eslint/eslint-plugin": "^5.30.4",
- "@typescript-eslint/parser": "^5.30.4",
- "eslint-config-prettier": "^8.5.0",
- "eslint-plugin-prettier": "^4.2.1",
- "husky": "^8.0.1",
- "is-ci": "^3.0.1",
- "jest": "^28.1.2",
}
}
Edit the root package.json
and add the following scripts:
{
"scripts": {
"clean": "nx run-many --target=clean --all",
"start:back:server": "nx start @project/server",
"build:barrels": "nx run-many --target=barrels --all",
"build": "nx run-many --target=build --all"
}
}
And run the following command:
yarn add barrelsby
Edit the root tsconfig.json
and add the following scripts:
{
"files": [],
"references": [
{
"path": "./packages/web/app"
},
{
"path": "./packages/web/components"
},
{
"path": "./packages/back/server"
}
]
}
- Edit the
packages/back/server/tsconfig.json
file,
Create .eslintrc.js
in packages/back/server
with the following code:
module.exports = require("@project/config/eslint/node.js");
- Create a
packages/config/jest.node.config.json
file from this example, - Create
swc.web.json
file from this example.
Create a packages/back/server/jest.config.json
with the following code:
module.exports = require("@project/config/jest/jest.node.config.js");
It's possible to use Yarn workspace to create backend package. This is an effective way to better organize your code. However, adding a back package requires performing some steps.
Here we will create the api
package which will contain all our Ts.ED Controllers
mkdir packages/back/api && cd packages/back/api && yarn init -y
Edit the packages/back/api/package.json
and apply changes:
{
- "name": "api"
+ "name": "@project/api"
+ "scripts": {
+ "clean": "rimraf dist tsconfig.tsbuildinfo",
+ "build": "yarn run barrels && tsc --build",
+ "barrels": "barrelsby --config .barrelsby.json",
+ "test": "yarn run lint && yarn run test:coverage",
+ "test:unit": "cross-env NODE_ENV=test jest",
+ "test:coverage": "yarn run test:unit",
+ "lint": "eslint '**/*.{ts,js}'",
+ "lint:fix": "eslint '**/*.{ts,js}' --fix"
+ }
}
- Create
.barrelsby.json
file from this example - Create
.eslintrc.js
file from this example - Create
.jest.config.js
file from this example - Create
tsconfig.json
file from this example
Edit the root tsconfig.json
and add the following references:
{
"references": [
{
"path": "./packages/back/api"
}
]
}
To link the server
package to the new api
package, you have to edit the packages/back/server/tsconfig.json
and
add also a reference:
{
"references": [
{
"path": "../api"
}
]
}
And to preserve the build order when you'll run the yarn build
command, you have to add the api
package dependency to the server
package:
{
"dependencies": {
"@project/api": "1.0.0"
}
}
Finally, run yarn install
to create link between packages!
Add the Ts.ED plugin @tsed/cli-core
to use custom commands:
yarn workspace @project/server add @tsed/cli-core @tsed/cli swagger-typescript-api @tsed/cli-generate-http-client
yarn workspace @project/server add -D @types/inquirer @types/fs-extra
Then add packages/back/server/bin/index.ts
file and add the following code:
#!/usr/bin/env node
import { CliCore } from "@tsed/cli-core";
import { GenerateHttpClientCmd } from "@tsed/cli-generate-http-client";
import { config } from "../config";
import { Server } from "../Server";
CliCore.bootstrap({
...config,
server: Server,
// add your custom commands here
commands: [GenerateHttpClientCmd],
httpClient: {
transformOperationId(operationId: string) {
return operationId.replace(/Controller/g, "");
}
}
}).catch(console.error);
Edit also the packages/back/server/package.json
and add the following script:
{
"scripts": {
"build:http:client": "tsed run generate-http-client --output ../../web/http-client/src/__generated__"
}
}
The following script will generate the HttpClient in the packages/web/http-client
.
Add a package.json
in packages/web/http-client
with the following content:
{
"name": "@project/http-client",
"version": "1.0.0",
"main": "src/index.ts",
"scripts": {
"build": "yarn run barrels",
"lint": "eslint \"./src/**/*.{js,jsx,ts,tsx}\"",
"lint:fix": "yarn lint --fix",
"test": "cross-env NODE_ENV=test jest --coverage",
"barrels": "barrelsby --config .barrelsby.json"
},
"devDependencies": {
"@project/server": "1.0.0"
}
}
Then add the following scripts to the root package.json:
{
"scripts": {
"build:http:client": "yarn build:back:server && nx build:http:client @project/server && yarn run build:barrels",
"postinstall": "yarn build:http:client"
}
}
Run yarn build:http:client
to generate the client!
Update the packages/web/app/vite.config.ts
to allow communication between the front and backend via the proxy options:
export default defineConfig({
plugins: [react(), eslint()],
server: {
proxy: {
"/rest": "http://localhost:8083"
}
}
});
We need to create a hook to call our backend. Here is the useVersion hook:
import "./App.css";
import { Button } from "@project/components";
import { httpClient, VersionInfoModel } from "@project/http-client";
import { useEffect, useState } from "react";
import logo from "./logo.svg";
function useVersion() {
const [versionInfo, setVersionInfo] = useState<VersionInfoModel>({} as any);
useEffect(() => {
httpClient.version.get().then((versionInfo: VersionInfoModel) => {
setVersionInfo(versionInfo);
});
}, [setVersionInfo]);
return { versionInfo };
}
Here we use the http client generated previously to consume data from our API.
Here is the complete App.tsx
code:
import "./App.css";
import { Button } from "@project/components";
import { httpClient, VersionInfoModel } from "@project/http-client";
import { useEffect, useState } from "react";
import logo from "./logo.svg";
function useVersion() {
const [versionInfo, setVersionInfo] = useState<VersionInfoModel>({} as any);
useEffect(() => {
httpClient.version.get().then((versionInfo: VersionInfoModel) => {
setVersionInfo(versionInfo);
});
}, [setVersionInfo]);
return { versionInfo };
}
function App() {
const [count, setCount] = useState(0);
const { versionInfo } = useVersion();
return (
<div className="text-center">
<header className="bg-gray-800 min-h-screen flex items-center justify-center app-header text-white flex-col">
<img src={logo} className="app-logo" alt="logo" />
<p>Hello Ts.ED + Vite + React!</p>
<p>Version: {versionInfo.version}</p>
<p>
<Button onClick={() => setCount((count) => count + 1)}>count is: {count}</Button>
</p>
<p>
Edit <code>App.tsx</code> and save to test HMR updates.
</p>
<p>
<a className="app-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">
Learn React
</a>
{" | "}
<a className="app-link" href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener noreferrer">
Vite Docs
</a>
</p>
</header>
</div>
);
}
export default App;
Unfortunately, I haven't found a good alternative for the
lerna version
. So, we need to install lerna to maintain and update packages version.
Install the following modules:
yarn add lerna @tsed/monorepo-utils semantic-release
Then add the following lines in the root package.json
:
{
"scripts": {
"configure": "monorepo ci configure",
"release": "semantic-release"
},
"monorepo": {
"productionBranch": "master",
"developBranch": "master",
"npmAccess": "public"
}
}
- Create a
release.config.json
file from this example, - Create a
lerna.json
file from this example.
That all! release
command will bump version, apply Git tag, publish all packages on NPM and push a release note on Github releases.
If you use Github Actions you can use the release
command as following:
deploy-packages:
runs-on: ubuntu-latest
needs: [ lint, test ]
if: github.event_name != 'pull_request' && contains('
refs/heads/production
refs/heads/alpha
refs/heads/beta
refs/heads/rc
', github.ref)
strategy:
matrix:
node-version: [ 16.x ]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: yarn install --immutable
- name: Release packages
env:
CI: true
GH_TOKEN: ${{ secrets.GH_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: yarn release