Hybrid Monorepo/Polyrepo Setup for Micro Frontends with React + TypeScript

Comprehensive Tooling and Patterns Research

SVG-HM-01: Hybrid Repo Strategy Overview -- Monorepo with radiating Polyrepos Core Monorepo design-system | shared-utils | types Turborepo + pnpm workspaces MFE Checkout polyrepo MFE Catalog polyrepo MFE Account polyrepo MFE Search polyrepo npm publish npm publish Renovate Renovate

Table of Contents

  1. Monorepo Tools Comparison for Core Packages
  2. Local Cross-Repo Development
  3. Design System Package Distribution
  4. TypeScript Project References
  5. CI/CD Patterns
  6. Recommended Package Manager
  7. Overall Recommended Architecture

1. Monorepo Tools Comparison for Core Packages

Architecture Context

Your monorepo contains: design system, shared utilities, shared types, and common configurations. This is a "library-focused" monorepo (not an "app monorepo"), which affects tool choice significantly.

Turborepo

SVG-HM-02: Turborepo Task Pipeline DAG -- lint, typecheck, build, test with caching Turborepo Task Pipeline (DAG) lint eslint typecheck tsc --noEmit build tsup / rspack test vitest Turbo Remote Cache -- cache hit ~0.2 s vs cold ~30 s

What it is: A high-performance build system for JavaScript/TypeScript monorepos by Vercel. Written in Rust (migrated from Go starting late 2023).

Strengths for your use case:

  • Minimal setup: add to an existing monorepo in under 10 minutes
  • Reads your package manager's workspace configuration (pnpm, npm, or yarn) and infers the dependency graph from package.json files
  • Remote caching via Vercel (or self-hosted) -- a cache hit takes ~0.2s vs ~30s for a cold build
  • Composable configuration (introduced in Turborepo 2.7, December 2025) allows reusable config snippets
  • Lightweight: does not impose opinions on project structure

Limitations:

  • Pure task runner -- no code generation, no dependency graph visualization, no project constraints enforcement
  • No built-in library publishing workflow
  • Less suited for enforcing architectural boundaries

turbo.json example for your setup:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "check-types": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["^build"]
    }
  }
}

Nx

What it is: A comprehensive build system and "Build Intelligence Platform" by Nrwl. Evolved significantly in 2025 with AI-driven CI/CD features.

Strengths for your use case:

  • Built-in dependency graph visualization (nx graph)
  • Powerful affected command: only builds/tests what changed
  • Built-in code generators for React libraries, components
  • Enforces module boundaries and architectural constraints via lint rules
  • Native Module Federation support with @nx/react/module-federation
  • First-class support for publishable libraries
  • Up to 7x faster than Turborepo in large monorepos (benchmark-dependent)

Limitations:

  • Steeper learning curve: Nx wants to manage your workspace its way
  • Heavier: more configuration, more concepts (executors, generators, targets)
  • Migration of an existing project is more complex than Turborepo
  • Can feel like overkill for a library-only monorepo with 4 packages

nx.json example:

{
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["{projectRoot}/dist"],
      "cache": true
    },
    "lint": {
      "cache": true
    },
    "test": {
      "cache": true
    }
  },
  "namedInputs": {
    "default": ["{projectRoot}/**/*"],
    "production": ["default", "!{projectRoot}/**/*.spec.ts"]
  }
}

pnpm Workspaces Alone

What it is: pnpm's built-in workspace feature for managing multiple packages in a single repository.

Strengths for your use case:

  • Zero additional tooling beyond pnpm itself
  • workspace:* protocol for local dependencies -- best-in-class local development ergonomics
  • Strict dependency resolution prevents phantom dependencies
  • Content-addressable store saves significant disk space

Limitations:

  • No task orchestration (builds run sequentially or you script parallelism yourself)
  • No caching (every build is a cold build)
  • No affected detection
  • No dependency graph visualization

pnpm-workspace.yaml:

packages:
  - "packages/*"

Recommendation for Your Setup

Use Turborepo + pnpm workspaces. Here is the reasoning:

  1. Your monorepo is library-focused (design system + shared libs), not an app monorepo. Turborepo's lightweight approach is ideal.
  2. Turborepo adds caching and task orchestration on top of pnpm workspaces without imposing structural opinions.
  3. The pnpm workspace:* protocol provides the best local development experience for inter-package references.
  4. Turborepo's remote caching dramatically speeds up CI.
  5. Nx would be the right choice if you had the micro frontends inside the monorepo (its Module Federation integration is excellent), but since your MFEs are in separate repos, Nx's advantages are diminished for the core packages monorepo.

Monorepo structure:

core-packages/
├── packages/
│   ├── design-system/        # React component library
│   │   ├── src/
│   │   ├── package.json      # @myorg/design-system
│   │   └── tsconfig.json
│   ├── shared-utils/          # Utility functions
│   │   ├── src/
│   │   ├── package.json      # @myorg/shared-utils
│   │   └── tsconfig.json
│   ├── shared-types/          # TypeScript type definitions
│   │   ├── src/
│   │   ├── package.json      # @myorg/shared-types
│   │   └── tsconfig.json
│   └── config/                # Shared configs (ESLint, TypeScript, Prettier)
│       ├── eslint/
│       ├── typescript/
│       └── prettier/
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
└── tsconfig.json              # Root tsconfig with project references

Root package.json:

{
  "name": "@myorg/core-packages",
  "private": true,
  "packageManager": "pnpm@9.15.0",
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "check-types": "turbo run check-types",
    "test": "turbo run test",
    "changeset": "changeset",
    "version-packages": "changeset version",
    "publish-packages": "turbo run build lint test && changeset publish"
  },
  "devDependencies": {
    "@changesets/cli": "^2.27.0",
    "turbo": "^2.3.0"
  }
}

2. Local Cross-Repo Development

This is the most critical challenge in your hybrid setup: how do developers work on a micro frontend while simultaneously iterating on the design system or shared utils?

How it works:

  • pnpm link <path-to-local-package> creates a symlink from the consuming project's node_modules to the local package directory
  • Changes in the linked package are immediately visible (no rebuild of the link required, but you do need to rebuild the package itself)

Configuration:

# In a micro frontend repo:
pnpm link /path/to/core-packages/packages/design-system
pnpm link /path/to/core-packages/packages/shared-utils

Limitations:

  • Symlinks can cause "dual package" issues: the linked package resolves its own node_modules from its original location, leading to duplicate React instances and the infamous "Invalid Hook Call" error
  • pnpm's strict node_modules structure makes linking across different pnpm workspaces particularly tricky
  • Does not simulate a real npm publish -- you may miss packaging issues
  • Breaks when the consuming project uses a different package manager than the source

Verdict: Not recommended as the primary approach for cross-repo development with React due to the duplicate dependency issue.

Approach 2: yalc

SVG-HM-03: Cross-Repo Development Flow -- yalc publish, add, test, push cycle yalc publish design-system/ yalc add mfe-checkout/ Local Testing pnpm dev yalc push auto-update all iterate & repeat ~/.yalc store

How it works:

  • yalc publish in the library project copies the built package files to a global store (~/.yalc)
  • yalc add @myorg/design-system in the consuming project copies files from the global store into the project and adds a file:.yalc/@myorg/design-system dependency to package.json
  • yalc push publishes AND updates all consuming projects in one step

Setup:

# Install globally
npm install -g yalc

# In the design system package (after building):
cd core-packages/packages/design-system
pnpm build
yalc publish

# In a micro frontend repo:
cd mfe-checkout
yalc add @myorg/design-system
yalc add @myorg/shared-utils

# When you make changes to the design system:
cd core-packages/packages/design-system
pnpm build && yalc push  # Automatically updates all consumers

Pros:

  • Simulates a real npm publish -- catches packaging issues early
  • No duplicate dependency problems (files are copied, not symlinked)
  • Works across different package managers
  • yalc push --watch can automate the push on changes

Cons:

  • Requires building the package before publishing to yalc (not instant)
  • Adds .yalc directory and modifies package.json / lockfile (must be gitignored)
  • Not maintained as actively as other tools (last significant update varies)
  • pnpm compatibility has had issues historically (though most are resolved)

Required .gitignore additions:

.yalc
yalc.lock

Verdict: Good middle-ground. Best for quick cross-repo iteration when you need realistic package resolution.

Approach 3: Verdaccio (Local npm Registry)

SVG-HM-05: Verdaccio Local Registry Flow -- publish, local registry, consume 1. npm publish core-packages/ Verdaccio localhost:4873 local npm registry 3. pnpm install mfe-checkout/ fallthrough to public npm

How it works:

  • Runs a full local npm registry on http://localhost:4873
  • You publish packages to it exactly as you would to npmjs.com
  • Consuming projects install from it using a scoped .npmrc configuration
  • Falls through to the public npm registry for packages not found locally

