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

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 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:

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 --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:

{
  "$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/** 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:

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 --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
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:

@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 --cwd apps/web run check
bun --cwd apps/web run 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.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:

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/ui and @dimensionlab/web; the app adapter path is consistently $lib/ui-adapter/model-renderer.