Skip to content

tsedio/tsed-vite-workspaces

Repository files navigation

Ts.ED logo

Ts.ED + Vite + Nx + Mono repo

PR Welcome semantic-release code style: prettier backers


Website   •   Getting started   •   Slack   •   Twitter

Introduction

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!

Features

  • 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

Steps

Prepare workspaces

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

Configure TypeScript

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"
    }
  ]
}

Eslint & prettier

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.

Web

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()
  ]
});

Node

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"
  }
}

Add lint-staged

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"
    ]
  }
}

Commit lint

yarn add -D @commitlint/cli @commitlint/config-conventional
echo "module.exports = {extends: ['@commitlint/config-angular']};" > commitlint.config.js

Husky

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",
}

Jest & testing-library

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 

Web

In packages/config/jest, create the following files:

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"
  }
}

Tailwind CSS

yarn workspace @project/config add -D tailwindcss tailwindcss-cli postcss autoprefixer postcss-flexbugs-fixes postcss-preset-env postcss-nested

In packages/config:

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.

Storybook

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");

Display tailwind configuration in storybook

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'}}/>

Create server

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

Configure TypeScript

Edit the root tsconfig.json and add the following scripts:

{
  "files": [],
  "references": [
    {
      "path": "./packages/web/app"
    },
    {
      "path": "./packages/web/components"
    },
    {
      "path": "./packages/back/server"
    }
  ]
}

Eslint

Create .eslintrc.js in packages/back/server with the following code:

module.exports = require("@project/config/eslint/node.js");

Jest

Create a packages/back/server/jest.config.json with the following code:

module.exports = require("@project/config/jest/jest.node.config.js");

Add subpackages in back

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.

Create the new package

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"
+ }
}

Add references

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!

Generate HttpClient from Ts.ED

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!

Configure proxy

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"
    }
  }
});

Get the server version and display it in the web app

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;

Publish packages

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"
  }
}

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