Setup:

# Install and start
npm install -g verdaccio
verdaccio  # Starts on http://localhost:4873

# Create a user (one-time)
npm adduser --registry http://localhost:4873

Project .npmrc (in each micro frontend repo):

@myorg:registry=http://localhost:4873

Publishing workflow:

# In design system package:
cd core-packages/packages/design-system
pnpm build
npm publish --registry http://localhost:4873

# In micro frontend:
pnpm install @myorg/design-system@latest

Docker setup for team-wide use:

# docker-compose.yml
services:
  verdaccio:
    image: verdaccio/verdaccio
    ports:
      - "4873:4873"
    volumes:
      - verdaccio-storage:/verdaccio/storage
      - ./verdaccio-config.yaml:/verdaccio/conf/config.yaml
volumes:
  verdaccio-storage:

Pros:

  • Most realistic simulation of the production workflow
  • Works with any package manager
  • Acts as a caching proxy for public npm (speeds up installs)
  • Teams can share a single Verdaccio instance
  • Can be used in CI for integration testing

Cons:

  • Requires running a separate service
  • More ceremony per publish cycle (version bump required each time)
  • Slower iteration loop compared to symlinks or yalc

Verdict: Best for team environments and CI integration testing. Overkill for individual developer iteration.

Approach 4: pnpm overrides / npm overrides

How it works:

  • Override any dependency resolution to point to a local file path
  • In pnpm, add overrides to the root package.json or pnpm-workspace.yaml

Configuration (package.json in micro frontend):

{
  "pnpm": {
    "overrides": {
      "@myorg/design-system": "file:../core-packages/packages/design-system",
      "@myorg/shared-utils": "file:../core-packages/packages/shared-utils"
    }
  }
}

Important caveat: This bakes absolute paths into pnpm-lock.yaml, which causes problems:

  • Lock files differ between developers' machines
  • Cannot be committed to version control reliably
  • There is an open feature request (May 2025) for "local-only overrides" in pnpm, but it is not yet implemented

Workaround -- use a local-only overrides script:

#!/bin/bash
# scripts/link-local.sh -- DO NOT COMMIT the modified package.json
CORE_PATH="${CORE_PACKAGES_PATH:-../core-packages}"

# Temporarily add overrides
node -e "
const pkg = require('./package.json');
pkg.pnpm = pkg.pnpm || {};
pkg.pnpm.overrides = {
  '@myorg/design-system': 'file:${CORE_PATH}/packages/design-system',
  '@myorg/shared-utils': 'file:${CORE_PATH}/packages/shared-utils',
  '@myorg/shared-types': 'file:${CORE_PATH}/packages/shared-types'
};
require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2));
"

pnpm install
echo "Local overrides applied. Run 'git checkout package.json pnpm-lock.yaml' to revert."

Verdict: Useful as a quick hack but not suitable as a team-wide workflow due to lock file contamination.

Approach 5: Module Federation for Local Development

How it works:

  • The design system is exposed as a Module Federation remote
  • Micro frontends consume it at runtime, not at build time
  • For local development, you point the remote URL to localhost

Configuration in the design system (remote):

// packages/design-system/rsbuild.config.ts (or webpack.config.js)
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';

export default {
  plugins: [
    new ModuleFederationPlugin({
      name: 'design_system',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/components/Button',
        './Input': './src/components/Input',
        './Card': './src/components/Card',
        './theme': './src/theme',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.2.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
      },
    }),
  ],
};

Configuration in a micro frontend (host/consumer):

// mfe-checkout/rsbuild.config.ts
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';

export default {
  plugins: [
    new ModuleFederationPlugin({
      name: 'mfe_checkout',
      remotes: {
        design_system: process.env.NODE_ENV === 'development'
          ? 'design_system@http://localhost:3001/remoteEntry.js'
          : 'design_system@https://cdn.myorg.com/design-system/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.2.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
      },
    }),
  ],
};

Usage in micro frontend code:

// Dynamic import from the federated module
const Button = React.lazy(() => import('design_system/Button'));

// Or with typed wrapper (recommended):
import type { ButtonProps } from '@myorg/design-system'; // types from npm
const Button = React.lazy(() => import('design_system/Button'));

Verdict: This is the most powerful approach for your architecture. It eliminates the need for linking/publishing during development entirely. See Section 3 for the full Module Federation vs npm package distribution analysis.

Use a layered approach:

