docs: plan turbo component library migration
This commit is contained in:
parent
786a283f31
commit
4eadd21f71
1 changed files with 436 additions and 0 deletions
436
docs/superpowers/plans/2026-06-20-turbo-component-library.md
Normal file
436
docs/superpowers/plans/2026-06-20-turbo-component-library.md
Normal file
|
|
@ -0,0 +1,436 @@
|
||||||
|
# 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 --cwd apps/web run 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 --cwd apps/web run db:generate",
|
||||||
|
"db:check": "bun --cwd apps/web run 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 --cwd packages/ui run build
|
||||||
|
bun --cwd packages/ui run check
|
||||||
|
bun --cwd packages/ui run 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 --cwd apps/web run check
|
||||||
|
bun --cwd apps/web run 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`.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue