image


Иногда я делаю сразу несколько похожих подпроектов в одном проекте, например, набор статичных лендингов для сбора заявок и блог на 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

image


Подключение библиотеки компонентов


Из корневого проекта переходим в наш 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 для множества проектов и минимум зависимостей.


image


Проверим зависимости на данный момент (только необходимые для самого 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"
    }
  }
}

Комментарии (0)