ScenarioToolWhy
Daily development (design system + MFE)Module Federation (dev server)Instant feedback, no build/link step
Testing packaging/exportsyalcCatches real packaging issues
Team-wide integration testingVerdaccio (Docker)Realistic registry simulation
CI integration testsVerdaccio or GitHub PackagesFull production-like flow
ProductionGitHub Packages (npm) + Module Federation (runtime)See Section 3

3. Design System Package Distribution

Package Structure

packages/design-system/
├── src/
│   ├── components/
│   │   ├── Button/
│   │   │   ├── Button.tsx
│   │   │   ├── Button.styles.ts    # or Button.module.css
│   │   │   ├── Button.test.tsx
│   │   │   └── index.ts
│   │   ├── Input/
│   │   ├── Card/
│   │   └── ...
│   ├── hooks/
│   │   ├── useTheme.ts
│   │   └── index.ts
│   ├── theme/
│   │   ├── tokens.ts
│   │   ├── ThemeProvider.tsx
│   │   └── index.ts
│   ├── utils/
│   │   └── index.ts
│   └── index.ts                    # Main barrel export
├── tsup.config.ts
├── package.json
├── tsconfig.json
└── CHANGELOG.md

Build Configuration with tsup

tsup (built on esbuild) is the recommended build tool for React component libraries due to its speed, simplicity, and tree-shaking support.

tsup.config.ts:

import { defineConfig } from 'tsup';

export default defineConfig({
  entry: {
    index: 'src/index.ts',
    // Granular entry points for better tree-shaking:
    'components/Button': 'src/components/Button/index.ts',
    'components/Input': 'src/components/Input/index.ts',
    'components/Card': 'src/components/Card/index.ts',
    'hooks/index': 'src/hooks/index.ts',
    'theme/index': 'src/theme/index.ts',
  },
  format: ['esm', 'cjs'],
  dts: true,
  splitting: true,
  clean: true,
  treeshake: true,
  sourcemap: true,
  external: ['react', 'react-dom'],    // Peer dependencies
  minify: false,                        // Let consumers handle minification
  target: 'es2020',
});

package.json for Distribution

{
  "name": "@myorg/design-system",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": {
        "types": "./dist/index.d.ts",
        "default": "./dist/index.js"
      },
      "require": {
        "types": "./dist/index.d.cts",
        "default": "./dist/index.cjs"
      }
    },
    "./components/*": {
      "import": {
        "types": "./dist/components/*/index.d.ts",
        "default": "./dist/components/*/index.js"
      },
      "require": {
        "types": "./dist/components/*/index.d.cts",
        "default": "./dist/components/*/index.cjs"
      }
    },
    "./hooks": {
      "import": {
        "types": "./dist/hooks/index.d.ts",
        "default": "./dist/hooks/index.js"
      }
    },
    "./theme": {
      "import": {
        "types": "./dist/theme/index.d.ts",
        "default": "./dist/theme/index.js"
      }
    }
  },
  "sideEffects": false,
  "files": ["dist"],
  "peerDependencies": {
    "react": "^18.2.0 || ^19.0.0",
    "react-dom": "^18.2.0 || ^19.0.0"
  },
  "publishConfig": {
    "registry": "https://npm.pkg.github.com",
    "access": "restricted"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/myorg/core-packages.git",
    "directory": "packages/design-system"
  },
  "scripts": {
    "build": "tsup",
    "dev": "tsup --watch",
    "lint": "eslint src/",
    "check-types": "tsc --noEmit",
    "test": "vitest run"
  }
}

Tree-Shaking Considerations

  1. "sideEffects": false -- This tells bundlers that any unused exports can be safely eliminated. If you have CSS imports or global styles, list them explicitly:
{
  "sideEffects": ["**/*.css", "**/*.scss"]
}
  1. ESM output is mandatory -- Tree-shaking only works reliably with ES modules. The "type": "module" and "module" field ensure bundlers use the ESM version.

  2. Multiple entry points -- The exports map with granular paths allows consumers to import only what they need:

// Imports only Button code, not the entire design system:
import { Button } from '@myorg/design-system/components/Button';
  1. External peer dependencies -- React and ReactDOM must be external in tsup config to avoid bundling them into the library.

  2. Avoid barrel file re-exports of entire modules -- Each component should have its own entry point. A single index.ts that re-exports everything defeats tree-shaking in some bundlers.

Versioning Strategy

Recommendation: Use Changesets with independent versioning.

.changeset/config.json:

{
  "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [],
  "linked": [],
  "access": "restricted",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": []
}

