This project uses a monorepo because:
-
Shared code — The SPA apps share UI components and utilities. With a monorepo, importing
@repo/shared-uiis as simple as a workspace dependency. No publishing, no version conflicts. -
Atomic changes — A change to a shared component and the apps that consume it can be reviewed and merged as a single pull request.
-
Unified CI — One pipeline definition handles validation and deployment for all apps. Change detection ensures only affected apps are processed.
-
Consistent tooling — TypeScript, ESLint, Vitest, and pnpm are configured once at the root and inherited by all packages.
The main site and the SPA apps are completely independent at runtime. They share a repository but have different:
- Technologies — The main site is static HTML/CSS/JS; SPAs are TypeScript + React
- Build processes — The main site has no build step; SPAs use Vite
- Deployment targets — Each app deploys to its own configurable remote path
- Dependencies — The main site has no Node.js dependencies; SPAs share
@repo/shared-uiand@repo/shared-utils
This independence is reflected in CI — changes to the main site never trigger SPA validation, and vice versa.
packages/shared-utils (pure TypeScript utilities, no dependencies)
│
▼
packages/shared-ui (React components, depends on shared-utils)
│
┌────┴────┐
▼ ▼
admin-spa portal-spa (both depend on shared-ui + shared-utils)
main-site (standalone, no shared package dependencies)
Shared packages (@repo/shared-ui, @repo/shared-utils) are consumed as raw TypeScript source — they have no build step.
Each package's package.json points main and types to src/index.ts. When a SPA app imports from @repo/shared-ui, pnpm's workspace protocol resolves the import to the package's source directory. Vite then transpiles the TypeScript during its normal transform pipeline.
For IDE support, each SPA's tsconfig.json uses TypeScript project references to point at the shared packages, enabling proper type-checking and editor navigation.
Uses dorny/paths-filter to detect which files changed:
| Filter | Paths | Triggers |
|---|---|---|
admin-spa |
apps/admin-spa/**, packages/** |
admin-spa validation + deploy |
portal-spa |
apps/portal-spa/**, packages/** |
portal-spa validation + deploy |
main-site |
apps/main-site/** |
main-site validation + deploy |
The packages/** glob is intentionally broad. A change to any shared package triggers both SPA apps. This is simpler and safer than tracking per-package dependencies, and the cost of an occasional unnecessary deploy (a cheap rsync) is much lower than the risk of missing one.
Uses native rules:changes with the same path patterns. Each job is gated by change rules — if no files match, the job is skipped entirely.
Only validation runs (lint, typecheck, test, build). No deployment.
Both validation and deployment run. The deploy jobs depend on successful validation.
All deployment uses rsync over SSH:
-
SPA apps: The
dist/directory (Vite build output) is rsync'd to the configured remote path. The--deleteflag ensures the remote directory exactly mirrors the build output. -
Main site: The
apps/main-site/directory is rsync'd to its configured remote path.
Both CI systems (GitHub Actions and GitLab CI) call the same bash scripts in scripts/, ensuring consistent behavior.