AstroJS projects in monorepo using npm workspaces
Sometimes I make several similar subprojects in one project, for example, a set of static landings for collecting applications and a blog on AstroJS. The subprojects differ in content and design theme, but use common blocks. At the same time, I do not want to publish common blocks in the public space.
In this case it is useful to have a monorepo, and I will do this without external dependencies only using npm workspaces.
Advantages of monorepo
Disk space efficiency
Only one copy of a dependency is installed, shared by multiple packages.
Faster installation
When installing external npm packages, we download only one package instead of multiple copies.
Consistent versions of dependencies
All packages in npm workspaces use the same version of a dependency – no more version conflicts.
Example of a project
Let's create a monorepo for the blog:
- External part of the site: AstroJS
- Component Library: Component Files
.astro
- library of auxiliary elements: scripts, types, styles
The project structure we want to achieve is:
/
├── node_modules/
├── packages
│ ├── astrojs (делаем сейчас один, потом его дублируем)
│ ├── design-components
│ └── helpers
├── package.json
└── other files
Then we can add several packages with AstroJS landing pages using the common blocks we've already created.
For each landing page/blog there is an AstroJS project with variables only for this project:
- the content of each project will be in its own folder with AstroJS:
/astrojs/src/content
; - css variables for the design theme of each landing page:
/astrojs/src/styles/theme.css
; - texts and a set of links for the menu, header and footer: for example,
/astrojs/src/data/linksFooter.json
; - favicons:
/astrojs/public/favicons/favicon.ico
- default pictures for social networks:
/astrojs/public/images/og-default.png
In each project we will include:
- local files with variables
- common component library (receives data from each project)
- common helpers files: styles, types and js/ts functions (files will receive data from each project)
Creating a root project
Let's create the root folder of our project:
mkdir root-project
cd root-project
Initialize the project:
npm init -y
Open the project in a code editor. I use VS Code:
code .
Editing our root package.json
for the entire project and specify where to get the child packages:
{
"name": "my-blog",
"private": true,
"workspaces": [
"packages/*"
]
}
Let's create folders for our packages:
- root-project/packages/astrojs
- root-project/packages/design-components
For convenience, I create files right away .editorconfig
And .nvmrc
in the root folder.
Creating a Package with AstroJS
Go to the folder with AstroJS and install AstroJS itself:
cd packages/astrojs
npm create astro@latest
During the installation process, select:
- install to current folder
- the simplest project
- We'll install the dependencies later
- git does not need to be initialized
After installation, a file appears in the package folder package.json
and the files required for AstroJS.
Editing the file package.json
:
- we write the name of the package, taking into account the root
my-blog
- we move all dependencies of the AstroJS package to dependencies of the root project
Code package.json
astrojs-package:
{
"name": "@my-blog/astrojs",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro",
"check": "astro check"
}
}
Code package.json
root package:
{
"name": "my-blog",
"private": true,
"workspaces": [
"packages/*"
],
"dependencies": {
"astro": "^4.13.1",
"typescript": "^5.5.4"
},
"devDependencies": {
"@astrojs/check": "^0.9.1"
}
}
Creating a package with a component library
Working with folders and files in the code editor.
We create package.json
in folder packages/design-components
:
{
"name": "@my-blog/design-components",
"version": "0.0.1",
"type": "module",
"private": true
}
Create a folder for components components
and the first component Card.astro
(path from root project: root-project/packages/design-components/components/Card.astro
)
---
---
<div>Card component</div>
Installing packages
Let's return to the root project.
Install dependencies (all local and external packages):
npm install
Result: added 412 packages, and audited 415 packages in 1m
A folder appeared in the root project node_modules
:
- many external projects
- folder
@my-blog
with links to subfolders:@my-blog/astrojs
And@my-blog/design-components
Connecting the Component Library
From the root project, go to our AstroJS package and run it:
cd packages/astrojs
npm run dev
In the browser we check: http://localhost:4321/ — the project has been launched.
Let's edit tsconfig.json
— add import aliases:
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["dist"]
}
Line "exclude": ["dist"]
needed because recently AstroJS checks the folder .dist
when checking with the command astro check
.
Let's edit our main page root-project/packages/astrojs/src/pages/index.astro
and add our component from the local package there
---
import Layout from "@/layouts/Layout.astro";
// components
import Card from "@my-blog/design-components/components/Card.astro";
---
<Layout title="Welcome to Astro.">
<main>
<h1>Привет, мир! Это монорепо</h1>
<p>Глобальные стили подключены из пакета `helpers`.</p>
<Card />
</main>
</Layout>
Congratulations! Now you can work with two local packages.
Connecting the site design theme
Create styles: root-project/packages/astrojs/src/styles/theme.css
:root {
/* FONTS */
--font-family-base: "Comic Sans MS";
/* COLORS */
--color-theme-pale: #f3e8ff;
--color-theme-muted: #d8b4fe;
--color-theme-neutral: #a855f7;
--color-theme-bold: #7e22ce;
--color-theme-intense: #581c87;
}
Add the style file to root-project/packages/astrojs/src/layouts/Layout.astro
:
---
// styles
import "@/styles/theme.css";
interface Props {
title: string;
}
const { title } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="description" content="Astro description" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="https://habr.com/favicon.svg" />
<title>{title}</title>
</head>
<body>
<slot />
</body>
</html>
Let's use these project-wide style variables in a component from the library root-project/packages/design-components/components/Card.astro
and add receiving props:
---
interface Props {
title?: string;
text?: string;
}
const { title = "Card title", text = "Card text" } = Astro.props;
---
<div class="card">
<p class="title">{title}</p>
<p class="text">{text}</p>
</div>
<style>
.card {
background-color: var(--color-theme-pale);
color: var(--color-theme-intense);
padding: 24px 16px;
border-radius: 8px;
display: flex;
flex-direction: column;
gap: 8px;
}
.title {
font-weight: 700;
}
</style>
Let's take the texts from AstroJS in the file into the component packages/astrojs/src/pages/index.astro
:
---
import Layout from "@/layouts/Layout.astro";
// components
import Card from "@my-blog/design-components/components/Card.astro";
---
<Layout title="Welcome to Astro.">
<main>
<h1>Привет, мир! Это монорепо</h1>
<p>Глобальные стили подключены из пакета `helpers`.</p>
<Card
title="Компонент Card подключен из пакета `design-components`"
text="Тексты и переменные для темы оформления подключены из пакета `astrojs`."
/>
</main>
</Layout>
Congratulations! Now your components from the common library can have individual design styles and texts that are set in the main project.
Creating and connecting the helpers package
It is useful to have common things for all projects in a separate package.
Let's create a package in a new folder root-project/packages/helpers
.
Let's add a file package.json
for the new package:
{
"name": "@my-blog/helpers",
"version": "0.0.1",
"type": "module",
"private": true
}
Let's create a common file with style reset root-project/packages/helpers/styles/reset.css
:
*,
*::after,
*::before {
margin: 0;
padding: 0;
box-sizing: border-box;
}
Let's create a common global styles file root-project/packages/helpers/styles/global.css
:
body {
min-width: 320px;
background-color: var(--color-background-page, red);
color: var(--color-text-page, blue);
line-height: 24px;
font-weight: 400;
font-size: 16px;
font-family: var(--font-family-base, monospace);
}
We go to the root project and install our new package:
# мы были в AstroJS проекте, остановим его: CTRL + C
# выходим на уровень корневого проекта:
cd ../..
# теперь мы в корневом проекте, устанавливаем новый локальный пакет:
npm i
Result: added 1 package, and audited 419 packages in 1s
Let's go back to the AstroJS package folder and run it again:
cd packages/astrojs
npm run dev
Connecting style files from the package helpers
in Layout root-project/packages/astrojs/src/layouts/Layout.astro
:
---
// styles
import "@my-blog/helpers/styles/reset.css";
import "@/styles/theme.css";
import "@my-blog/helpers/styles/global.css";
interface Props {
title: string;
}
const { title } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="description" content="Astro description" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="https://habr.com/favicon.svg" />
<title>{title}</title>
</head>
<body>
<slot />
</body>
</html>
Congratulations! Your components are now separated from the styles, which will be the same for all projects.
conclusions
After all these steps we have a monorepo for many projects and a minimum of dependencies.
Let's check the dependencies at the moment (only those necessary for AstroJS itself): npm ls
my-blog
├── @astrojs/check@0.9.1
├── @my-blog/astrojs@0.0.1 -> ./packages/astrojs
├── @my-blog/design-components@0.0.1 -> ./packages/design-components
├── @my-blog/helpers@0.0.1 -> ./packages/helpers
├── astro@4.13.1
└── typescript@5.5.4
For ease of development, you can set up AstroJS projects according to my instructions.
In this case, we install external packages into the root project:
- stylelint
- pretty
- eslint
- other cookies.
If the AstroJS project needs dependencies like “@astrojs/react”, we also install them in the root project.
FAQ
QUESTION: Should we publish our packages to npm?
There is no need to publish to npm. These are local dependencies that live in your own monorep.
If you use your component library in many projects (not just for this blog), you can take it out as a separate project and publish it to npm with your own name. Then in future projects you will install an external package from the general npm. Other users will be able to see and use your component library in the general npm.
An example of such a solution: Yandex UI components library. They could have left it as an internal project only in their monorep, but they made it publicly available.
QUESTION: What do we add to git?
The root project goes into git: git init
.
In the project inside git you will have all your local packages.
Don't forget to check what you have in git from the package with AstroJS: .gitignore
it should be banned
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
QUESTION: what to do if we install a monorepo already created by someone via npm workspaces?
In the root folder we do npm install
— all dependencies, both external and local, will be installed.
QUESTION: How to work with several packages at once, for example, StrapiJS as a CMS and AstroJS as a frontend?
In the root project for package.json
add commands:
{
"name": "my-blog",
"workspaces": [
"packages/*"
],
{
"scripts": {
"build": "npm run build:package-a && npm run build:package-b",
"build:package-a": "cd packages/package-a && npm run build",
"build:package-b": "cd packages/package-b && npm run build"
}
}
}