Workflow:

# Developer makes changes, then:
pnpm changeset
# Interactive prompt: select packages, bump type, write summary

# CI or release manager runs:
pnpm changeset version    # Updates versions + changelogs
pnpm changeset publish    # Publishes to registry

# Or combine with Turborepo:
pnpm publish-packages     # build + lint + test + publish

Why independent versioning (not fixed):

  • @myorg/shared-types changes far more frequently than @myorg/design-system
  • Fixed versioning forces ALL packages to bump on every change, causing unnecessary updates in consuming repos
  • Independent versioning lets micro frontends update only the packages they actually depend on

Publishing to a Private npm Registry

Recommendation: GitHub Packages.

Why GitHub Packages over alternatives:

  • Native integration with GitHub Actions (authentication via GITHUB_TOKEN)
  • Access control mirrors repository permissions
  • Scoped to your GitHub organization
  • No additional service to manage
  • Free for private packages within GitHub plans

Setup:

  1. Root .npmrc in the monorepo:
@myorg:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
  1. Each package's package.json must include:
{
  "name": "@myorg/design-system",
  "publishConfig": {
    "registry": "https://npm.pkg.github.com"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/myorg/core-packages.git"
  }
}
  1. In each micro frontend's .npmrc:
@myorg:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}

Cloudflare alternative: Cloudflare does not offer a native npm registry service. If you want a self-hosted alternative to GitHub Packages, Verdaccio on Cloudflare Workers or a similar edge deployment could work, but it is significantly more effort for little benefit over GitHub Packages.

npm Package vs Module Federation: When to Use Each

Dual distribution strategy (recommended):

SVG-HM-04: Design System Dual Distribution -- npm package (build-time) and MF remote (runtime) Design System: Dual Distribution @myorg/design-system source of truth npm Package (Build-Time) TypeScript types | tree-shaking | semver shared-types, shared-utils, DS types MF Remote (Runtime) UI components | independent deploy Button, Input, Card, ThemeProvider tsup build rspack bundle
Aspectnpm PackageModule Federation Remote
When consumedBuild timeRuntime
TypeScript typesFull type safety via .d.tsRequires separate type package or @module-federation/typescript
VersioningExplicit (semver in package.json)Implicit (whatever is deployed)
Offline/CI buildsWorks without networkRequires remote to be available
Tree-shakingExcellent (with ESM)Limited (loads exposed modules)
Independent deploymentNo (requires rebuild)Yes (update remote, consumers get changes)
Best forShared types, utilities, theme tokensUI components, feature modules

Recommended approach:

  • Publish @myorg/shared-types and @myorg/shared-utils as npm packages only (they are consumed at build time and tree-shake well)
  • Publish @myorg/design-system as both npm package AND Module Federation remote:
    • npm package: for TypeScript types, IDE autocompletion, and as a fallback
    • Module Federation remote: for runtime consumption with independent deployment
  • Use @myorg/config packages as npm packages only (dev dependencies)

Module Federation shared configuration for the design system:

// Design system federation config
{
  name: 'design_system',
  filename: 'remoteEntry.js',
  exposes: {
    './Button': './src/components/Button',
    './Input': './src/components/Input',
    './Card': './src/components/Card',
    './ThemeProvider': './src/theme/ThemeProvider',
  },
  shared: {
    react: {
      singleton: true,
      requiredVersion: '^18.2.0',
      eager: false,         // Do NOT set eager:true -- it bloats the entry
    },
    'react-dom': {
      singleton: true,
      requiredVersion: '^18.2.0',
      eager: false,
    },
    // Share the design system's theme tokens as a singleton:
    '@myorg/shared-utils': {
      singleton: true,
      requiredVersion: '^1.0.0',
    },
  },
}

4. TypeScript Project References

How TypeScript Project References Work

TypeScript project references allow the compiler to understand your monorepo as a series of connected projects ("islands") rather than one monolithic unit. Each package is compiled independently, with explicit dependency declarations.

Key compiler options required:

OptionPurpose
composite: trueEnables project reference mode; implies declaration and incremental
declaration: trueGenerates .d.ts files (required for cross-project type resolution)
declarationMap: trueGenerates sourcemaps for declarations (enables "Go to Definition" across packages)
incremental: trueCaches compilation results in tsconfig.tsbuildinfo

Configuration for the Monorepo

Shared base config -- packages/config/typescript/base.json:

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "forceConsistentCasingInFileNames": true,
    "noUncheckedIndexedAccess": true
  }
}

