diff --git a/README.md b/README.md index 31c1333..98dc8ec 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,16 @@ reusable React component library. This project is not a Homepage customization and does not depend on Homepage runtime, frontend code, or configuration. The dashboard will be model-driven: -the reusable UI package stays content-free, while environment-specific data -lives in validated dashboard model state inside the web app. +the reusable UI package stays content-free, the reusable dashboard model +package owns schema and validation, and environment-specific data lives in +validated dashboard model state inside the web app. ## Workspace Layout - `apps/web`: Vite React website, Bun API server, model fixtures, Drizzle persistence, Playwright e2e checks, and container build. +- `packages/dashboard-model`: reusable dashboard schema, validation, and + generic model fixtures shared by apps and tooling. - `packages/ui`: reusable dashboard React components, design tokens, shadcn/radix primitives, generic fixtures, and Storybook. - `docs/superpowers`: migration specs and execution plans used for this repo. @@ -65,7 +68,7 @@ with an explicit migration error. ## Seed Data The initial Dimension Lab dashboard lives in -`apps/web/src/lib/model/fixtures/dimensionlab.ts` as validated model data. It +`apps/web/src/lib/dashboard-seed/dimensionlab.ts` as validated model data. It includes the first-screen telemetry, service groups, status strip, weather module, Iconify icon identifiers, links, and datasource references. Values that are not live yet are labeled as fallback values in the data so later datasource diff --git a/apps/web/Containerfile b/apps/web/Containerfile index 912f780..7f12f94 100644 --- a/apps/web/Containerfile +++ b/apps/web/Containerfile @@ -4,6 +4,7 @@ WORKDIR /repo ENV PATH=/repo/apps/web/node_modules/.bin:/repo/packages/ui/node_modules/.bin:/repo/node_modules/.bin:$PATH COPY package.json bun.lock turbo.json tsconfig.base.json ./ COPY apps/web/package.json apps/web/package.json +COPY packages/dashboard-model/package.json packages/dashboard-model/package.json COPY packages/ui/package.json packages/ui/package.json RUN bun install --frozen-lockfile diff --git a/apps/web/package.json b/apps/web/package.json index c2c183f..2e9fef6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -15,6 +15,7 @@ "db:check": "drizzle-kit check" }, "dependencies": { + "@dimensionlab/dashboard-model": "workspace:*", "@dimensionlab/ui": "workspace:*", "@sinclair/typebox": "^0.34.49", "ajv": "^8.20.0", diff --git a/apps/web/src/lib/model/fixtures/dimensionlab.test.ts b/apps/web/src/lib/dashboard-seed/dimensionlab.test.ts similarity index 99% rename from apps/web/src/lib/model/fixtures/dimensionlab.test.ts rename to apps/web/src/lib/dashboard-seed/dimensionlab.test.ts index 38b2ac6..31c01d9 100644 --- a/apps/web/src/lib/model/fixtures/dimensionlab.test.ts +++ b/apps/web/src/lib/dashboard-seed/dimensionlab.test.ts @@ -7,7 +7,7 @@ import type { DatasourceReference, ServiceEntry, TelemetryCard, -} from "../schema"; +} from "@dimensionlab/dashboard-model"; describe("Dimension Lab dashboard seed", () => { test("defines the primary first-screen sections as model data", () => { diff --git a/apps/web/src/lib/model/fixtures/dimensionlab.ts b/apps/web/src/lib/dashboard-seed/dimensionlab.ts similarity index 99% rename from apps/web/src/lib/model/fixtures/dimensionlab.ts rename to apps/web/src/lib/dashboard-seed/dimensionlab.ts index c2b4808..6cc544b 100644 --- a/apps/web/src/lib/model/fixtures/dimensionlab.ts +++ b/apps/web/src/lib/dashboard-seed/dimensionlab.ts @@ -7,7 +7,7 @@ import { type ServiceGroup, type Severity, type TelemetryCard, -} from "../schema"; +} from "@dimensionlab/dashboard-model"; type NumericValueKind = "percent" | "bytes" | "temperature" | "latency" | "number"; diff --git a/apps/web/src/lib/model/fixtures/index.ts b/apps/web/src/lib/model/fixtures/index.ts deleted file mode 100644 index a04278d..0000000 --- a/apps/web/src/lib/model/fixtures/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { dimensionLabDashboardFixture } from "./dimensionlab"; -export { genericDashboardFixture } from "./generic"; diff --git a/apps/web/src/lib/presentation-boundary.test.ts b/apps/web/src/lib/presentation-boundary.test.ts index 7daf204..8a62205 100644 --- a/apps/web/src/lib/presentation-boundary.test.ts +++ b/apps/web/src/lib/presentation-boundary.test.ts @@ -74,5 +74,7 @@ function findFiles(path: string, extension: string): string[] { } function withoutInternalPackageScope(source: string): string { - return source.replaceAll("@dimensionlab/ui", "@internal/ui"); + return source + .replaceAll("@dimensionlab/dashboard-model", "@internal/dashboard-model") + .replaceAll("@dimensionlab/ui", "@internal/ui"); } diff --git a/apps/web/src/lib/server/agent-config/agent-config.test.ts b/apps/web/src/lib/server/agent-config/agent-config.test.ts index 87449e8..cdc60cf 100644 --- a/apps/web/src/lib/server/agent-config/agent-config.test.ts +++ b/apps/web/src/lib/server/agent-config/agent-config.test.ts @@ -3,8 +3,8 @@ import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, test } from "vitest"; -import type { DashboardDocument } from "$lib/model"; -import { genericDashboardFixture } from "$lib/model/fixtures/generic"; +import type { DashboardDocument } from "@dimensionlab/dashboard-model"; +import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures"; import { createDashboardStore, type DashboardStore } from "$lib/server/db/dashboard-store"; import { AgentConfigAuthorizationError, diff --git a/apps/web/src/lib/server/agent-config/index.ts b/apps/web/src/lib/server/agent-config/index.ts index ffa7dd8..ab3d0ae 100644 --- a/apps/web/src/lib/server/agent-config/index.ts +++ b/apps/web/src/lib/server/agent-config/index.ts @@ -16,7 +16,7 @@ import { type ServiceGroup, type StatusItem, type TelemetryCard, -} from "$lib/model"; +} from "@dimensionlab/dashboard-model"; import { createDashboardStore, DashboardRevisionNotFoundError, diff --git a/apps/web/src/lib/server/dashboard.test.ts b/apps/web/src/lib/server/dashboard.test.ts index b15fa6c..7851ebb 100644 --- a/apps/web/src/lib/server/dashboard.test.ts +++ b/apps/web/src/lib/server/dashboard.test.ts @@ -3,8 +3,8 @@ import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, test } from "vitest"; -import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; -import { genericDashboardFixture } from "$lib/model/fixtures/generic"; +import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab"; +import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures"; import { createDashboardStore, type DashboardStore } from "./db/dashboard-store"; import { loadDashboardRuntime } from "./dashboard"; diff --git a/apps/web/src/lib/server/dashboard.ts b/apps/web/src/lib/server/dashboard.ts index 7821ee0..47ebfa2 100644 --- a/apps/web/src/lib/server/dashboard.ts +++ b/apps/web/src/lib/server/dashboard.ts @@ -1,5 +1,5 @@ -import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; -import type { DashboardDocument } from "$lib/model"; +import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab"; +import type { DashboardDocument } from "@dimensionlab/dashboard-model"; import { createDashboardStore, DashboardPersistenceValidationError, diff --git a/apps/web/src/lib/server/datasources/dashboard-datasources.test.ts b/apps/web/src/lib/server/datasources/dashboard-datasources.test.ts index 19082a2..61acdd3 100644 --- a/apps/web/src/lib/server/datasources/dashboard-datasources.test.ts +++ b/apps/web/src/lib/server/datasources/dashboard-datasources.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test, vi } from "vitest"; import { DASHBOARD_SCHEMA_VERSION, type DashboardDocument, -} from "$lib/model"; +} from "@dimensionlab/dashboard-model"; import { resolveDashboardDatasources } from "."; describe("dashboard datasource resolution", () => { diff --git a/apps/web/src/lib/server/datasources/index.ts b/apps/web/src/lib/server/datasources/index.ts index 804fbb5..d69db86 100644 --- a/apps/web/src/lib/server/datasources/index.ts +++ b/apps/web/src/lib/server/datasources/index.ts @@ -8,7 +8,7 @@ import type { StatusItem, StatusStrip, TelemetryCard, -} from "$lib/model"; +} from "@dimensionlab/dashboard-model"; export interface DatasourceResolutionOptions { fetch?: DatasourceFetch; diff --git a/apps/web/src/lib/server/db/dashboard-store.test.ts b/apps/web/src/lib/server/db/dashboard-store.test.ts index ecb80f3..c2717f5 100644 --- a/apps/web/src/lib/server/db/dashboard-store.test.ts +++ b/apps/web/src/lib/server/db/dashboard-store.test.ts @@ -3,8 +3,8 @@ import { mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, test, vi } from "vitest"; -import { genericDashboardFixture } from "$lib/model/fixtures/generic"; -import type { DashboardDocument } from "$lib/model"; +import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures"; +import type { DashboardDocument } from "@dimensionlab/dashboard-model"; import { DashboardPersistenceValidationError, createDashboardStore, diff --git a/apps/web/src/lib/server/db/dashboard-store.ts b/apps/web/src/lib/server/db/dashboard-store.ts index 92c7a73..3928206 100644 --- a/apps/web/src/lib/server/db/dashboard-store.ts +++ b/apps/web/src/lib/server/db/dashboard-store.ts @@ -3,7 +3,7 @@ import { desc, eq } from "drizzle-orm"; import { type DashboardDocument, type DashboardValidationFailure, -} from "$lib/model"; +} from "@dimensionlab/dashboard-model"; import { type DashboardDatabaseConnection, openDashboardDatabase, diff --git a/apps/web/src/lib/server/db/model-migrations.test.ts b/apps/web/src/lib/server/db/model-migrations.test.ts index eb16b8f..5c863a0 100644 --- a/apps/web/src/lib/server/db/model-migrations.test.ts +++ b/apps/web/src/lib/server/db/model-migrations.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "vitest"; -import { DASHBOARD_SCHEMA_VERSION } from "$lib/model"; -import { genericDashboardFixture } from "$lib/model/fixtures/generic"; +import { DASHBOARD_SCHEMA_VERSION } from "@dimensionlab/dashboard-model"; +import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures"; import { UnsupportedDashboardModelVersionError, migrateDashboardDocumentForPersistence, diff --git a/apps/web/src/lib/server/db/model-migrations.ts b/apps/web/src/lib/server/db/model-migrations.ts index 92550e6..72e9c47 100644 --- a/apps/web/src/lib/server/db/model-migrations.ts +++ b/apps/web/src/lib/server/db/model-migrations.ts @@ -3,7 +3,7 @@ import { validateDashboardDocument, type DashboardDocument, type DashboardValidationFailure, -} from "$lib/model"; +} from "@dimensionlab/dashboard-model"; export interface DashboardModelMigrationSuccess { valid: true; diff --git a/apps/web/src/lib/server/db/schema.ts b/apps/web/src/lib/server/db/schema.ts index c148035..a3a7206 100644 --- a/apps/web/src/lib/server/db/schema.ts +++ b/apps/web/src/lib/server/db/schema.ts @@ -1,4 +1,4 @@ -import type { DashboardDocument } from "$lib/model"; +import type { DashboardDocument } from "@dimensionlab/dashboard-model"; import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; export const dashboardDocuments = sqliteTable("dashboard_documents", { diff --git a/apps/web/src/lib/ui-adapter/model-renderer.test.ts b/apps/web/src/lib/ui-adapter/model-renderer.test.ts index fb04ec4..2f8178d 100644 --- a/apps/web/src/lib/ui-adapter/model-renderer.test.ts +++ b/apps/web/src/lib/ui-adapter/model-renderer.test.ts @@ -1,9 +1,9 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; -import { type DashboardDocument } from "$lib/model"; -import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; -import { genericDashboardFixture } from "$lib/model/fixtures/generic"; +import { type DashboardDocument } from "@dimensionlab/dashboard-model"; +import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab"; +import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures"; import { dashboardDocumentToUiDashboard } from "./model-renderer"; const appRoot = process.cwd().endsWith(`${join("apps", "web")}`) @@ -93,6 +93,7 @@ describe("dashboard model renderer", () => { "utf8", ) .toLowerCase() + .replaceAll("@dimensionlab/dashboard-model", "@internal/dashboard-model") .replaceAll("@dimensionlab/ui", "@internal/ui"); expect(source).not.toContain("dimension"); diff --git a/apps/web/src/lib/ui-adapter/model-renderer.ts b/apps/web/src/lib/ui-adapter/model-renderer.ts index 1db2173..7ac1912 100644 --- a/apps/web/src/lib/ui-adapter/model-renderer.ts +++ b/apps/web/src/lib/ui-adapter/model-renderer.ts @@ -6,7 +6,7 @@ import type { StatusItem, StatusStrip, TelemetryCard, -} from "$lib/model"; +} from "@dimensionlab/dashboard-model"; import type { UiDashboardPreview, UiModuleBlock, diff --git a/apps/web/src/lib/workspace-boundary.test.ts b/apps/web/src/lib/workspace-boundary.test.ts index 51652dc..4f4200c 100644 --- a/apps/web/src/lib/workspace-boundary.test.ts +++ b/apps/web/src/lib/workspace-boundary.test.ts @@ -88,4 +88,61 @@ describe("workspace boundaries", () => { expect(uiPackage.exports).toHaveProperty("."); expect(uiPackage.exports).toHaveProperty("./styles.css"); }); + + test("keeps the dashboard model in a reusable internal package", () => { + const webPackage = JSON.parse( + readFileSync(join(root, "apps/web/package.json"), "utf8"), + ) as { dependencies?: Record }; + const modelPackage = JSON.parse( + readFileSync(join(root, "packages/dashboard-model/package.json"), "utf8"), + ) as { exports?: Record; name?: string }; + const tsconfig = JSON.parse( + readFileSync(join(root, "tsconfig.json"), "utf8"), + ) as { references?: Array<{ path: string }> }; + + expect(webPackage.dependencies?.["@dimensionlab/dashboard-model"]).toBe( + "workspace:*", + ); + expect(modelPackage.name).toBe("@dimensionlab/dashboard-model"); + expect(modelPackage.exports).toHaveProperty("."); + expect(modelPackage.exports).toHaveProperty("./fixtures"); + expect(tsconfig.references).toEqual( + expect.arrayContaining([{ path: "./packages/dashboard-model" }]), + ); + expect(existsSync(join(root, "apps/web/src/lib/model/index.ts"))).toBe( + false, + ); + }); + + test("stages web workspace dependency manifests before container install", () => { + const webPackage = JSON.parse( + readFileSync(join(root, "apps/web/package.json"), "utf8"), + ) as { dependencies?: Record }; + const workspacePackages = [ + "packages/ui/package.json", + "packages/dashboard-model/package.json", + ].map((manifestPath) => { + const packageJson = JSON.parse( + readFileSync(join(root, manifestPath), "utf8"), + ) as { name?: string }; + + return [packageJson.name, manifestPath] as const; + }); + const manifestsByPackageName = new Map(workspacePackages); + const containerfile = readFileSync( + join(root, "apps/web/Containerfile"), + "utf8", + ); + + const workspaceDependencyManifests = Object.entries( + webPackage.dependencies ?? {}, + ) + .filter(([, version]) => version.startsWith("workspace:")) + .map(([packageName]) => manifestsByPackageName.get(packageName)); + + expect(workspaceDependencyManifests).not.toContain(undefined); + for (const manifestPath of workspaceDependencyManifests) { + expect(containerfile).toContain(`COPY ${manifestPath} ${manifestPath}`); + } + }); }); diff --git a/apps/web/src/page.test.tsx b/apps/web/src/page.test.tsx index fc68147..2ec5869 100644 --- a/apps/web/src/page.test.tsx +++ b/apps/web/src/page.test.tsx @@ -1,6 +1,6 @@ import { renderToString } from "react-dom/server"; import { describe, expect, test } from "vitest"; -import { genericDashboardFixture } from "$lib/model/fixtures/generic"; +import { genericDashboardFixture } from "@dimensionlab/dashboard-model/fixtures"; import { AppStateView, resolveDocumentMetadata } from "./App"; describe("home page model renderer", () => { diff --git a/apps/web/src/server/routes/dashboard.test.ts b/apps/web/src/server/routes/dashboard.test.ts index ebe575b..21ad58d 100644 --- a/apps/web/src/server/routes/dashboard.test.ts +++ b/apps/web/src/server/routes/dashboard.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { dimensionLabDashboardFixture } from "$lib/model/fixtures/dimensionlab"; +import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab"; import { loadDashboardResponse } from "./dashboard"; describe("dashboard API route", () => { diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index edfa07d..f9f784d 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -3,6 +3,10 @@ "compilerOptions": { "baseUrl": ".", "paths": { + "@dimensionlab/dashboard-model": ["../../packages/dashboard-model/src/index.ts"], + "@dimensionlab/dashboard-model/fixtures": [ + "../../packages/dashboard-model/src/fixtures/index.ts" + ], "@dimensionlab/ui": ["../../packages/ui/src/index.ts"], "@dimensionlab/ui/styles.css": ["../../packages/ui/src/styles.css"], "$lib/*": ["src/lib/*"] diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index d88ee57..687c180 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -30,6 +30,12 @@ export default defineConfig({ "@dimensionlab/ui": fileURLToPath( new URL("../../packages/ui/src/index.ts", import.meta.url), ), + "@dimensionlab/dashboard-model/fixtures": fileURLToPath( + new URL("../../packages/dashboard-model/src/fixtures/index.ts", import.meta.url), + ), + "@dimensionlab/dashboard-model": fileURLToPath( + new URL("../../packages/dashboard-model/src/index.ts", import.meta.url), + ), $lib: fileURLToPath(new URL("./src/lib", import.meta.url)), }, }, diff --git a/bun.lock b/bun.lock index 543520d..d6915d0 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,7 @@ "name": "@dimensionlab/web", "version": "0.0.1", "dependencies": { + "@dimensionlab/dashboard-model": "workspace:*", "@dimensionlab/ui": "workspace:*", "@sinclair/typebox": "^0.34.49", "ajv": "^8.20.0", @@ -40,6 +41,22 @@ "vitest": "^4.1.9", }, }, + "packages/dashboard-model": { + "name": "@dimensionlab/dashboard-model", + "version": "0.0.1", + "dependencies": { + "@sinclair/typebox": "^0.34.49", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + }, + "devDependencies": { + "@types/bun": "^1.3.14", + "@types/node": "^25.9.3", + "bun-types": "^1.3.14", + "typescript": "^6.0.3", + "vitest": "^4.1.9", + }, + }, "packages/ui": { "name": "@dimensionlab/ui", "version": "0.0.1", @@ -137,6 +154,8 @@ "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + "@dimensionlab/dashboard-model": ["@dimensionlab/dashboard-model@workspace:packages/dashboard-model"], + "@dimensionlab/ui": ["@dimensionlab/ui@workspace:packages/ui"], "@dimensionlab/web": ["@dimensionlab/web@workspace:apps/web"], @@ -671,7 +690,7 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="], - "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + "@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], "@vitest/mocker": ["@vitest/mocker@4.1.9", "", { "dependencies": { "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw=="], @@ -681,7 +700,7 @@ "@vitest/snapshot": ["@vitest/snapshot@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA=="], - "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + "@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], "@vitest/utils": ["@vitest/utils@4.1.9", "", { "dependencies": { "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA=="], @@ -745,7 +764,7 @@ "caniuse-lite": ["caniuse-lite@1.0.30001799", "", {}, "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw=="], - "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], @@ -873,7 +892,7 @@ "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], @@ -1479,6 +1498,8 @@ "@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="], @@ -1495,14 +1516,6 @@ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], - "@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - - "@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - - "@vitest/mocker/@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], - - "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1549,14 +1562,14 @@ "shadcn/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], + "storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + + "storybook/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "vitest/@vitest/expect": ["@vitest/expect@4.1.9", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.9", "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA=="], - - "vitest/@vitest/spy": ["@vitest/spy@4.1.9", "", {}, "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA=="], - "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1625,8 +1638,6 @@ "@oxc-resolver/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], - "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], - "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -1689,10 +1700,16 @@ "shadcn/open/wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + "storybook/@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + + "storybook/@vitest/expect/chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + + "storybook/@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "storybook/@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], } } diff --git a/packages/dashboard-model/package.json b/packages/dashboard-model/package.json new file mode 100644 index 0000000..978522e --- /dev/null +++ b/packages/dashboard-model/package.json @@ -0,0 +1,34 @@ +{ + "name": "@dimensionlab/dashboard-model", + "version": "0.0.1", + "private": true, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./fixtures": { + "types": "./dist/fixtures/index.d.ts", + "default": "./dist/fixtures/index.js" + } + }, + "scripts": { + "build": "rm -rf dist && tsc -p tsconfig.build.json", + "check": "tsc --noEmit", + "test": "vitest run", + "test:unit": "vitest run" + }, + "dependencies": { + "@sinclair/typebox": "^0.34.49", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1" + }, + "devDependencies": { + "@types/bun": "^1.3.14", + "@types/node": "^25.9.3", + "bun-types": "^1.3.14", + "typescript": "^6.0.3", + "vitest": "^4.1.9" + } +} diff --git a/apps/web/src/lib/model/fixtures/generic.ts b/packages/dashboard-model/src/fixtures/generic.ts similarity index 100% rename from apps/web/src/lib/model/fixtures/generic.ts rename to packages/dashboard-model/src/fixtures/generic.ts diff --git a/packages/dashboard-model/src/fixtures/index.ts b/packages/dashboard-model/src/fixtures/index.ts new file mode 100644 index 0000000..f62d646 --- /dev/null +++ b/packages/dashboard-model/src/fixtures/index.ts @@ -0,0 +1 @@ +export { genericDashboardFixture } from "./generic"; diff --git a/apps/web/src/lib/model/index.ts b/packages/dashboard-model/src/index.ts similarity index 100% rename from apps/web/src/lib/model/index.ts rename to packages/dashboard-model/src/index.ts diff --git a/apps/web/src/lib/model/schema.test.ts b/packages/dashboard-model/src/schema.test.ts similarity index 96% rename from apps/web/src/lib/model/schema.test.ts rename to packages/dashboard-model/src/schema.test.ts index 7eeed74..9975f87 100644 --- a/apps/web/src/lib/model/schema.test.ts +++ b/packages/dashboard-model/src/schema.test.ts @@ -5,7 +5,6 @@ import { validateDashboardDocument, } from "."; import { - dimensionLabDashboardFixture, genericDashboardFixture, } from "./fixtures"; @@ -19,12 +18,6 @@ describe("dashboard model validation", () => { } }); - it("accepts the Dimension Lab dashboard fixture", () => { - const result = validateDashboardDocument(dimensionLabDashboardFixture); - - expect(result.valid).toBe(true); - }); - it("rejects documents with an unsupported schema version", () => { const invalid = { ...genericDashboardFixture, diff --git a/apps/web/src/lib/model/schema.ts b/packages/dashboard-model/src/schema.ts similarity index 100% rename from apps/web/src/lib/model/schema.ts rename to packages/dashboard-model/src/schema.ts diff --git a/apps/web/src/lib/model/validation.ts b/packages/dashboard-model/src/validation.ts similarity index 100% rename from apps/web/src/lib/model/validation.ts rename to packages/dashboard-model/src/validation.ts diff --git a/packages/dashboard-model/tsconfig.build.json b/packages/dashboard-model/tsconfig.build.json new file mode 100644 index 0000000..5d3e7ce --- /dev/null +++ b/packages/dashboard-model/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": false, + "noEmit": false, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules", "src/**/*.test.ts"] +} diff --git a/packages/dashboard-model/tsconfig.json b/packages/dashboard-model/tsconfig.json new file mode 100644 index 0000000..26cb1c4 --- /dev/null +++ b/packages/dashboard-model/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "types": ["node", "bun-types"] + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/tsconfig.json b/tsconfig.json index 57ef1e6..8fe9b00 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "files": [], "references": [ + { "path": "./packages/dashboard-model" }, { "path": "./apps/web" }, { "path": "./packages/ui" } ]