feat(#1): Set up monorepo scaffold with pnpm workspaces + TurboRepo

Implements the foundational project structure including:
- pnpm workspaces configuration
- TurboRepo for build orchestration
- NestJS 11.1.12 API (apps/api)
- Next.js 16.1.6 web app (apps/web)
- Shared packages (config, shared, ui)
- TypeScript strict mode configuration
- ESLint + Prettier setup
- Vitest for unit testing (19 passing tests)

Fixes #1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-01-28 13:31:33 -06:00
commit 92e20b1686
109 changed files with 8320 additions and 0 deletions

18
apps/api/.swcrc Normal file
View File

@@ -0,0 +1,18 @@
{
"sourceMaps": true,
"jsc": {
"target": "es2022",
"parser": {
"syntax": "typescript",
"decorators": true
},
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"keepClassNames": true
},
"module": {
"type": "es6"
}
}

8
apps/api/nest-cli.json Normal file
View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

40
apps/api/package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "@mosaic/api",
"version": "0.0.1",
"private": true,
"scripts": {
"build": "nest build",
"dev": "nest start --watch",
"start": "node dist/main",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"src/**/*.ts\"",
"lint:fix": "eslint \"src/**/*.ts\" --fix",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:e2e": "vitest run --config ./vitest.e2e.config.ts"
},
"dependencies": {
"@nestjs/common": "^11.1.12",
"@nestjs/core": "^11.1.12",
"@nestjs/platform-express": "^11.1.12",
"@mosaic/shared": "workspace:*",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@mosaic/config": "workspace:*",
"@nestjs/cli": "^11.0.6",
"@nestjs/schematics": "^11.0.1",
"@nestjs/testing": "^11.1.12",
"@swc/core": "^1.10.18",
"@types/express": "^5.0.1",
"@types/node": "^22.13.4",
"typescript": "^5.8.2",
"unplugin-swc": "^1.5.2",
"vitest": "^3.0.8"
}
}

14
apps/api/src/app.controller.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
import { AppService } from "./app.service";
import type { ApiResponse } from "@mosaic/shared/types";
interface HealthStatus {
status: string;
timestamp: string;
}
export declare class AppController {
private readonly appService;
constructor(appService: AppService);
getHello(): string;
getHealth(): ApiResponse<HealthStatus>;
}
export {};
//# sourceMappingURL=app.controller.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"app.controller.d.ts","sourceRoot":"","sources":["app.controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,UAAU,YAAY;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qBACa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAAV,UAAU,EAAE,UAAU;IAGnD,QAAQ,IAAI,MAAM;IAKlB,SAAS,IAAI,WAAW,CAAC,YAAY,CAAC;CASvC"}

View File

@@ -0,0 +1,50 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppController = void 0;
const common_1 = require("@nestjs/common");
const app_service_1 = require("./app.service");
let AppController = class AppController {
appService;
constructor(appService) {
this.appService = appService;
}
getHello() {
return this.appService.getHello();
}
getHealth() {
return {
success: true,
data: {
status: "healthy",
timestamp: new Date().toISOString(),
},
};
}
};
exports.AppController = AppController;
__decorate([
(0, common_1.Get)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", String)
], AppController.prototype, "getHello", null);
__decorate([
(0, common_1.Get)("health"),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Object)
], AppController.prototype, "getHealth", null);
exports.AppController = AppController = __decorate([
(0, common_1.Controller)(),
__metadata("design:paramtypes", [app_service_1.AppService])
], AppController);
//# sourceMappingURL=app.controller.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"app.controller.js","sourceRoot":"","sources":["app.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAiD;AACjD,+CAA2C;AASpC,IAAM,aAAa,GAAnB,MAAM,aAAa;IACK;IAA7B,YAA6B,UAAsB;QAAtB,eAAU,GAAV,UAAU,CAAY;IAAG,CAAC;IAGvD,QAAQ;QACN,OAAO,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;IACpC,CAAC;IAGD,SAAS;QACP,OAAO;YACL,OAAO,EAAE,IAAI;YACb,IAAI,EAAE;gBACJ,MAAM,EAAE,SAAS;gBACjB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aACpC;SACF,CAAC;IACJ,CAAC;CACF,CAAA;AAlBY,sCAAa;AAIxB;IADC,IAAA,YAAG,GAAE;;;;6CAGL;AAGD;IADC,IAAA,YAAG,EAAC,QAAQ,CAAC;;;;8CASb;wBAjBU,aAAa;IADzB,IAAA,mBAAU,GAAE;qCAE8B,wBAAU;GADxC,aAAa,CAkBzB"}

