Иногда я делаю сразу несколько похожих подпроектов в одном проекте, например, набор статичных лендингов для сбора заявок и блог на AstroJS. Подпроекты отличаются контентом и темой оформления, но используют общие блоки. При этом публиковать общие блоки в публичном пространстве не хочется.
В этом случае полезно иметь монорепу, и я это сделаю без внешних зависимостей только с помощью npm workspaces.
Преимущества monorepo
Эффективность дискового пространства
Устанавливается только одна копия зависимости, общая для нескольких пакетов.
Более быстрая установка
При установке внешних npm-пакетов мы скачиваем только один пакет вместо нескольких копий.
Согласованные версии зависимостей
Все пакеты в npm workspaces используют одну и ту же версию зависимости — больше нет конфликта версий.
Пример проекта
Создадим монорепо для блога:
- внешняя часть сайта: AstroJS
- библиотека компонентов: файлы компонентов
.astro
- библиотека вспомогательных элементов: скрипты, типы, стили
Структура проекта, которую мы хотим получить:
/
├── node_modules/
├── packages
│ ├── astrojs (делаем сейчас один, потом его дублируем)
│ ├── design-components
│ └── helpers
├── package.json
└── other files
Потом мы можем добавить несколько пакетов с лендингами на AstroJS, используя уже созданные общие блоки.
Для каждого лендинга/блога есть свой AstroJS проект с переменными только для этого проекта:
- контент каждого проекта будет в своей папке c AstroJS:
/astrojs/src/content
; - css-переменные для темы оформления каждого лендинга:
/astrojs/src/styles/theme.css
; - тексты и набор ссылок для меню, шапки и подвала: например,
/astrojs/src/data/linksFooter.json
; - favicons:
/astrojs/public/favicons/favicon.ico
- дефолтные картинки для соцсетей:
/astrojs/public/images/og-default.png
В каждый проект будем подключать:
- локальные файлы с переменными
- общую библиотеку компонентов (получает данные от каждого проекта)
- общие файлы helpers: стили, типы и js/ts-функции (файлы будут получать данные от каждого проекта)
Создание корневого проекта
Создаем корневую папку нашего проекта:
mkdir root-project
cd root-project
Инициализируем проект:
npm init -y
Открываем проект в редакторе кода. Я использую VS Code:
code .
Редактируем наш корневой package.json
для всего проекта и указываем, откуда брать дочерние пакеты:
{
"name": "my-blog",
"private": true,
"workspaces": [
"packages/*"
]
}
Создаем папки для наших пакетов:
- root-project / packages / astrojs
- root-project / packages / design-components
Для удобства я сразу создаю файлы .editorconfig
и .nvmrc
в корневой папке.
Создание пакета с AstroJS
Заходим в папку с AstroJS и устанавливаем сам AstroJS:
cd packages/astrojs
npm create astro@latest
В процессе установки выбираем:
- установка в текущую папку
- самый простой проект
- зависимости установим потом
- git инициализировать не надо
После установки в папке пакета появляется файл package.json
и необходимые для AstroJS файлы.
Редактируем файл package.json
:
- прописываем название пакета, с учетом корневого
my-blog
- выносим все dependencies пакета AstroJS в dependencies корневого проекта
Код package.json
astrojs-пакета:
{
"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"
}
}
Код package.json
корневого пакета:
{
"name": "my-blog",
"private": true,
"workspaces": [
"packages/*"
],
"dependencies": {
"astro": "^4.13.1",
"typescript": "^5.5.4"
},
"devDependencies": {
"@astrojs/check": "^0.9.1"
}
}
Создание пакета с библиотекой компонентов
Работаем с папками и файлами в редакторе кода.
Создаем package.json
в папке packages/design-components
:
{
"name": "@my-blog/design-components",
"version": "0.0.1",
"type": "module",
"private": true
}
Создаем папку для компонентов components
и первый компонент Card.astro
(путь от корневого проекта: root-project/packages/design-components/components/Card.astro
)
---
---
<div>Card component</div>
Установка пакетов
Возвращаемся в корневой проект.
Устанавливаем зависимости (все локальные и внешние пакеты):
npm install
Результат: added 412 packages, and audited 415 packages in 1m
В корневом проекте появилась папка node_modules
:
- множество внешних проектов
- папка
@my-blog
с линками на подпапки:@my-blog/astrojs
и@my-blog/design-components
Подключение библиотеки компонентов
Из корневого проекта переходим в наш AstroJS пакет и запускаем его:
cd packages/astrojs
npm run dev
В браузере проверяем: http://localhost:4321/ — проект запустился.
Отредактируем tsconfig.json
— добавим import aliases:
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["dist"]
}
Строка "exclude": ["dist"]
нужна, потому что последнее время AstroJS проверяет папку .dist
при проверке командой astro check
.
Отредактируем нашу главную страницу root-project/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 />
</main>
</Layout>
Поздравляю! Теперь вы умеете работать в двумя локальными пакетами.
Подключение темы оформления сайта
Создаем стили: 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;
}
Добавляем файл стилей в 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="/favicon.svg" />
<title>{title}</title>
</head>
<body>
<slot />
</body>
</html>
Используем эти переменные стилей всего проекта в компоненте из библиотеки root-project/packages/design-components/components/Card.astro
и добавим получение 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>
Заберем в компонент тексты из AstroJS в файле 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>
Поздравляю! Теперь ваши компоненты из общей библиотеки могут иметь индивидуальный стиль оформления и тексты, которые задаются в основном проекте.
Создание и подключение пакета helpers
Полезно иметь общие вещи для всех проектов в отдельном пакете.
Создадим пакет в новой папке root-project/packages/helpers
.
Добавим файл package.json
для нового пакета:
{
"name": "@my-blog/helpers",
"version": "0.0.1",
"type": "module",
"private": true
}
Создадим общий файл со сбросом стилей root-project/packages/helpers/styles/reset.css
:
*,
*::after,
*::before {
margin: 0;
padding: 0;
box-sizing: border-box;
}
Создадим общий файл глобальных стилей 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);
}
Выходим в корневой проект и устанавливаем наш новый пакет:
# мы были в AstroJS проекте, остановим его: CTRL + C
# выходим на уровень корневого проекта:
cd ../..
# теперь мы в корневом проекте, устанавливаем новый локальный пакет:
npm i
Результат: added 1 package, and audited 419 packages in 1s
Возвращаемся в папку пакета AstroJS и запускаем его снова:
cd packages/astrojs
npm run dev
Подключаем файлы стилей из пакета helpers
в 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="/favicon.svg" />
<title>{title}</title>
</head>
<body>
<slot />
</body>
</html>
Поздравляю! Теперь ваши компоненты отделены от стилей, которые будут одинаковы для всех проектов.
Выводы
После всех этих действий у нас monorepo для множества проектов и минимум зависимостей.
Проверим зависимости на данный момент (только необходимые для самого AstroJS): 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
Для удобства разработки можно настроить AstroJS проекты по моей инструкции.
В этом случае внешние пакеты устанавливаем в корневой проект:
- stylelint
- prettier
- eslint
- прочие печенюшки.
Если AstroJS проекту понадобятся зависимости типа "@astrojs/react", тоже устанавливаем их в корневой проект.
Часто задаваемые вопросы
ВОПРОС: Нужно ли публиковать наши пакеты в npm?
Публиковать в npm не нужно. Это локальные зависимости, которые живут в вашей же монорепе.
Если вы используете свою библиотеку компонентов на множестве проектов (не только для этого блога), то можете вынести ее как отдельный проект и публиковать ее в npm со своим собственным названием. Тогда в будущих проектах вы будете устанавливать уже внешний пакет из общего npm. Другие пользователи смогут увидеть и использовать вашу библиотеку компонентов в общем npm.
Примера такого решения: библиотека UI компонентов Яндекса. Они могли оставить ее как внутренний проект только в своей монорепе, но вынесли в публичный доступ.
ВОПРОС: Что добавляем в git?
В git идет корневой проект: git init
.
В проекте внутри git у вас будут все ваши локальные пакеты.
Не забудьте проверить, что у вас уходит в git из пакета с AstroJS: в .gitignore
надо запретить
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
ВОПРОС: какие действия, если устанавливаем уже созданную кем-то монорепу через npm workspaces?
В корневой папке делаем npm install
— установятся все зависимости, как внешние, так и локальные.
ВОПРОС: как работать сразу с несколькими пакетами, например, StrapiJS в качестве CMS и AstroJS как внешнюю часть?
В корневом проекте для package.json
добавляем команды:
{
"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"
}
}
}