Shared React library config -- packages/config/typescript/react-library.json:

{
  "extends": "./base.json",
  "compilerOptions": {
    "jsx": "react-jsx",
    "lib": ["DOM", "DOM.Iterable", "ES2020"],
    "composite": true
  }
}

Root tsconfig.json (project references):

{
  "files": [],
  "references": [
    { "path": "packages/shared-types" },
    { "path": "packages/shared-utils" },
    { "path": "packages/design-system" }
  ]
}

packages/shared-types/tsconfig.json:

{
  "extends": "../config/typescript/base.json",
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"]
}

packages/shared-utils/tsconfig.json:

{
  "extends": "../config/typescript/base.json",
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"],
  "references": [
    { "path": "../shared-types" }
  ]
}

packages/design-system/tsconfig.json:

{
  "extends": "../config/typescript/react-library.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"],
  "references": [
    { "path": "../shared-types" },
    { "path": "../shared-utils" }
  ]
}

Building with Project References

# Build all projects in dependency order:
tsc --build

# Build a specific project and its dependencies:
tsc --build packages/design-system

# Clean build artifacts:
tsc --build --clean

In turbo.json, the check-types task already handles this via the dependsOn: ["^build"] configuration, ensuring packages are built before their dependents are type-checked.

tsconfig paths for Local Development (within the monorepo)

Path aliases in the root tsconfig.json enable IDE features (autocompletion, go-to-definition) without requiring a build step:

{
  "compilerOptions": {
    "paths": {
      "@myorg/shared-types": ["./packages/shared-types/src"],
      "@myorg/shared-types/*": ["./packages/shared-types/src/*"],
      "@myorg/shared-utils": ["./packages/shared-utils/src"],
      "@myorg/shared-utils/*": ["./packages/shared-utils/src/*"],
      "@myorg/design-system": ["./packages/design-system/src"],
      "@myorg/design-system/*": ["./packages/design-system/src/*"]
    }
  }
}

Important: These paths are for development only. The actual module resolution for consumers uses the exports field in package.json. The pnpm workspace:* protocol resolves the dependency, and the exports map points to the built output.

Type Safety for External Repos (Micro Frontends)

When micro frontends consume the packages from the npm registry, type safety comes from the .d.ts files published alongside the JavaScript:

  1. tsup generates .d.ts files from the TypeScript source
  2. The "types" and "exports" fields in package.json point to these declarations
  3. Consumers get full type checking, autocompletion, and go-to-definition (via declarationMap)

For Module Federation types, use @module-federation/typescript:

pnpm add -D @module-federation/typescript

This plugin generates type declarations for federated modules and makes them available to consuming hosts. Alternatively, maintain a @myorg/design-system-types package that exports only the TypeScript interfaces and types, consumed as an npm dependency even when the components themselves come via Module Federation.

A pragmatic approach is to have micro frontends depend on @myorg/design-system as a devDependency (for types only) while consuming components at runtime via Module Federation:

{
  "devDependencies": {
    "@myorg/design-system": "^2.0.0"
  }
}
// Types come from the npm package (build time):
import type { ButtonProps } from '@myorg/design-system';

// Components come from Module Federation (runtime):
const Button = React.lazy(() => import('design_system/Button'));

5. CI/CD Patterns

Monorepo CI/CD (Core Packages)

GitHub Actions workflow for the monorepo:

.github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile

      - run: pnpm build
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

      - run: pnpm lint

      - run: pnpm check-types

      - run: pnpm test

.github/workflows/release.yml:

name: Release

on:
  push:
    branches: [main]

permissions:
  contents: write
  packages: write
  pull-requests: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
          registry-url: 'https://npm.pkg.github.com'
          scope: '@myorg'

      - run: pnpm install --frozen-lockfile

      - run: pnpm build
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

      - name: Create Release Pull Request or Publish
        id: changesets
        uses: changesets/action@v1
        with:
          publish: pnpm publish-packages
          version: pnpm changeset version
          title: 'chore: version packages'
          commit: 'chore: version packages'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }}

How this workflow operates:

  1. On every push to main, the Changesets action checks if there are pending changesets.
  2. If there are changesets, it creates a "Version Packages" PR that bumps versions and updates changelogs.
  3. When that PR is merged, the action detects the version bumps and runs pnpm publish-packages, which builds, tests, and publishes to GitHub Packages.

Deploy Design System Module Federation Remote