View File

@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { AppService } from "./app.service";
import { AppController } from "./app.controller";
describe("AppController", () => {
const appService = new AppService();
const controller = new AppController(appService);
describe("getHello", () => {
it('should return "Mosaic Stack API"', () => {
expect(controller.getHello()).toBe("Mosaic Stack API");
});
});
describe("getHealth", () => {
it("should return health status", () => {
const result = controller.getHealth();
expect(result.success).toBe(true);
expect(result.data.status).toBe("healthy");
expect(result.data.timestamp).toBeDefined();
});
});
});
describe("AppService", () => {
const service = new AppService();
it('should return "Mosaic Stack API"', () => {
expect(service.getHello()).toBe("Mosaic Stack API");
});
});

View File

@@ -0,0 +1,29 @@
import { Controller, Get } from "@nestjs/common";
import { AppService } from "./app.service";
import type { ApiResponse } from "@mosaic/shared";
interface HealthStatus {
status: string;
timestamp: string;
}
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@Get("health")
getHealth(): ApiResponse<HealthStatus> {
return {
success: true,
data: {
status: "healthy",
timestamp: new Date().toISOString(),
},
};
}
}

3
apps/api/src/app.module.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
export declare class AppModule {
}
//# sourceMappingURL=app.module.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"app.module.d.ts","sourceRoot":"","sources":["app.module.ts"],"names":[],"mappings":"AAIA,qBAKa,SAAS;CAAG"}

View File

@@ -0,0 +1,23 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppModule = void 0;
const common_1 = require("@nestjs/common");
const app_controller_1 = require("./app.controller");
const app_service_1 = require("./app.service");
let AppModule = class AppModule {
};
exports.AppModule = AppModule;
exports.AppModule = AppModule = __decorate([
(0, common_1.Module)({
imports: [],
controllers: [app_controller_1.AppController],
providers: [app_service_1.AppService],
})
], AppModule);
//# sourceMappingURL=app.module.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,qDAAiD;AACjD,+CAA2C;AAOpC,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IALrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,EAAE;QACX,WAAW,EAAE,CAAC,8BAAa,CAAC;QAC5B,SAAS,EAAE,CAAC,wBAAU,CAAC;KACxB,CAAC;GACW,SAAS,CAAG"}

View File

@@ -0,0 +1,10 @@
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

4
apps/api/src/app.service.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
export declare class AppService {
getHello(): string;
}
//# sourceMappingURL=app.service.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"app.service.d.ts","sourceRoot":"","sources":["app.service.ts"],"names":[],"mappings":"AAEA,qBACa,UAAU;IACrB,QAAQ,IAAI,MAAM;CAGnB"}

View File

@@ -0,0 +1,20 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppService = void 0;
const common_1 = require("@nestjs/common");
let AppService = class AppService {
getHello() {
return "Mosaic Stack API";
}
};
exports.AppService = AppService;
exports.AppService = AppService = __decorate([
(0, common_1.Injectable)()
], AppService);
//# sourceMappingURL=app.service.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"app.service.js","sourceRoot":"","sources":["app.service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA4C;AAGrC,IAAM,UAAU,GAAhB,MAAM,UAAU;IACrB,QAAQ;QACN,OAAO,kBAAkB,CAAC;IAC5B,CAAC;CACF,CAAA;AAJY,gCAAU;qBAAV,UAAU;IADtB,IAAA,mBAAU,GAAE;GACA,UAAU,CAItB"}

View File

@@ -0,0 +1,8 @@
import { Injectable } from "@nestjs/common";
@Injectable()
export class AppService {
getHello(): string {
return "Mosaic Stack API";
}
}

2
apps/api/src/main.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=main.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["main.ts"],"names":[],"mappings":""}

16
apps/api/src/main.js Normal file
View File

@@ -0,0 +1,16 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@nestjs/core");
const app_module_1 = require("./app.module");
async function bootstrap() {
const app = await core_1.NestFactory.create(app_module_1.AppModule);
app.enableCors();
const port = process.env["PORT"] ?? 3001;
await app.listen(port);
console.log(`API running on http://localhost:${port}`);
}
bootstrap().catch((err) => {
console.error("Failed to start application:", err);
process.exit(1);
});
//# sourceMappingURL=main.js.map

