Skip to Content
IntroductionGeneralArchitecture

Architecture

How we structured this repository using monorepos.

Monorepo Structure

This repository is organized as a monorepo, which includes various applications and packages that are developed, built, and published independently. We use Turborepo to manage our build system, which provides a high-performance environment for handling tasks across our JavaScript and TypeScript codebases.

Note: The root directory does not contain a src/ directory. Instead, individual projects—whether applications or packages—each have their own src/ directory within apps/ or packages/.

Each project has its own package.json file. We utilize pnpm Workspaces  to manage dependencies efficiently across all packages and applications.

Managing Dependencies in a Monorepo

Maintaining consistency across packages is essential to avoid subtle bugs and discrepancies. For example, mismatched TypeScript type definitions can cause hard-to-trace errors.

Our strategy is to create a consistent, stable, and predictable development environment for all packages in the monorepo. This is achieved by using explicit versioning for type declarations and crucial dependencies such as React.

Levels of package.json

  1. Root Level package.json:

    • Includes dependencies shared across multiple packages.
    • Houses essential development tools like Vite, Vitest, and @vitejs/plugin-react-swc required by several packages.
  2. Package Level package.json:

    • Contains dependencies specific to that package.
    • May include dependencies from the root package.json but with different versions or those unique to the package.
    • Uses the workspace: protocol to reference other packages within the monorepo.
    • Consumes development dependencies from the root without separate installations.

Dependency Types

  • dependencies: Required for the project to run, installed automatically with your package.
  • devDependencies: Needed only for development, not installed with your package.
  • peerDependencies: Indicates that your package can work only if the consumer also installs a specific version of this dependency.

Example of Dependency Specification

{ "name": "@akinon/some-package", "version": "1.0.0", "dependencies": { "antd": "^5.0.0" }, "peerDependencies": { "react": ">=18 || >=19", "react-dom": ">=18 || >=19" } }

Handling Specific Version Requirements

If a package needs a different version from the monorepo standard, it’s specified in its own package.json. This allows for flexibility and isolated version management when needed.

Caching with Turborepo

Turborepo includes a robust task caching mechanism to accelerate the build and test processes. It stores results and input hashes, reusing them if the inputs haven’t changed, which saves time.

Public vs. Private Packages

In our monorepo, you can manage both public and private packages. Public packages are published to npm, while private packages, marked with "private": true in their package.json, are not.

Referencing Packages

To reference a package within the monorepo, use its name rather than a relative path, ensuring correct version management:

{ "name": "@akinon/docs", "dependencies": { "@akinon/app-client": "workspace:*", "@akinon/icons": "workspace:*", "@akinon/ui-react": "workspace:*", ... } }

Using workspace:* ensures the latest version within the workspace is used. Specify a version number to pin a specific version.

Creating a New Package

To start a new package within our monorepo:

  1. Choose apps/ for applications or packages/ for libraries.
  2. Create a folder with a descriptive name reflecting its functionality.
  3. Run npm init to generate a package.json file.
  4. Add any internal dependencies using the workspace: protocol.
  5. Install external dependencies as required.
  6. Begin development in the src/ directory.

Naming Conventions

  • Scope: All packages are scoped under @akinon.
  • Descriptive Names: Ensure names clearly reflect the package’s purpose.
  • Kebab-Case: Use kebab-case for package names, e.g., @akinon/ui-components.

Using Turborepo

Turborepo is integral to our setup, functioning as a task runner with features like efficient caching and parallel task execution. It understands package dependencies, ensuring tasks run in the proper sequence.

turbo.json Configuration

Here’s a look at our turbo.json configuration:

{ "$schema": "https://turbo.build/schema.json", "globalDependencies": ["**/.env.*local"], "pipeline": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"], "env": ["NODE_ENV"] }, "typecheck": {}, "lint": { "outputs": [] }, "dev": { "cache": false }, "start": { "cache": false } } }
  • Pipeline: Defines the various tasks (e.g., build, test, lint) and their dependencies.
  • Depends On: Specifies the dependencies of a task. The ^ symbol indicates that the task depends on the build task of all its dependencies.
  • Outputs: Lists the file/directory patterns that should be cached after the task is executed.