dimensionlab-website/docs/superpowers/plans/2026-06-20-turbo-component-library.md

436 lines
14 KiB
Markdown

# Turbo Component Library Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Convert the single-package Dimension Lab website into a Turborepo workspace where `apps/web` consumes a compiled reusable React component library from `packages/ui`.
**Architecture:** The root becomes a private Bun workspace with Turbo orchestration. `packages/ui` owns generic reusable UI components, CSS, theme helpers, Storybook, and package-level tests. `apps/web` owns the website runtime, model/server/database logic, the model-to-UI adapter, e2e tests, and deployment container.
**Tech Stack:** Bun workspaces, Turborepo, Vite, React 19, TypeScript, Storybook React Vite, Tailwind CSS v4, shadcn CSS, Vitest, Playwright, Drizzle ORM, Bun SQLite.
---
## File Structure
- Create root `turbo.json`: cacheable task graph for `build`, `check`, `test:unit`, `build-storybook`, `test:e2e`, and `test:qa`.
- Create root `tsconfig.base.json`: shared strict TypeScript defaults.
- Modify root `package.json`: private workspace root with `apps/*` and `packages/*`, Turbo scripts, and `turbo` dev dependency only.
- Create `packages/ui/package.json`: compiled `@dimensionlab/ui` package with code and CSS exports.
- Create `packages/ui/tsconfig.json` and `packages/ui/tsconfig.build.json`: package typecheck and declaration/JS build config.
- Create `packages/ui/src/styles.css`: library style entry importing font, uPlot CSS, tokens, and component CSS.
- Move `src/lib/ui/components/**` to `packages/ui/src/components/**`.
- Move `src/lib/ui/stories/**` to `packages/ui/src/stories/**`.
- Move `src/lib/ui/tokens.css` to `packages/ui/src/tokens.css`.
- Move `src/lib/ui/theme.ts`, `types.ts`, `format.ts`, `fixtures.ts`, and `index.ts` to `packages/ui/src/**`.
- Move `src/lib/ui/components/styles.css` to `packages/ui/src/components/styles.css`.
- Move `.storybook/**` to `packages/ui/.storybook/**`.
- Move unused shadcn primitives from `src/lib/components/ui/**` to `packages/ui/src/primitives/**` and update their `cn` import to package-local `src/utils.ts`.
- Move `src/lib/utils.ts` to `packages/ui/src/utils.ts`.
- Move app runtime files into `apps/web`: `src/App.tsx`, `src/main.tsx`, `src/app.css`, `src/server/**`, `src/lib/model/**`, `src/lib/server/**`, `src/lib/testing/**`, `src/vite-env.d.ts`, `src/page.test.tsx`, `src/server/dev.ts`, `tests/**`, `drizzle/**`, `Containerfile`, `index.html`, `playwright.config.ts`, `vite.config.ts`, `drizzle.config.ts`, and app-specific README/deployment files.
- Move `src/lib/ui/model-renderer.ts` and `model-renderer.test.ts` into `apps/web/src/lib/ui-adapter/**`.
- Create `apps/web/package.json`, `apps/web/tsconfig.json`, `apps/web/vite.config.ts`, and `apps/web/playwright.config.ts`.
- Update `apps/web/src/App.tsx` to import components/types from `@dimensionlab/ui` and import the adapter from `$lib/ui-adapter/model-renderer`.
- Update `apps/web/src/app.css` to import `@dimensionlab/ui/styles.css` instead of local UI CSS files.
- Update tests that read paths so package boundary tests inspect `packages/ui` and app tests inspect `apps/web`.
- Update `README.md` to describe the workspace commands, package boundaries, Storybook location, and deployment path.
## Task 1: Workspace And Boundary Tests
**Files:**
- Modify: `package.json`
- Create: `turbo.json`
- Create: `tsconfig.base.json`
- Create: `packages/ui/package.json`
- Create: `packages/ui/tsconfig.json`
- Create: `packages/ui/tsconfig.build.json`
- Create: `apps/web/package.json`
- Create: `apps/web/tsconfig.json`
- Create: `apps/web/src/lib/workspace-boundary.test.ts`
- [ ] **Step 1: Write the failing workspace boundary test**
Create `apps/web/src/lib/workspace-boundary.test.ts`:
```ts
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
const root = join(import.meta.dir, "../../../..");
describe("workspace boundaries", () => {
test("declares the root as a turbo-managed bun workspace", () => {
const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
private?: boolean;
scripts?: Record<string, string>;
workspaces?: string[];
};
expect(packageJson.private).toBe(true);
expect(packageJson.workspaces).toEqual(["apps/*", "packages/*"]);
expect(packageJson.scripts?.build).toBe("turbo build");
expect(existsSync(join(root, "turbo.json"))).toBe(true);
});
test("keeps the website app and reusable UI library as separate packages", () => {
const webPackage = JSON.parse(
readFileSync(join(root, "apps/web/package.json"), "utf8"),
) as { dependencies?: Record<string, string>; name?: string };
const uiPackage = JSON.parse(
readFileSync(join(root, "packages/ui/package.json"), "utf8"),
) as { exports?: Record<string, unknown>; name?: string };
expect(webPackage.name).toBe("@dimensionlab/web");
expect(webPackage.dependencies?.["@dimensionlab/ui"]).toBe("workspace:*");
expect(uiPackage.name).toBe("@dimensionlab/ui");
expect(uiPackage.exports).toHaveProperty(".");
expect(uiPackage.exports).toHaveProperty("./styles.css");
});
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run:
```sh
bunx vitest run apps/web/src/lib/workspace-boundary.test.ts
```
Expected: FAIL because `apps/web`, `packages/ui`, and `turbo.json` do not exist.
- [ ] **Step 3: Add minimal workspace manifests**
Create root `package.json` as the workspace orchestrator:
```json
{
"name": "dimensionlab",
"version": "0.0.1",
"private": true,
"type": "module",
"packageManager": "bun@1.3.14",
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"dev": "turbo dev --filter=@dimensionlab/web",
"build": "turbo build",
"preview": "bun run --cwd apps/web preview",
"storybook": "turbo storybook --filter=@dimensionlab/ui",
"build-storybook": "turbo build-storybook --filter=@dimensionlab/ui",
"check": "turbo check",
"test": "turbo test:unit",
"test:unit": "turbo test:unit",
"test:e2e": "turbo test:e2e --filter=@dimensionlab/web",
"test:qa": "turbo test:qa",
"db:generate": "bun run --cwd apps/web db:generate",
"db:check": "bun run --cwd apps/web db:check"
},
"devDependencies": {
"turbo": "^2.5.0"
}
}
```
Create `turbo.json`:
```json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", "build/**"]
},
"check": {
"dependsOn": ["^build"],
"outputs": []
},
"test:unit": {
"dependsOn": ["^build"],
"outputs": []
},
"build-storybook": {
"dependsOn": ["^build"],
"outputs": ["storybook-static/**"]
},
"test:e2e": {
"dependsOn": ["build", "^build"],
"outputs": ["test-results/**", "playwright-report/**"]
},
"test:qa": {
"dependsOn": ["check", "test:unit", "build", "build-storybook", "test:e2e"],
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
},
"storybook": {
"cache": false,
"persistent": true
}
}
}
```
Create minimal `packages/ui/package.json` with `@dimensionlab/ui` exports and
minimal `apps/web/package.json` with `@dimensionlab/ui` as a workspace
dependency. Move the full dependency lists in Task 2 and Task 3.
- [ ] **Step 4: Run the boundary test**
Run:
```sh
bunx vitest run apps/web/src/lib/workspace-boundary.test.ts
```
Expected: PASS.
- [ ] **Step 5: Commit**
```sh
git add package.json turbo.json tsconfig.base.json apps/web/package.json apps/web/tsconfig.json apps/web/src/lib/workspace-boundary.test.ts packages/ui/package.json packages/ui/tsconfig.json packages/ui/tsconfig.build.json
git commit -m "build: add turbo workspace manifests"
```
## Task 2: Extract The UI Package
**Files:**
- Move: `src/lib/ui/components/**` to `packages/ui/src/components/**`
- Move: `src/lib/ui/stories/**` to `packages/ui/src/stories/**`
- Move: `src/lib/ui/{index.ts,types.ts,theme.ts,format.ts,fixtures.ts,tokens.css}` to `packages/ui/src/**`
- Move: `.storybook/**` to `packages/ui/.storybook/**`
- Move: `src/lib/components/ui/**` to `packages/ui/src/primitives/**`
- Move: `src/lib/utils.ts` to `packages/ui/src/utils.ts`
- Create: `packages/ui/src/styles.css`
- Modify: `packages/ui/src/index.ts`
- Modify: `packages/ui/src/storybook.test.ts`
- Modify: `packages/ui/src/content-boundary.test.ts`
- [ ] **Step 1: Write the failing UI isolation assertion**
Update the package boundary tests to read from `packages/ui/src` and assert no
imports from `apps/web`, `$lib/server`, or `$lib/model` exist:
```ts
expect(source).not.toMatch(/from ["'](?:apps\/web|\$lib\/server|\$lib\/model)/);
```
Run:
```sh
bunx vitest run packages/ui/src/content-boundary.test.ts
```
Expected: FAIL until the files move and the app-specific adapter is removed.
- [ ] **Step 2: Move generic UI files**
Run mechanical moves with `git mv`. Move `model-renderer.ts` out of the UI
package in Task 3 rather than into `packages/ui`.
- [ ] **Step 3: Add the UI style entry**
Create `packages/ui/src/styles.css`:
```css
@import "@fontsource-variable/geist";
@import "uplot/dist/uPlot.min.css";
@import "./tokens.css";
@import "./components/styles.css";
```
- [ ] **Step 4: Make package imports relative or package-local**
Update moved primitive files to import `cn` from `../utils`.
Remove `dashboardDocumentToUiDashboard` from `packages/ui/src/index.ts`.
- [ ] **Step 5: Build the package**
Run:
```sh
bun run --cwd packages/ui build
bun run --cwd packages/ui check
bun run --cwd packages/ui test:unit
```
Expected: PASS and `packages/ui/dist` contains `index.js`, `index.d.ts`, and
CSS files.
- [ ] **Step 6: Commit**
```sh
git add packages/ui src/lib/ui src/lib/components src/lib/utils.ts
git commit -m "refactor(ui): extract reusable component package"
```
## Task 3: Move The Web App Workspace
**Files:**
- Move: `src/**` app files that are not reusable UI to `apps/web/src/**`
- Move: `tests/**` to `apps/web/tests/**`
- Move: `drizzle/**` to `apps/web/drizzle/**`
- Move: `Containerfile` to `apps/web/Containerfile`
- Move: `index.html`, `vite.config.ts`, `playwright.config.ts`, `drizzle.config.ts` to `apps/web/**`
- Create: `apps/web/src/lib/ui-adapter/model-renderer.ts`
- Modify: `apps/web/src/App.tsx`
- Modify: `apps/web/src/app.css`
- [ ] **Step 1: Move the model adapter out of UI**
Move `src/lib/ui/model-renderer.ts` to
`apps/web/src/lib/ui-adapter/model-renderer.ts` and update imports from
`$lib/model` plus UI types from `@dimensionlab/ui`.
- [ ] **Step 2: Move app runtime and tests**
Use `git mv` for app/server/model/test/deployment files into `apps/web`.
- [ ] **Step 3: Update app imports**
In `apps/web/src/App.tsx`, import UI components and types from
`@dimensionlab/ui`, and import `dashboardDocumentToUiDashboard` from
`$lib/ui-adapter/model-renderer`.
In `apps/web/src/app.css`, replace local UI imports with:
```css
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@dimensionlab/ui/styles.css";
```
- [ ] **Step 4: Run app typecheck and unit tests**
Run:
```sh
bun run --cwd apps/web check
bun run --cwd apps/web test:unit
```
Expected: PASS.
- [ ] **Step 5: Commit**
```sh
git add apps/web src tests drizzle Containerfile index.html vite.config.ts playwright.config.ts drizzle.config.ts
git commit -m "refactor(web): move website into app workspace"
```
## Task 4: Wire Turbo, Storybook, Playwright, And Container
**Files:**
- Modify: `apps/web/playwright.config.ts`
- Modify: `apps/web/Containerfile`
- Modify: `apps/web/vite.config.ts`
- Modify: `packages/ui/.storybook/main.ts`
- Modify: `packages/ui/.storybook/preview.ts`
- Modify: `tests/e2e/storybook-server.ts` after move to `apps/web/tests/e2e/storybook-server.ts`
- Modify: `README.md`
- [ ] **Step 1: Update Playwright commands for workspaces**
In `apps/web/playwright.config.ts`, make the web server command build from the
workspace root or app directory consistently:
```ts
command: `DISABLE_LIVE_DATASOURCES=1 bun run build && DISABLE_LIVE_DATASOURCES=1 DATABASE_URL=${databaseUrl} HOST=127.0.0.1 PORT=${port} bun build/index.js`
```
This command runs inside `apps/web` when invoked through the app package script.
- [ ] **Step 2: Update Storybook package paths**
`packages/ui/.storybook/main.ts` should use `../src/**/*.stories.@(js|ts|tsx)`.
`packages/ui/.storybook/preview.ts` should import `../src/styles.css` and use
generic MSW handlers only if they are moved into the UI package.
- [ ] **Step 3: Update the container build**
`apps/web/Containerfile` should build from the repository root context or copy
only the workspace files it needs. Preserve the runtime command:
```dockerfile
CMD ["bun", "build/index.js"]
```
- [ ] **Step 4: Run full root checks**
Run:
```sh
bun install
bun run check
bun run test:unit
bun run build
bun run build-storybook
bun run test:e2e
```
Expected: PASS.
- [ ] **Step 5: Commit**
```sh
git add README.md apps/web packages/ui package.json turbo.json bun.lock
git commit -m "build: wire turbo workspace qa"
```
## Task 5: Final QA, PR, Review, Merge, Deploy
**Files:**
- No planned source edits unless verification finds blockers.
- [ ] **Step 1: Run release gate**
Run:
```sh
bun run test:qa
```
Expected: PASS.
- [ ] **Step 2: Push and open PR**
Run:
```sh
git push -u origin codex/turbo-component-library
```
Open a ready PR against `main` titled:
```text
refactor: migrate dashboard to turbo component library
```
- [ ] **Step 3: Independent review**
Send an independent reviewer to inspect the PR diff against the objective:
Turbo repo, `apps/web`, compiled `packages/ui`, reusable components removed
from app, Storybook with UI package, root QA passing, no server behavior change.
- [ ] **Step 4: Resolve blockers**
For each blocking review finding, write or update a failing test first, verify
the failure, implement the fix, run targeted checks, commit, push, and re-review.
- [ ] **Step 5: Merge and deploy**
When review and checks are clean, merge the PR into `main`, sync the production
checkout, rebuild the Podman image, restart `dimensionlab-website.service`, and
verify `https://dimensionlab.net/` with a browser smoke check.
## Self-Review
- Spec coverage: every completion criterion in the design maps to Task 1 through
Task 5.
- Placeholder scan: no task says TBD, TODO, or "add tests" without commands.
- Type consistency: package names are consistently `@dimensionlab/ui` and
`@dimensionlab/web`; the app adapter path is consistently
`$lib/ui-adapter/model-renderer`.