1
apps/api/src/main.js.map Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"main.js","sourceRoot":"","sources":["main.ts"],"names":[],"mappings":";;AAAA,uCAA2C;AAC3C,6CAAyC;AAEzC,KAAK,UAAU,SAAS;IACtB,MAAM,GAAG,GAAG,MAAM,kBAAW,CAAC,MAAM,CAAC,sBAAS,CAAC,CAAC;IAChD,GAAG,CAAC,UAAU,EAAE,CAAC;IAEjB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC;IACzC,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAEvB,OAAO,CAAC,GAAG,CAAC,mCAAmC,IAAI,EAAE,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;IACjC,OAAO,CAAC,KAAK,CAAC,8BAA8B,EAAE,GAAG,CAAC,CAAC;IACnD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}

17
apps/api/src/main.ts Normal file
View File

@@ -0,0 +1,17 @@
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors();
const port = process.env["PORT"] ?? 3001;
await app.listen(port);
console.log(`API running on http://localhost:${port}`);
}
bootstrap().catch((err: unknown) => {
console.error("Failed to start application:", err);
process.exit(1);
});

13
apps/api/tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"extends": "@mosaic/config/typescript/nestjs",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
}

27
apps/api/vitest.config.ts Normal file
View File

@@ -0,0 +1,27 @@
import swc from "unplugin-swc";
import { defineConfig } from "vitest/config";
import path from "path";
export default defineConfig({
test: {
globals: false,
environment: "node",
include: ["src/**/*.test.ts", "src/**/*.spec.ts"],
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: ["node_modules/", "dist/"],
},
setupFiles: ["./vitest.setup.ts"],
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
plugins: [
swc.vite({
module: { type: "es6" },
}),
],
});

1
apps/api/vitest.setup.ts Normal file
View File

@@ -0,0 +1 @@
import "reflect-metadata";

6
apps/web/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

7
apps/web/next.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: ["@mosaic/ui", "@mosaic/shared"],
};
export default nextConfig;

36
apps/web/package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "@mosaic/web",
"version": "0.0.1",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev --turbopack --port 3000",
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"typecheck": "tsc --noEmit",
"clean": "rm -rf .next",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@mosaic/shared": "workspace:*",
"@mosaic/ui": "workspace:*",
"next": "^16.1.6",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@mosaic/config": "workspace:*",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@types/node": "^22.13.4",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
"jsdom": "^26.0.0",
"typescript": "^5.8.2",
"vitest": "^3.0.8"
}
}

View File

@@ -0,0 +1,20 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-rgb: 0, 0, 0;
}
}
body {
color: rgb(var(--foreground-rgb));
background: rgb(var(--background-rgb));
}

View File

@@ -0,0 +1,16 @@
import type { Metadata } from "next";
import type { ReactNode } from "react";
import "./globals.css";
export const metadata: Metadata = {
title: "Mosaic Stack",
description: "Mosaic Stack Web Application",
};
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

View File

@@ -0,0 +1,22 @@
import { describe, expect, it, afterEach } from "vitest";
import { render, screen, cleanup } from "@testing-library/react";
import Home from "./page";
afterEach(() => {
cleanup();
});
describe("Home", () => {
it("should render the title", () => {
render(<Home />);
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Mosaic Stack");
});
it("should render the buttons", () => {
render(<Home />);
const buttons = screen.getAllByRole("button");
expect(buttons.length).toBe(2);
expect(buttons[0]).toHaveTextContent("Get Started");
expect(buttons[1]).toHaveTextContent("Learn More");
});
});

16
apps/web/src/app/page.tsx Normal file
View File

@@ -0,0 +1,16 @@
import { Button } from "@mosaic/ui";
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
<h1 className="text-4xl font-bold mb-8">Mosaic Stack</h1>
<p className="text-lg text-gray-600 mb-8">
Welcome to the Mosaic Stack monorepo
</p>
<div className="flex gap-4">
<Button variant="primary">Get Started</Button>
<Button variant="secondary">Learn More</Button>
</div>
</main>
);
}

11
apps/web/tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"extends": "@mosaic/config/typescript/nextjs",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

23
apps/web/vitest.config.ts Normal file
View File

@@ -0,0 +1,23 @@
import { defineConfig } from "vitest/config";
import path from "path";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
globals: false,
environment: "jsdom",
include: ["src/**/*.test.tsx", "src/**/*.test.ts"],
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: ["node_modules/", ".next/"],
},
setupFiles: ["./vitest.setup.ts"],
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});

1
apps/web/vitest.setup.ts Normal file
View File

@@ -0,0 +1 @@
import "@testing-library/jest-dom/vitest";