If you serve the design system as a Module Federation remote, add a deployment step:

      - name: Deploy Module Federation Remote
        if: steps.changesets.outputs.published == 'true'
        run: |
          # Build the MF remote bundle
          cd packages/design-system
          pnpm build:federation

          # Deploy to CDN (example: Cloudflare R2 or AWS S3)
          aws s3 sync dist/federation/ s3://my-cdn-bucket/design-system/ \
            --cache-control "public, max-age=31536000, immutable"

          # Invalidate CDN cache
          aws cloudfront create-invalidation \
            --distribution-id ${{ secrets.CF_DISTRIBUTION_ID }} \
            --paths "/design-system/remoteEntry.js"

Micro Frontend Repos: Consuming New Versions

Automated dependency updates with Renovate (recommended over Dependabot):

Why Renovate:

  • Supports 90+ package managers (vs Dependabot's 30+)
  • Groups monorepo package updates into a single PR (group:monorepos preset)
  • Shared config presets standardize dependency policies across all repos
  • Built-in automerge with configurable rules
  • Regex managers for updating versions in any file format
  • Works across GitHub, GitLab, Bitbucket, Azure DevOps

Shared Renovate config (in a separate repo or gist):

renovate-config/default.json:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended",
    "group:monorepos",
    ":automergeMinor",
    ":automergeDigest"
  ],
  "packageRules": [
    {
      "description": "Auto-merge patch updates from our design system",
      "matchPackagePatterns": ["@myorg/*"],
      "matchUpdateTypes": ["patch"],
      "automerge": true,
      "automergeType": "pr",
      "platformAutomerge": true
    },
    {
      "description": "Group all @myorg packages together",
      "matchPackagePatterns": ["@myorg/*"],
      "groupName": "myorg core packages"
    },
    {
      "description": "Require manual review for major updates",
      "matchPackagePatterns": ["@myorg/*"],
      "matchUpdateTypes": ["major"],
      "automerge": false,
      "reviewers": ["team:frontend-platform"]
    }
  ],
  "registryAliases": {
    "npm": "https://npm.pkg.github.com"
  },
  "npmrc": "@myorg:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=${RENOVATE_TOKEN}"
}

In each micro frontend repo, renovate.json:

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["github>myorg/renovate-config"]
}

Alternative: GitHub Actions webhook-triggered updates.

When the monorepo publishes new versions, trigger updates across micro frontend repos:

.github/workflows/notify-consumers.yml (in monorepo):

name: Notify Consumers

on:
  workflow_run:
    workflows: [Release]
    types: [completed]

jobs:
  notify:
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    runs-on: ubuntu-latest
    strategy:
      matrix:
        repo:
          - myorg/mfe-checkout
          - myorg/mfe-catalog
          - myorg/mfe-account
          - myorg/mfe-search
          - myorg/mfe-admin
    steps:
      - name: Trigger dependency update
        uses: peter-evans/repository-dispatch@v3
        with:
          token: ${{ secrets.CROSS_REPO_PAT }}
          repository: ${{ matrix.repo }}
          event-type: core-packages-updated
          client-payload: '{"ref": "${{ github.ref }}"}'

In each micro frontend repo, .github/workflows/update-deps.yml:

name: Update Core Packages

on:
  repository_dispatch:
    types: [core-packages-updated]

permissions:
  contents: write
  pull-requests: write

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: 'https://npm.pkg.github.com'
          scope: '@myorg'

      - run: pnpm update "@myorg/*" --latest
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Create Pull Request
        uses: peter-evans/create-pull-request@v6
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          commit-message: 'chore: update @myorg core packages'
          title: 'chore: update @myorg core packages'
          body: |
            Automated update triggered by new release of core packages.

            Please verify:
            - [ ] Design system components render correctly
            - [ ] No TypeScript errors
            - [ ] Tests pass
          branch: chore/update-core-packages
          delete-branch: true

Micro Frontend CI Pipeline

.github/workflows/ci.yml (in each MFE repo):

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
          registry-url: 'https://npm.pkg.github.com'
          scope: '@myorg'

      - run: pnpm install --frozen-lockfile
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - run: pnpm lint
      - run: pnpm check-types
      - run: pnpm test
      - run: pnpm build

  deploy:
    needs: ci
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # ... deploy micro frontend to CDN/hosting

Comparison Matrix

Featurepnpmyarn (v4 Berry)npm (v10)
Disk efficiencyContent-addressable store (best)PnP or node_modulesStandard node_modules
Install speedFastestFast with PnPSlowest
Strict depsEnforced by defaultOptional (PnP)Not enforced
Workspace protocolworkspace:* (best)workspace:*Supported (basic)
Cross-repo linkingpnpm link (strict)yarn linknpm link
Module Federation compatExcellentPnP has issuesGood
Turborepo integrationFirst-classFirst-classFirst-class
Monorepo supportNative workspacesNative workspacesNative workspaces
overrides / resolutionspnpm.overridesresolutionsoverrides

Recommendation: pnpm

pnpm is the clear choice for your setup. Here is why:

  1. Content-addressable store: Across 5-10 micro frontend repos + the monorepo, pnpm's global store prevents duplicate downloads. A single copy of react@18.2.0 is shared across all projects.

  2. Strict dependency resolution: Prevents phantom dependencies (importing packages not declared in package.json), which is critical when your packages will be consumed by other repos that may have different dependency trees.

  3. workspace:* protocol: The best local development experience within the monorepo. When you publish, workspace:* is automatically replaced with the actual version number.

  4. Module Federation compatibility: pnpm's node_modules structure (symlinks into .pnpm store) works well with webpack and rspack module resolution. Unlike Yarn PnP, there are no known compatibility issues with Module Federation.

  5. Turborepo integration: Turborepo reads pnpm workspace configuration natively and understands the dependency graph.

  6. Cross-repo linking: While pnpm link has quirks, the combination with yalc or Module Federation dev servers covers all cross-repo scenarios.

  7. Performance at scale: With 5-10 micro frontend repos, pnpm's install speed advantage compounds significantly.

pnpm version pin in the monorepo:

{
  "packageManager": "pnpm@9.15.0",
  "engines": {
    "node": ">=20.0.0",
    "pnpm": ">=9.0.0"
  }
}

Use corepack to ensure consistent pnpm versions:

corepack enable
corepack prepare pnpm@9.15.0 --activate

Summary of Tool Choices

ConcernToolAlternatives Considered
Package managerpnpmyarn, npm
Monorepo orchestrationTurborepoNx, pnpm alone
Package buildingtsupRollup, Vite library mode
Versioning/publishingChangesetsLerna, manual
Private registryGitHub PackagesVerdaccio, npm Enterprise
Cross-repo local devModule Federation dev + yalcpnpm link, Verdaccio
Module Federation runtime@module-federation/enhanced (Rspack/webpack)Vite plugin
Automated dep updatesRenovateDependabot
TypeScript configProject references + shared base configspaths-only
CI/CDGitHub Actions + Turborepo remote cacheCircleCI, Jenkins

Developer Workflow (Day-to-Day)

1. Clone both repos:
   git clone myorg/core-packages
   git clone myorg/mfe-checkout

2. Start the design system dev server (Module Federation remote):
   cd core-packages && pnpm dev
   # Design system serves remoteEntry.js at localhost:3001

3. Start the micro frontend (Module Federation host):
   cd mfe-checkout && pnpm dev
   # MFE consumes design system from localhost:3001

4. Make changes to the design system:
   # Edit packages/design-system/src/components/Button/Button.tsx
   # Hot reload propagates through Module Federation -- no linking needed

5. When ready to share with the team:
   pnpm changeset              # Declare the change
   git commit && git push      # PR and review
   # On merge: CI builds, versions, and publishes automatically
   # Renovate creates update PRs in all micro frontend repos

Production Architecture

SVG-HM-06: Production Architecture -- CDN, Workers, R2, KV serving micro frontends CDN Edge (Cloudflare) Workers routing + SSR R2 Storage MF remotes + static KV Store config + feature flags GitHub Packages npm (build-time) MFE Host shell app MFE Checkout remote MFE Catalog remote MFE Account remote MFE ...N remote runtime (MF remote) build-time (npm pkg)

Key Principles

  1. Types at build time, components at runtime: Use npm packages for TypeScript types and utilities. Use Module Federation for UI components that benefit from independent deployment.

  2. Automate everything across repos: Changesets for versioning, Renovate for dependency propagation, GitHub Actions for CI/CD. Minimize manual coordination.

  3. Strict boundaries: pnpm's strict resolution + TypeScript project references + clear package boundaries prevent dependency chaos.

  4. Cache aggressively: Turborepo remote cache for the monorepo, pnpm's content-addressable store for installations, CDN caching for Module Federation remotes.

  5. Dual development modes: Module Federation for daily cross-repo development (instant feedback), yalc for testing real packaging behavior before releases.


Sources