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 -2Add 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 -yEdit 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.jsonand add"version": "1.0.0".
Then install NX:
yarn dlx add-nx-to-monorepoCreate 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-eslintIn packages/config/eslint:
- Create a
packages/config/eslint/node.jsfile from this example, - Create a
packages/config/eslint/web.jsfile 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-stagedEdit 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.jsyarn 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.jsfile from this example, - Create
cssTransform.jsfile from this example, - Create
fileTransform.jsfile from this example, - Create
setupTest.jsfile from this example, - Create
swc.web.jsonfile 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-nestedIn packages/config:
- Create
postcss.config.jsfile from this example, - Create
tailwind.config.jsfile 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 -yAdd 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-postcssEdit 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 rimrafThen 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 JestEdit 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 barrelsbyEdit 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.jsonfile,
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.jsonfile from this example, - Create
swc.web.jsonfile 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 -yEdit 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.jsonfile from this example - Create
.eslintrc.jsfile from this example - Create
.jest.config.jsfile from this example - Create
tsconfig.jsonfile 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-extraThen 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.jsonfile from this example, - Create a
lerna.jsonfile 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