14 KiB
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 forbuild,check,test:unit,build-storybook,test:e2e, andtest:qa. - Create root
tsconfig.base.json: shared strict TypeScript defaults. - Modify root
package.json: private workspace root withapps/*andpackages/*, Turbo scripts, andturbodev dependency only. - Create
packages/ui/package.json: compiled@dimensionlab/uipackage with code and CSS exports. - Create
packages/ui/tsconfig.jsonandpackages/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/**topackages/ui/src/components/**. - Move
src/lib/ui/stories/**topackages/ui/src/stories/**. - Move
src/lib/ui/tokens.csstopackages/ui/src/tokens.css. - Move
src/lib/ui/theme.ts,types.ts,format.ts,fixtures.ts, andindex.tstopackages/ui/src/**. - Move
src/lib/ui/components/styles.csstopackages/ui/src/components/styles.css. - Move
.storybook/**topackages/ui/.storybook/**. - Move unused shadcn primitives from
src/lib/components/ui/**topackages/ui/src/primitives/**and update theircnimport to package-localsrc/utils.ts. - Move
src/lib/utils.tstopackages/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.tsandmodel-renderer.test.tsintoapps/web/src/lib/ui-adapter/**. - Create
apps/web/package.json,apps/web/tsconfig.json,apps/web/vite.config.ts, andapps/web/playwright.config.ts. - Update
apps/web/src/App.tsxto import components/types from@dimensionlab/uiand import the adapter from$lib/ui-adapter/model-renderer. - Update
apps/web/src/app.cssto import@dimensionlab/ui/styles.cssinstead of local UI CSS files. - Update tests that read paths so package boundary tests inspect
packages/uiand app tests inspectapps/web. - Update
README.mdto 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:
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:
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:
{
"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:
{
"$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:
bunx vitest run apps/web/src/lib/workspace-boundary.test.ts
Expected: PASS.
- Step 5: Commit
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/**topackages/ui/src/components/** -
Move:
src/lib/ui/stories/**topackages/ui/src/stories/** -
Move:
src/lib/ui/{index.ts,types.ts,theme.ts,format.ts,fixtures.ts,tokens.css}topackages/ui/src/** -
Move:
.storybook/**topackages/ui/.storybook/** -
Move:
src/lib/components/ui/**topackages/ui/src/primitives/** -
Move:
src/lib/utils.tstopackages/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:
expect(source).not.toMatch(/from ["'](?:apps\/web|\$lib\/server|\$lib\/model)/);
Run:
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:
@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:
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
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 toapps/web/src/** -
Move:
tests/**toapps/web/tests/** -
Move:
drizzle/**toapps/web/drizzle/** -
Move:
Containerfiletoapps/web/Containerfile -
Move:
index.html,vite.config.ts,playwright.config.ts,drizzle.config.tstoapps/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:
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@dimensionlab/ui/styles.css";
- Step 4: Run app typecheck and unit tests
Run:
bun run --cwd apps/web check
bun run --cwd apps/web test:unit
Expected: PASS.
- Step 5: Commit
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.tsafter move toapps/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:
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:
CMD ["bun", "build/index.js"]
- Step 4: Run full root checks
Run:
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
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:
bun run test:qa
Expected: PASS.
- Step 2: Push and open PR
Run:
git push -u origin codex/turbo-component-library
Open a ready PR against main titled:
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/uiand@dimensionlab/web; the app adapter path is consistently$lib/ui-adapter/model-renderer.