AstroJS projects in monorepo using npm workspaces

image

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:

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:

In each project we will include:

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:

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:

After installation, a file appears in the package folder package.json and the files required for AstroJS.

Editing the file package.json:

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:

image

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.

image

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

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *