31
loading...
This website collects cookies to deliver better user experience
packages
directory to hold the different projects in our monorepo. Your structure should begin looking like this:.
└── packages
└── simple-express-app
└── server.ts
From within the `packages/simple-express-app` directory, run:
yarn init
yarn add express
yarn add -D typescript @types/express
npx tsc --init
tsconfig.json
file. Add the following to it:packages/simple-express-server/tsconfig.json
{
...
"outDir": "./dist",
}
packages/simple-express-server/server.ts
import express from 'express';
const app = express();
const port = 3001;
app.get("/data", (req, res) => {
res.json({ foo: "bar" });
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
.
└── packages
└── simple-express-app
├── server.ts
├── yarn.lock
├── package.json
└── tsconfig.json
package.json
called start
that we can run with yarn
:packages/simple-express-server/package.json
{
"name": "simple-express-server",
"version": "1.0.0",
"main": "dist/server.js",
"license": "MIT",
"scripts": {
"start": "tsc && node dist/server.js"
},
"devDependencies": {
"@types/express": "^4.17.13",
"typescript": "^4.5.4"
},
"dependencies": {
"express": "^4.17.1"
}
}
packages
directory and run this command:yarn create react-app simple-react-app --template typescript
App.tsx
file in the src
directory of the project generated by create-react-app
. We are going to add a simple button that uses the browser fetch API to grab the data from our server and log it to the console. packages/simple-react-app/src/App.tsx
import React from "react";
import logo from "./logo.svg";
import "./App.css";
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
{ /* NEW */ }
<button
onClick={() => {
fetch("http://localhost:3001/", {})
.then((response) => response.json())
.then((data) => console.log(data));
}}
>
GET SOME DATA
</button>
</header>
</div>
);
}
export default App;
.
└── packages
├── simple-express-server
│ ├── server.ts
│ ├── yarn.lock
│ ├── package.json
│ └── tsconfig.json
└── simple-react-app
└── [default setup]
Lerna: For running scripts across multiple projects and adding new dependencies. Lerna is also built to manage publishing your packages (though we will not be doing that as part of this tutorial)
Yarn workspaces: For hoisting all shared dependencies into a single node_modules
folder in the root directory. Each project can still define its own dependencies, so that you don't confuse which dependencies are required for which (client vs. server) for example, but it will pool the installed packages in the root.
yarn init
yarn add -D lerna typescript
npx lerna init
{
"packages": ["packages/*"],
"version": "0.0.0",
"npmClient": "yarn",
"useWorkspaces": true
}
yarn
is our NPM client and that we are using workspaces.package.json
:package.json
{
"name": "monorepo-example",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"start": "lerna run --parallel start"
},
"devDependencies": {
"lerna": "^4.0.0"
}
}
Set private
to true
which is necessary for workspaces to functions
Defined the location of the workspaces as packages/*
which matches any directory we place in packages
Added a script that uses Lerna to run. This will allow us to use a single command to run the equivalent of yarn start
in both our Express server and React app simultaneously. This way they are coupled together so that we don't accidentally forget to run one, knowing that currently they both rely on each other. The --parallel
flag allows them to run at the same time.
simple-express-server
and the one that comes bundled with your simple-react-app
. Make sure both versions are the same in each project's package.json
and both are listed in devDependencies
. Most likely the React app version will be older, so that is the one that should be changed.)npx lerna clean -y
yarn install
node_modules
folders in each of your two packages. This is the equivalent of simply deleting them yourself. node_modules
folder in the root directory.node_modules
in the root is full of packages, while the node_modules
folders in simple-express-server
and simple-react-app
only have a couple (these are mostly symlinks to binaries that are necessary due to the way yarn/npm function)..gitignore
file in the root to make sure we don't commit our auto-generated files:.gitignore
node_modules/
dist/
yarn start
npx tsc --init
.tsconfig.json
. You can delete all the defaults values from this file (your individual projects will se their own configuration values.) The only field you need to include is:tsconfig.json
{
"compilerOptions": {
"baseUrl": "./packages"
}
}
.
├── packages
| ├── simple-express-server
| │ ├── server.ts
| │ ├── yarn.lock
| │ ├── package.json
| │ └── tsconfig.json
| └── simple-react-app
| └── [default setup]
├── lerna.json
├── tsconfig.json
├── package.json
└── yarn.lock
create-react-app
generated automatically. .git
directory inside packages/simple-react-app
. This step is VERY IMPORTANT. Make sure there is no .git
directory inside simple-react-app
.git add .
git commit -am 'first commit'
git remote add origin YOUR_GIT_REPO_ADDRESS
git push -u origin YOUR_BRANCH_NAME
lerna
to install it to both. This will help us make sure that we keep the same version in sync and require us to only have one copy of it in the root directory. npx lerna add lodash packages/simple-*
npx lerna add @types/lodash packages/simple-* --dev
lodash
in any of the projects in the packages
directory that match the simple-*
pattern (which includes both of ours). When using this command you can install the package to dev and peer dependencies by adding --dev
or --peer
at the end. More info on this command here.package.json
file in both your packages you'll see that lodash
has been added with the same version to both files, but the actual package itself has a single copy in the node_modules
folder of your root directory. server.ts
file in our Express project to do a couple of new things. We'll import the shared lodash
library and use one of its functions (_.snakeCase()
) and we'll define a type interface that defines the shape of the data we are sending and export it so that we can also use that interface in our React app to typesafe server queries. server.ts
file to look like the following:packages/simple-express-server.ts
import express from "express";
import _ from "lodash";
const app = express();
const port = 3001;
export interface QueryPayload {
payload: string;
}
app.use((_req, res, next) => {
// Allow any website to connect
res.setHeader("Access-Control-Allow-Origin", "*");
// Continue to next middleware
next();
});
app.get("/", (_req, res) => {
const responseData: QueryPayload = {
payload: _.snakeCase("Server data returned successfully"),
};
res.json(responseData);
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
data
to payload
for clarity)App.tsx
component in simple-react-app
. We'll import lodash
just for no other reason to show that we can import the same package in both client and server. We'll use it to apply _.toUpper()
to the "Learn React" text. QueryPayload
interface from our simple-express-server
project. This is all possible through the magic of workspaces and Typescript.packages/simple-react-app/src/App.tsx
import React from "react";
import logo from "./logo.svg";
import "./App.css";
import _ from "lodash";
import { QueryPayload } from "simple-express-server/server";
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
{_.toUpper("Learn React")}
</a>
<button
onClick={() => {
fetch("http://localhost:3001/", {})
.then((response) => response.json())
.then((data: QueryPayload) => console.log(data.payload));
}}
>
GET SOME DATA
</button>
</header>
</div>
);
}
export default App;
"baseUrl": "./packages"
value in the the tsconfig.json
in the root directory. QueryPayload
directly from our server. That is fairly harmless, but what if we npx lerna create simple-shared-data
npx lerna add typescript --dev
yarn install
simple-shared-data
in your packages
. We've already added the same version of Typescript as a dev dependency. lib
directory that includes the default JS entrypoint as we will not be using it. index.ts
file inside of packages/simple-shared-data
where we will place any types or data that either our front-end, back-end or both can have access to. packages/simple-shared-data/index.ts
export interface QueryPayload {
payload: string;
}
packages/simple-express-server/server.ts
import { QueryPayload } from 'simple-shared-data';
...
packages/simple-react-app/src/App.tsx
import { QueryPayload } from 'simple-shared-data';
...
simple-react-ap
simple-shared-data
simple-express-server
simple-shared-data
<DarkMode />
component. The component is not part of a separate library we can install with an NPM command, it exists as part of a React application that has its own repository.packages/simple-react-app/src
directory we'll run this command:git submodule add [email protected]:alexeagleson/react-dark-mode.git
react-dark-mode
directory (the name of the git repository, you can add another argument after the above command to name the directory yourself).<DarkMode />
component it's as simple as adding:packages/simple-react-app/src/App.tsx
...
import DarkMode from "./react-dark-mode/src/DarkMode";
function App() {
return (
<div className="App">
...
<DarkMode />
</div>
);
}
export default App;
background-color
styles in App.css
are going to override the body
styles, so we need to update App.css
for it to work:packages/simple-react-app/src/App.css
...
.App-header {
/* background-color: #282c34; */
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
/* color: white; */
}
.App-link {
/* color: #61dafb; */
}
...
git status
new file: ../../../.gitmodules
. That's something new if you've never used submodules before. It's a hidden file that has been added to the project root. Let's take a look inside it:[submodule "packages/simple-react-app/src/react-dark-mode"]
path = packages/simple-react-app/src/react-dark-mode
url = [email protected]:alexeagleson/react-dark-mode.git
packages/simple-react-app/src/react-dark-mode/src/DarkMode.css
...
[data-theme="dark"] {
--font-color: #eee;
--background-color: #333;
--link-color: peachpuff;
}
lightblue
to peachpuff
.monorepo-example
repository, but there IS a new commit to react-dark-mode
. Even though we are still inside our monorepo project!git pull
and git fetch
to your main root monorepo aren't going to automatically pull new changes to submodules. To do that you need to run:git submodule update
git pull
it will pull the information about relevant submodules, but it won't actually pull the code from them into your repository. You need to run:git submodule init
--recurse-submodules
flag like so:git pull --recurse-submodules
or
git clone --recurse-submodules
@
character. Below I will quickly show how to update this tutorial to add a @my-namespace
namespace:name
value in each of your three package.json
files with @my-namespace
. For example simple-express-server/package.json
will now be:{
"name": "@my-namespace/simple-express-server",
...
}
packages/simple-express-server/server.ts
import { QueryPayload } from '@my-namespace/simple-shared-data';
...
packages/simple-react-app/src/App.tsx
import { QueryPayload } from '@my-namespace/simple-shared-data';
...
yarn install
to update those packages inside your root node_modules
directory and you're good to go!