WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto

Merge pull request #6 from malpercio-dev/claude/analyze-test-coverage-6nuSo

docs: analyze test coverage gaps and propose testing strategy

authored by

Malpercio and committed by
GitHub
5aeb4fec eae0f515

+1560 -5
+1
apps/appview/package.json
··· 8 "dev": "tsx watch --env-file=../../.env src/index.ts", 9 "start": "node dist/index.js", 10 "lint": "tsc --noEmit", 11 "clean": "rm -rf dist", 12 "db:generate": "drizzle-kit generate", 13 "db:migrate": "drizzle-kit migrate"
··· 8 "dev": "tsx watch --env-file=../../.env src/index.ts", 9 "start": "node dist/index.js", 10 "lint": "tsc --noEmit", 11 + "test": "vitest run", 12 "clean": "rm -rf dist", 13 "db:generate": "drizzle-kit generate", 14 "db:migrate": "drizzle-kit migrate"
+68
apps/appview/src/lib/__tests__/config.test.ts
···
··· 1 + import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 2 + 3 + describe("loadConfig", () => { 4 + const originalEnv = { ...process.env }; 5 + 6 + beforeEach(() => { 7 + vi.resetModules(); 8 + }); 9 + 10 + afterEach(() => { 11 + process.env = { ...originalEnv }; 12 + }); 13 + 14 + async function loadConfig() { 15 + const mod = await import("../config.js"); 16 + return mod.loadConfig(); 17 + } 18 + 19 + it("returns default port 3000 when PORT is undefined", async () => { 20 + delete process.env.PORT; 21 + const config = await loadConfig(); 22 + expect(config.port).toBe(3000); 23 + }); 24 + 25 + it("parses PORT as an integer", async () => { 26 + process.env.PORT = "4000"; 27 + const config = await loadConfig(); 28 + expect(config.port).toBe(4000); 29 + expect(typeof config.port).toBe("number"); 30 + }); 31 + 32 + it("returns default PDS URL when PDS_URL is undefined", async () => { 33 + delete process.env.PDS_URL; 34 + const config = await loadConfig(); 35 + expect(config.pdsUrl).toBe("https://bsky.social"); 36 + }); 37 + 38 + it("uses provided environment variables", async () => { 39 + process.env.PORT = "5000"; 40 + process.env.FORUM_DID = "did:plc:test123"; 41 + process.env.PDS_URL = "https://my-pds.example.com"; 42 + process.env.DATABASE_URL = "postgres://localhost/testdb"; 43 + const config = await loadConfig(); 44 + expect(config.port).toBe(5000); 45 + expect(config.forumDid).toBe("did:plc:test123"); 46 + expect(config.pdsUrl).toBe("https://my-pds.example.com"); 47 + expect(config.databaseUrl).toBe("postgres://localhost/testdb"); 48 + }); 49 + 50 + it("returns empty string for forumDid when FORUM_DID is undefined", async () => { 51 + delete process.env.FORUM_DID; 52 + const config = await loadConfig(); 53 + expect(config.forumDid).toBe(""); 54 + }); 55 + 56 + it("returns empty string for databaseUrl when DATABASE_URL is undefined", async () => { 57 + delete process.env.DATABASE_URL; 58 + const config = await loadConfig(); 59 + expect(config.databaseUrl).toBe(""); 60 + }); 61 + 62 + it("returns NaN for port when PORT is empty string (?? does not catch empty strings)", async () => { 63 + process.env.PORT = ""; 64 + const config = await loadConfig(); 65 + // Documents a gap: ?? only catches null/undefined, not "" 66 + expect(config.port).toBeNaN(); 67 + }); 68 + });
+34
apps/appview/src/routes/__tests__/categories.test.ts
···
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { Hono } from "hono"; 3 + import { apiRoutes } from "../index.js"; 4 + 5 + const app = new Hono().route("/api", apiRoutes); 6 + 7 + describe("GET /api/categories", () => { 8 + it("returns 200", async () => { 9 + const res = await app.request("/api/categories"); 10 + expect(res.status).toBe(200); 11 + }); 12 + 13 + it("returns an object with a categories array", async () => { 14 + const res = await app.request("/api/categories"); 15 + const body = await res.json(); 16 + expect(body).toHaveProperty("categories"); 17 + expect(Array.isArray(body.categories)).toBe(true); 18 + }); 19 + }); 20 + 21 + describe("GET /api/categories/:id/topics", () => { 22 + it("returns 200", async () => { 23 + const res = await app.request("/api/categories/123/topics"); 24 + expect(res.status).toBe(200); 25 + }); 26 + 27 + it("echoes the category id and returns a topics array", async () => { 28 + const res = await app.request("/api/categories/42/topics"); 29 + const body = await res.json(); 30 + expect(body).toHaveProperty("categoryId", "42"); 31 + expect(body).toHaveProperty("topics"); 32 + expect(Array.isArray(body.topics)).toBe(true); 33 + }); 34 + });
+23
apps/appview/src/routes/__tests__/forum.test.ts
···
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { Hono } from "hono"; 3 + import { apiRoutes } from "../index.js"; 4 + 5 + const app = new Hono().route("/api", apiRoutes); 6 + 7 + describe("GET /api/forum", () => { 8 + it("returns 200", async () => { 9 + const res = await app.request("/api/forum"); 10 + expect(res.status).toBe(200); 11 + }); 12 + 13 + it("returns forum metadata with expected shape", async () => { 14 + const res = await app.request("/api/forum"); 15 + const body = await res.json(); 16 + expect(body).toHaveProperty("name"); 17 + expect(body).toHaveProperty("description"); 18 + expect(body).toHaveProperty("did"); 19 + expect(typeof body.name).toBe("string"); 20 + expect(typeof body.description).toBe("string"); 21 + expect(typeof body.did).toBe("string"); 22 + }); 23 + });
+28
apps/appview/src/routes/__tests__/health.test.ts
···
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { Hono } from "hono"; 3 + import { apiRoutes } from "../index.js"; 4 + 5 + const app = new Hono().route("/api", apiRoutes); 6 + 7 + describe("GET /api/healthz", () => { 8 + it("returns 200 with ok status", async () => { 9 + const res = await app.request("/api/healthz"); 10 + expect(res.status).toBe(200); 11 + const body = await res.json(); 12 + expect(body).toEqual({ status: "ok", version: "0.1.0" }); 13 + }); 14 + 15 + it("returns application/json content type", async () => { 16 + const res = await app.request("/api/healthz"); 17 + expect(res.headers.get("content-type")).toContain("application/json"); 18 + }); 19 + }); 20 + 21 + describe("GET /api/healthz/ready", () => { 22 + it("returns 200 with ready status", async () => { 23 + const res = await app.request("/api/healthz/ready"); 24 + expect(res.status).toBe(200); 25 + const body = await res.json(); 26 + expect(body).toEqual({ status: "ready" }); 27 + }); 28 + });
+18
apps/appview/src/routes/__tests__/posts.test.ts
···
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { Hono } from "hono"; 3 + import { apiRoutes } from "../index.js"; 4 + 5 + const app = new Hono().route("/api", apiRoutes); 6 + 7 + describe("POST /api/posts", () => { 8 + it("returns 501 not implemented", async () => { 9 + const res = await app.request("/api/posts", { method: "POST" }); 10 + expect(res.status).toBe(501); 11 + }); 12 + 13 + it("returns an error message", async () => { 14 + const res = await app.request("/api/posts", { method: "POST" }); 15 + const body = await res.json(); 16 + expect(body).toHaveProperty("error", "not implemented"); 17 + }); 18 + });
+25
apps/appview/src/routes/__tests__/routing.test.ts
···
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { Hono } from "hono"; 3 + import { apiRoutes } from "../index.js"; 4 + 5 + const app = new Hono().route("/api", apiRoutes); 6 + 7 + describe("API routing", () => { 8 + it("returns 404 for unknown routes", async () => { 9 + const res = await app.request("/api/nonexistent"); 10 + expect(res.status).toBe(404); 11 + }); 12 + 13 + it("mounts all expected route prefixes", async () => { 14 + const routes = [ 15 + "/api/healthz", 16 + "/api/forum", 17 + "/api/categories", 18 + ]; 19 + 20 + for (const route of routes) { 21 + const res = await app.request(route); 22 + expect(res.status, `${route} should be reachable`).not.toBe(404); 23 + } 24 + }); 25 + });
+34
apps/appview/src/routes/__tests__/topics.test.ts
···
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { Hono } from "hono"; 3 + import { apiRoutes } from "../index.js"; 4 + 5 + const app = new Hono().route("/api", apiRoutes); 6 + 7 + describe("GET /api/topics/:id", () => { 8 + it("returns 200", async () => { 9 + const res = await app.request("/api/topics/abc123"); 10 + expect(res.status).toBe(200); 11 + }); 12 + 13 + it("echoes the topic id and returns expected shape", async () => { 14 + const res = await app.request("/api/topics/abc123"); 15 + const body = await res.json(); 16 + expect(body).toHaveProperty("topicId", "abc123"); 17 + expect(body).toHaveProperty("post"); 18 + expect(body).toHaveProperty("replies"); 19 + expect(Array.isArray(body.replies)).toBe(true); 20 + }); 21 + }); 22 + 23 + describe("POST /api/topics", () => { 24 + it("returns 501 not implemented", async () => { 25 + const res = await app.request("/api/topics", { method: "POST" }); 26 + expect(res.status).toBe(501); 27 + }); 28 + 29 + it("returns an error message", async () => { 30 + const res = await app.request("/api/topics", { method: "POST" }); 31 + const body = await res.json(); 32 + expect(body).toHaveProperty("error", "not implemented"); 33 + }); 34 + });
+7
apps/appview/vitest.config.ts
···
··· 1 + import { defineConfig } from "vitest/config"; 2 + 3 + export default defineConfig({ 4 + test: { 5 + environment: "node", 6 + }, 7 + });
+1
apps/web/package.json
··· 8 "dev": "tsx watch --env-file=../../.env src/index.ts", 9 "start": "node dist/index.js", 10 "lint": "tsc --noEmit", 11 "clean": "rm -rf dist" 12 }, 13 "dependencies": {
··· 8 "dev": "tsx watch --env-file=../../.env src/index.ts", 9 "start": "node dist/index.js", 10 "lint": "tsc --noEmit", 11 + "test": "vitest run", 12 "clean": "rm -rf dist" 13 }, 14 "dependencies": {
+74
apps/web/src/lib/__tests__/api.test.ts
···
··· 1 + import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 2 + 3 + const mockFetch = vi.fn(); 4 + 5 + describe("fetchApi", () => { 6 + beforeEach(() => { 7 + vi.stubGlobal("fetch", mockFetch); 8 + vi.stubEnv("APPVIEW_URL", "http://localhost:3000"); 9 + vi.resetModules(); 10 + }); 11 + 12 + afterEach(() => { 13 + vi.unstubAllGlobals(); 14 + vi.unstubAllEnvs(); 15 + mockFetch.mockReset(); 16 + }); 17 + 18 + async function loadFetchApi() { 19 + const mod = await import("../api.js"); 20 + return mod.fetchApi; 21 + } 22 + 23 + it("calls the correct URL", async () => { 24 + mockFetch.mockResolvedValueOnce({ 25 + ok: true, 26 + json: () => Promise.resolve({ data: "test" }), 27 + }); 28 + 29 + const fetchApi = await loadFetchApi(); 30 + await fetchApi("/categories"); 31 + 32 + expect(mockFetch).toHaveBeenCalledOnce(); 33 + const calledUrl = mockFetch.mock.calls[0][0]; 34 + expect(calledUrl).toBe("http://localhost:3000/api/categories"); 35 + }); 36 + 37 + it("returns parsed JSON on success", async () => { 38 + const expected = { categories: [{ id: 1, name: "General" }] }; 39 + mockFetch.mockResolvedValueOnce({ 40 + ok: true, 41 + json: () => Promise.resolve(expected), 42 + }); 43 + 44 + const fetchApi = await loadFetchApi(); 45 + const result = await fetchApi("/categories"); 46 + expect(result).toEqual(expected); 47 + }); 48 + 49 + it("throws on non-ok response", async () => { 50 + mockFetch.mockResolvedValueOnce({ 51 + ok: false, 52 + status: 500, 53 + statusText: "Internal Server Error", 54 + }); 55 + 56 + const fetchApi = await loadFetchApi(); 57 + await expect(fetchApi("/fail")).rejects.toThrow( 58 + "AppView API error: 500 Internal Server Error" 59 + ); 60 + }); 61 + 62 + it("throws on 404 response", async () => { 63 + mockFetch.mockResolvedValueOnce({ 64 + ok: false, 65 + status: 404, 66 + statusText: "Not Found", 67 + }); 68 + 69 + const fetchApi = await loadFetchApi(); 70 + await expect(fetchApi("/missing")).rejects.toThrow( 71 + "AppView API error: 404 Not Found" 72 + ); 73 + }); 74 + });
+59
apps/web/src/lib/__tests__/config.test.ts
···
··· 1 + import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; 2 + 3 + describe("loadConfig", () => { 4 + const originalEnv = { ...process.env }; 5 + 6 + beforeEach(() => { 7 + vi.resetModules(); 8 + }); 9 + 10 + afterEach(() => { 11 + process.env = { ...originalEnv }; 12 + }); 13 + 14 + async function loadConfig() { 15 + const mod = await import("../config.js"); 16 + return mod.loadConfig(); 17 + } 18 + 19 + it("returns default port 3001 when PORT is undefined", async () => { 20 + delete process.env.PORT; 21 + const config = await loadConfig(); 22 + expect(config.port).toBe(3001); 23 + }); 24 + 25 + it("parses PORT as an integer", async () => { 26 + process.env.PORT = "8080"; 27 + const config = await loadConfig(); 28 + expect(config.port).toBe(8080); 29 + expect(typeof config.port).toBe("number"); 30 + }); 31 + 32 + it("returns default appview URL when APPVIEW_URL is undefined", async () => { 33 + delete process.env.APPVIEW_URL; 34 + const config = await loadConfig(); 35 + expect(config.appviewUrl).toBe("http://localhost:3000"); 36 + }); 37 + 38 + it("uses provided environment variables", async () => { 39 + process.env.PORT = "9000"; 40 + process.env.APPVIEW_URL = "https://api.atbb.space"; 41 + const config = await loadConfig(); 42 + expect(config.port).toBe(9000); 43 + expect(config.appviewUrl).toBe("https://api.atbb.space"); 44 + }); 45 + 46 + it("returns NaN for port when PORT is empty string (?? does not catch empty strings)", async () => { 47 + process.env.PORT = ""; 48 + const config = await loadConfig(); 49 + // Documents a gap: ?? only catches null/undefined, not "" 50 + expect(config.port).toBeNaN(); 51 + }); 52 + 53 + it("returns empty string for appviewUrl when APPVIEW_URL is empty string", async () => { 54 + process.env.APPVIEW_URL = ""; 55 + const config = await loadConfig(); 56 + // Documents a gap: ?? only catches null/undefined, not "" 57 + expect(config.appviewUrl).toBe(""); 58 + }); 59 + });
+7
apps/web/vitest.config.ts
···
··· 1 + import { defineConfig } from "vitest/config"; 2 + 3 + export default defineConfig({ 4 + test: { 5 + environment: "node", 6 + }, 7 + });
+259
docs/test-coverage-analysis.md
···
··· 1 + # Test Coverage Analysis 2 + 3 + ## Current State: No Tests Exist 4 + 5 + The monorepo has **zero test infrastructure**. No testing framework is installed, no test scripts exist in any `package.json`, and `turbo.json` has no `test` task. There are approximately **530 lines of source code** across 4 packages with 0% test coverage. 6 + 7 + --- 8 + 9 + ## Recommended Test Framework Setup 10 + 11 + **Vitest** is the best fit for this project: 12 + - Native ESM support (the repo uses `"type": "module"` everywhere) 13 + - Built-in TypeScript support via `tsx`/`esbuild` (no separate ts-jest config) 14 + - Workspace-aware — can share a root config while per-package configs override as needed 15 + - Fast, with watch mode out of the box 16 + 17 + ### Infrastructure needed 18 + 19 + 1. Install `vitest` as a root devDependency 20 + 2. Add a root `vitest.workspace.ts` pointing at each package 21 + 3. Add `"test": "vitest run"` scripts to each package's `package.json` 22 + 4. Add a `"test"` task to `turbo.json` (with `dependsOn: ["^build"]` since appview/web depend on lexicon types) 23 + 5. Add `pnpm test` as a root script 24 + 25 + --- 26 + 27 + ## Package-by-Package Gaps and Recommendations 28 + 29 + ### 1. `@atbb/appview` — API Server (highest priority) 30 + 31 + This is the most complex package (260 LOC) and where most business logic will land as stubs are implemented. It has the database schema, config loading, AT Protocol agent creation, and all API routes. 32 + 33 + #### a) Route handler tests (high value) 34 + 35 + **Files:** `src/routes/health.ts`, `src/routes/forum.ts`, `src/routes/categories.ts`, `src/routes/topics.ts`, `src/routes/posts.ts` 36 + 37 + **What to test:** 38 + - `GET /api/healthz` returns `200` with `{ status: "ok", version: "0.1.0" }` 39 + - `GET /api/healthz/ready` returns `200` with `{ status: "ready" }` 40 + - `GET /api/forum` returns `200` with the expected forum shape 41 + - `GET /api/categories` returns `200` with `{ categories: [] }` 42 + - `GET /api/categories/:id/topics` returns `200` and echoes the `id` param 43 + - `POST /api/topics` returns `501` (not implemented) 44 + - `POST /api/posts` returns `501` (not implemented) 45 + 46 + **How:** Use Hono's built-in `app.request()` test helper — no HTTP server needed: 47 + ```ts 48 + import { describe, it, expect } from "vitest"; 49 + import { apiRoutes } from "../src/routes/index.js"; 50 + import { Hono } from "hono"; 51 + 52 + const app = new Hono().route("/api", apiRoutes); 53 + 54 + describe("GET /api/healthz", () => { 55 + it("returns ok status", async () => { 56 + const res = await app.request("/api/healthz"); 57 + expect(res.status).toBe(200); 58 + expect(await res.json()).toEqual({ status: "ok", version: "0.1.0" }); 59 + }); 60 + }); 61 + ``` 62 + 63 + **Why it matters:** As stubs are replaced with real implementations, having route-level tests in place catches regressions in response shape, status codes, and content-type headers. These tests are cheap to write now and will grow in value. 64 + 65 + #### b) Config loading tests (medium value) 66 + 67 + **File:** `src/lib/config.ts` 68 + 69 + **What to test:** 70 + - Returns correct defaults when env vars are absent (`PORT` defaults to `3000`, `PDS_URL` defaults to `https://bsky.social`) 71 + - Parses `PORT` as an integer (not a string) 72 + - Returns provided env var values when set 73 + - Handles `PORT` set to a non-numeric string (currently `parseInt` would return `NaN` — should this throw?) 74 + 75 + **Why it matters:** Config loading is the root of most "it works on my machine" bugs. The current implementation silently accepts empty strings for `forumDid` and `databaseUrl`, which will cause hard-to-debug runtime failures. Tests would document this behavior and motivate adding validation. 76 + 77 + #### c) Database schema tests (medium value) 78 + 79 + **File:** `src/db/schema.ts` 80 + 81 + **What to test:** 82 + - Schema definitions export the expected table names 83 + - Column types match expectations (e.g., `posts.deleted` defaults to `false`) 84 + - Foreign key references are correct (`posts.did` → `users.did`, `posts.rootPostId` → `posts.id`, etc.) 85 + - Index names are correct and unique constraints are in place 86 + 87 + **How:** These can be pure unit tests against the Drizzle schema objects — no database connection needed. Drizzle table objects expose `._.columns` and other metadata you can assert against. 88 + 89 + **Why it matters:** Schema is the foundation. If someone accidentally removes an index or changes a foreign key, these tests catch it before it hits a migration. 90 + 91 + #### d) Database integration tests (high value, but requires infrastructure) 92 + 93 + **What to test:** 94 + - Insert/select/update/delete for each table 95 + - Foreign key constraints are enforced (e.g., inserting a post with a non-existent `did` fails) 96 + - Unique index violations behave as expected 97 + - The `createDb()` factory produces a working Drizzle client 98 + 99 + **How:** Use a test PostgreSQL instance. Options: 100 + - **Testcontainers** (Docker-based, spins up a real Postgres per test suite) 101 + - **pg-mem** (in-memory Postgres emulator, faster but not 100% compatible) 102 + - A shared test database with transaction rollback between tests 103 + 104 + **Why it matters:** This is where the most subtle bugs live — constraint violations, bad joins, missing indexes. As the appview stubs are fleshed out with real queries, these tests become critical. 105 + 106 + #### e) AT Protocol agent factory test (low value now, higher later) 107 + 108 + **File:** `src/lib/atproto.ts` 109 + 110 + Currently just `new AtpAgent({ service: config.pdsUrl })` — not much to test. But as authentication and record-writing logic is added, this module should have tests verifying: 111 + - Agent is created with the correct service URL 112 + - Authentication errors are handled gracefully 113 + - Record write/read operations produce expected AT URI formats 114 + 115 + --- 116 + 117 + ### 2. `@atbb/web` — Server-Rendered Web UI 118 + 119 + #### a) API client tests (high value) 120 + 121 + **File:** `src/lib/api.ts` 122 + 123 + **What to test:** 124 + - `fetchApi("/categories")` calls the correct URL (`${appviewUrl}/api/categories`) 125 + - Throws an `Error` with status code and status text on non-2xx responses 126 + - Returns parsed JSON on success 127 + - Handles network failures (fetch throws) 128 + 129 + **How:** Mock `global.fetch` with `vi.fn()` or use `msw` (Mock Service Worker): 130 + ```ts 131 + import { describe, it, expect, vi, beforeEach } from "vitest"; 132 + 133 + // Mock fetch globally 134 + const mockFetch = vi.fn(); 135 + vi.stubGlobal("fetch", mockFetch); 136 + 137 + describe("fetchApi", () => { 138 + it("throws on non-ok response", async () => { 139 + mockFetch.mockResolvedValueOnce({ 140 + ok: false, status: 500, statusText: "Internal Server Error", 141 + }); 142 + const { fetchApi } = await import("../src/lib/api.js"); 143 + await expect(fetchApi("/test")).rejects.toThrow("AppView API error: 500"); 144 + }); 145 + }); 146 + ``` 147 + 148 + **Why it matters:** `fetchApi` is the single point of contact between the web UI and the appview. Error handling here determines whether users see useful error messages or blank pages. 149 + 150 + #### b) JSX component / layout tests (medium value) 151 + 152 + **File:** `src/layouts/base.tsx`, `src/routes/home.tsx` 153 + 154 + **What to test:** 155 + - `BaseLayout` renders valid HTML with the provided title 156 + - `BaseLayout` uses the default title "atBB Forum" when none is provided 157 + - `BaseLayout` includes the HTMX script tag 158 + - Home route returns `200` with `text/html` content type 159 + - Home page includes "Welcome to atBB" heading 160 + 161 + **How:** Use Hono's `app.request()` and assert against the HTML string, or use a lightweight HTML parser. Hono JSX components can be tested by rendering them and checking the output string. 162 + 163 + #### c) Config loading tests (low-medium value) 164 + 165 + **File:** `src/lib/config.ts` 166 + 167 + Same pattern as the appview config tests — verify defaults, parsing, and presence of required values. 168 + 169 + --- 170 + 171 + ### 3. `@atbb/lexicon` — Lexicon Definitions 172 + 173 + #### a) YAML-to-JSON build script tests (medium value) 174 + 175 + **File:** `scripts/build.ts` 176 + 177 + **What to test:** 178 + - Each YAML file in `lexicons/` produces valid JSON 179 + - Output JSON matches the expected Lexicon schema structure (has `lexicon`, `id`, `defs` fields) 180 + - No duplicate lexicon IDs across files 181 + - The `id` field in each lexicon matches its file path (e.g., `space/atbb/post.yaml` has `id: "space.atbb.post"`) 182 + 183 + **How:** Rather than testing the build script directly (it's I/O-heavy), write validation tests that run against the YAML source files: 184 + ```ts 185 + import { parse } from "yaml"; 186 + import { readFileSync } from "fs"; 187 + import { glob } from "glob"; 188 + 189 + describe("lexicon definitions", () => { 190 + const files = glob.sync("**/*.yaml", { cwd: "lexicons" }); 191 + 192 + it.each(files)("%s has a valid lexicon structure", (file) => { 193 + const content = readFileSync(`lexicons/${file}`, "utf-8"); 194 + const parsed = parse(content); 195 + expect(parsed).toHaveProperty("lexicon", 1); 196 + expect(parsed).toHaveProperty("id"); 197 + expect(parsed).toHaveProperty("defs"); 198 + }); 199 + }); 200 + ``` 201 + 202 + **Why it matters:** Lexicon definitions are the API contract for the entire AT Protocol integration. A malformed lexicon causes downstream build failures in type generation and runtime validation errors. Catching issues at the YAML level is far cheaper than debugging them at the API level. 203 + 204 + #### b) Schema contract tests (high value) 205 + 206 + **What to test:** 207 + - `space.atbb.post` has `text` as a required string field 208 + - `space.atbb.post` has optional `reply` with `root` and `parent` refs 209 + - `space.atbb.forum.forum` uses `key: literal:self` 210 + - `space.atbb.forum.category` uses `key: tid` 211 + - All `strongRef` usages have both `uri` and `cid` fields 212 + - `knownValues` are used (not `enum`) for extensible fields like `modAction.action` 213 + 214 + **Why it matters:** These are the **contract tests** of the system. If a lexicon field is accidentally renamed or a required field becomes optional, it breaks interoperability with any PDS that stores atBB records. These tests protect the public API surface. 215 + 216 + --- 217 + 218 + ### 4. `@atbb/spike` — PDS Integration Script 219 + 220 + The spike is a manual integration test. It doesn't need unit tests itself, but: 221 + 222 + #### Extractable test utilities (medium value) 223 + 224 + The spike contains reusable patterns for: 225 + - Authenticating with a PDS 226 + - Creating/reading/deleting AT Protocol records 227 + - Generating TIDs 228 + 229 + These should be extracted into a shared test utility module (e.g., `packages/test-utils/`) that integration tests across the monorepo can use. 230 + 231 + --- 232 + 233 + ## Priority Matrix 234 + 235 + | Priority | Area | Package | Effort | Impact | 236 + |----------|------|---------|--------|--------| 237 + | **P0** | Test infrastructure setup (vitest, turbo task, CI) | root | Low | Unblocks everything | 238 + | **P0** | Appview route handler tests | appview | Low | Catches regressions as stubs are implemented | 239 + | **P1** | Web API client tests (`fetchApi`) | web | Low | Validates the only web→appview boundary | 240 + | **P1** | Lexicon schema contract tests | lexicon | Low | Protects the AT Protocol API surface | 241 + | **P1** | Config loading tests (both packages) | appview, web | Low | Documents defaults, catches parse bugs | 242 + | **P2** | Database schema unit tests | appview | Medium | Catches accidental schema changes | 243 + | **P2** | JSX layout/component tests | web | Medium | Ensures correct HTML output | 244 + | **P2** | Lexicon build script validation | lexicon | Low | Catches YAML/JSON conversion issues | 245 + | **P3** | Database integration tests | appview | High | Requires Postgres test infra (Docker/testcontainers) | 246 + | **P3** | AT Protocol integration tests | appview | High | Requires PDS test instance or mock | 247 + | **P3** | Extract spike utilities into shared test-utils | spike | Medium | Enables reuse across integration tests | 248 + 249 + --- 250 + 251 + ## Suggested Implementation Order 252 + 253 + 1. **Set up vitest** at the root + per-package, add `test` task to turbo.json 254 + 2. **Appview route tests** — quick wins since Hono has a built-in test helper and the routes are simple right now 255 + 3. **Lexicon contract tests** — validate YAML schema structure to protect the AT Protocol API 256 + 4. **Web `fetchApi` tests** — mock fetch, verify URL construction and error handling 257 + 5. **Config tests** for both packages — small but catches real bugs 258 + 6. **Database schema tests** — assert on Drizzle metadata objects 259 + 7. **Database integration tests** — add testcontainers or similar once there are real queries to test
+4 -2
package.json
··· 6 "build": "turbo run build", 7 "dev": "turbo run dev", 8 "lint": "turbo run lint", 9 - "clean": "turbo run clean" 10 }, 11 "devDependencies": { 12 "turbo": "^2.4.0", 13 - "typescript": "^5.7.0" 14 } 15 }
··· 6 "build": "turbo run build", 7 "dev": "turbo run dev", 8 "lint": "turbo run lint", 9 + "clean": "turbo run clean", 10 + "test": "turbo run test" 11 }, 12 "devDependencies": { 13 "turbo": "^2.4.0", 14 + "typescript": "^5.7.0", 15 + "vitest": "^4.0.18" 16 } 17 }
+2 -1
packages/db/package.json
··· 18 "scripts": { 19 "build": "tsc", 20 "lint": "tsc --noEmit", 21 - "clean": "rm -rf dist" 22 }, 23 "dependencies": { 24 "drizzle-orm": "^0.45.1",
··· 18 "scripts": { 19 "build": "tsc", 20 "lint": "tsc --noEmit", 21 + "clean": "rm -rf dist", 22 + "test": "vitest run" 23 }, 24 "dependencies": { 25 "drizzle-orm": "^0.45.1",
+178
packages/db/src/__tests__/schema.test.ts
···
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { getTableName, getTableColumns } from "drizzle-orm"; 3 + import { 4 + forums, 5 + categories, 6 + users, 7 + memberships, 8 + posts, 9 + modActions, 10 + } from "../schema.js"; 11 + 12 + describe("database schema", () => { 13 + describe("forums table", () => { 14 + it("has the correct table name", () => { 15 + expect(getTableName(forums)).toBe("forums"); 16 + }); 17 + 18 + it("has expected columns", () => { 19 + const cols = getTableColumns(forums); 20 + expect(cols).toHaveProperty("id"); 21 + expect(cols).toHaveProperty("did"); 22 + expect(cols).toHaveProperty("rkey"); 23 + expect(cols).toHaveProperty("cid"); 24 + expect(cols).toHaveProperty("name"); 25 + expect(cols).toHaveProperty("description"); 26 + expect(cols).toHaveProperty("indexedAt"); 27 + }); 28 + 29 + it("has did and rkey as not-null", () => { 30 + const cols = getTableColumns(forums); 31 + expect(cols.did.notNull).toBe(true); 32 + expect(cols.rkey.notNull).toBe(true); 33 + }); 34 + }); 35 + 36 + describe("categories table", () => { 37 + it("has the correct table name", () => { 38 + expect(getTableName(categories)).toBe("categories"); 39 + }); 40 + 41 + it("has expected columns", () => { 42 + const cols = getTableColumns(categories); 43 + expect(cols).toHaveProperty("id"); 44 + expect(cols).toHaveProperty("did"); 45 + expect(cols).toHaveProperty("rkey"); 46 + expect(cols).toHaveProperty("cid"); 47 + expect(cols).toHaveProperty("name"); 48 + expect(cols).toHaveProperty("description"); 49 + expect(cols).toHaveProperty("slug"); 50 + expect(cols).toHaveProperty("sortOrder"); 51 + expect(cols).toHaveProperty("forumId"); 52 + expect(cols).toHaveProperty("createdAt"); 53 + expect(cols).toHaveProperty("indexedAt"); 54 + }); 55 + }); 56 + 57 + describe("users table", () => { 58 + it("has the correct table name", () => { 59 + expect(getTableName(users)).toBe("users"); 60 + }); 61 + 62 + it("uses did as primary key", () => { 63 + const cols = getTableColumns(users); 64 + expect(cols.did.primary).toBe(true); 65 + }); 66 + 67 + it("has handle as optional", () => { 68 + const cols = getTableColumns(users); 69 + expect(cols.handle.notNull).toBe(false); 70 + }); 71 + }); 72 + 73 + describe("memberships table", () => { 74 + it("has the correct table name", () => { 75 + expect(getTableName(memberships)).toBe("memberships"); 76 + }); 77 + 78 + it("has expected columns", () => { 79 + const cols = getTableColumns(memberships); 80 + expect(cols).toHaveProperty("id"); 81 + expect(cols).toHaveProperty("did"); 82 + expect(cols).toHaveProperty("rkey"); 83 + expect(cols).toHaveProperty("cid"); 84 + expect(cols).toHaveProperty("forumId"); 85 + expect(cols).toHaveProperty("forumUri"); 86 + expect(cols).toHaveProperty("role"); 87 + expect(cols).toHaveProperty("roleUri"); 88 + expect(cols).toHaveProperty("joinedAt"); 89 + expect(cols).toHaveProperty("createdAt"); 90 + expect(cols).toHaveProperty("indexedAt"); 91 + }); 92 + 93 + it("has did and forumUri as not-null", () => { 94 + const cols = getTableColumns(memberships); 95 + expect(cols.did.notNull).toBe(true); 96 + expect(cols.forumUri.notNull).toBe(true); 97 + }); 98 + }); 99 + 100 + describe("posts table", () => { 101 + it("has the correct table name", () => { 102 + expect(getTableName(posts)).toBe("posts"); 103 + }); 104 + 105 + it("has expected columns for the unified post model", () => { 106 + const cols = getTableColumns(posts); 107 + expect(cols).toHaveProperty("id"); 108 + expect(cols).toHaveProperty("did"); 109 + expect(cols).toHaveProperty("rkey"); 110 + expect(cols).toHaveProperty("cid"); 111 + expect(cols).toHaveProperty("text"); 112 + expect(cols).toHaveProperty("forumUri"); 113 + expect(cols).toHaveProperty("rootPostId"); 114 + expect(cols).toHaveProperty("parentPostId"); 115 + expect(cols).toHaveProperty("rootUri"); 116 + expect(cols).toHaveProperty("parentUri"); 117 + expect(cols).toHaveProperty("createdAt"); 118 + expect(cols).toHaveProperty("indexedAt"); 119 + expect(cols).toHaveProperty("deleted"); 120 + }); 121 + 122 + it("has text as not-null", () => { 123 + const cols = getTableColumns(posts); 124 + expect(cols.text.notNull).toBe(true); 125 + }); 126 + 127 + it("has deleted defaulting to false", () => { 128 + const cols = getTableColumns(posts); 129 + expect(cols.deleted.notNull).toBe(true); 130 + expect(cols.deleted.hasDefault).toBe(true); 131 + }); 132 + 133 + it("has rootPostId and parentPostId as nullable (topics have no parent)", () => { 134 + const cols = getTableColumns(posts); 135 + expect(cols.rootPostId.notNull).toBe(false); 136 + expect(cols.parentPostId.notNull).toBe(false); 137 + }); 138 + }); 139 + 140 + describe("modActions table", () => { 141 + it("has the correct table name", () => { 142 + expect(getTableName(modActions)).toBe("mod_actions"); 143 + }); 144 + 145 + it("has expected columns", () => { 146 + const cols = getTableColumns(modActions); 147 + expect(cols).toHaveProperty("id"); 148 + expect(cols).toHaveProperty("did"); 149 + expect(cols).toHaveProperty("rkey"); 150 + expect(cols).toHaveProperty("cid"); 151 + expect(cols).toHaveProperty("action"); 152 + expect(cols).toHaveProperty("subjectDid"); 153 + expect(cols).toHaveProperty("subjectPostUri"); 154 + expect(cols).toHaveProperty("forumId"); 155 + expect(cols).toHaveProperty("reason"); 156 + expect(cols).toHaveProperty("createdBy"); 157 + expect(cols).toHaveProperty("expiresAt"); 158 + expect(cols).toHaveProperty("createdAt"); 159 + expect(cols).toHaveProperty("indexedAt"); 160 + }); 161 + 162 + it("has action and createdBy as not-null", () => { 163 + const cols = getTableColumns(modActions); 164 + expect(cols.action.notNull).toBe(true); 165 + expect(cols.createdBy.notNull).toBe(true); 166 + }); 167 + }); 168 + 169 + describe("all tables export correctly", () => { 170 + it("exports six tables", () => { 171 + const tables = [forums, categories, users, memberships, posts, modActions]; 172 + expect(tables).toHaveLength(6); 173 + tables.forEach((table) => { 174 + expect(table).toBeDefined(); 175 + }); 176 + }); 177 + }); 178 + });
+7
packages/db/vitest.config.ts
···
··· 1 + import { defineConfig } from "vitest/config"; 2 + 3 + export default defineConfig({ 4 + test: { 5 + environment: "node", 6 + }, 7 + });
+97
packages/lexicon/__tests__/lexicons.test.ts
···
··· 1 + import { describe, it, expect } from "vitest"; 2 + import { readFileSync } from "node:fs"; 3 + import { join } from "node:path"; 4 + import { parse as parseYaml } from "yaml"; 5 + import { globSync } from "glob"; 6 + 7 + const LEXICONS_DIR = join(import.meta.dirname, "..", "lexicons"); 8 + const yamlFiles = globSync("**/*.yaml", { cwd: LEXICONS_DIR }); 9 + 10 + describe("lexicon definitions", () => { 11 + it("finds at least one lexicon file", () => { 12 + expect(yamlFiles.length).toBeGreaterThan(0); 13 + }); 14 + 15 + describe.each(yamlFiles)("%s", (file) => { 16 + const content = readFileSync(join(LEXICONS_DIR, file), "utf-8"); 17 + const parsed = parseYaml(content); 18 + 19 + it("has lexicon version 1", () => { 20 + expect(parsed).toHaveProperty("lexicon", 1); 21 + }); 22 + 23 + it("has an id field", () => { 24 + expect(parsed).toHaveProperty("id"); 25 + expect(typeof parsed.id).toBe("string"); 26 + }); 27 + 28 + it("has a defs object", () => { 29 + expect(parsed).toHaveProperty("defs"); 30 + expect(typeof parsed.defs).toBe("object"); 31 + }); 32 + 33 + it("has an id that matches the file path", () => { 34 + // space/atbb/post.yaml -> space.atbb.post 35 + const expectedId = file.replace(/\.yaml$/, "").replace(/\//g, "."); 36 + expect(parsed.id).toBe(expectedId); 37 + }); 38 + }); 39 + }); 40 + 41 + describe("lexicon uniqueness", () => { 42 + it("has no duplicate lexicon ids", () => { 43 + const ids = yamlFiles.map((file) => { 44 + const content = readFileSync(join(LEXICONS_DIR, file), "utf-8"); 45 + return parseYaml(content).id; 46 + }); 47 + const unique = new Set(ids); 48 + expect(unique.size).toBe(ids.length); 49 + }); 50 + }); 51 + 52 + describe("record key conventions", () => { 53 + const recordLexicons = yamlFiles 54 + .map((file) => { 55 + const content = readFileSync(join(LEXICONS_DIR, file), "utf-8"); 56 + return { file, parsed: parseYaml(content) }; 57 + }) 58 + .filter(({ parsed }) => parsed.defs?.main?.type === "record"); 59 + 60 + it.each(recordLexicons.map(({ file, parsed }) => [file, parsed]))( 61 + "%s has a valid record key type", 62 + (_file, parsed) => { 63 + const key = parsed.defs.main.key; 64 + expect(["tid", "literal:self"]).toContain(key); 65 + } 66 + ); 67 + }); 68 + 69 + describe("extensible fields use knownValues", () => { 70 + it("modAction.action uses knownValues, not enum", () => { 71 + const content = readFileSync( 72 + join(LEXICONS_DIR, "space/atbb/modAction.yaml"), 73 + "utf-8" 74 + ); 75 + const parsed = parseYaml(content); 76 + const actionField = 77 + parsed.defs.main.record?.properties?.action; 78 + expect(actionField).toBeDefined(); 79 + expect(actionField).not.toHaveProperty("enum"); 80 + expect(actionField).toHaveProperty("knownValues"); 81 + }); 82 + }); 83 + 84 + describe("strongRef definitions", () => { 85 + it("strongRef has uri and cid fields", () => { 86 + const content = readFileSync( 87 + join(LEXICONS_DIR, "com/atproto/repo/strongRef.yaml"), 88 + "utf-8" 89 + ); 90 + const parsed = parseYaml(content); 91 + const mainDef = parsed.defs.main; 92 + expect(mainDef.properties).toHaveProperty("uri"); 93 + expect(mainDef.properties).toHaveProperty("cid"); 94 + expect(mainDef.required).toContain("uri"); 95 + expect(mainDef.required).toContain("cid"); 96 + }); 97 + });
+5 -2
packages/lexicon/package.json
··· 13 "build": "pnpm run build:json && pnpm run build:types", 14 "build:json": "tsx scripts/build.ts", 15 "build:types": "bash -c 'shopt -s globstar && lex gen-api --yes ./dist/types ./dist/json/**/*.json'", 16 "clean": "rm -rf dist" 17 }, 18 "devDependencies": { 19 "@atproto/lex-cli": "^0.5.0", 20 "tsx": "^4.0.0", 21 - "yaml": "^2.7.0", 22 - "glob": "^11.0.0" 23 } 24 }
··· 13 "build": "pnpm run build:json && pnpm run build:types", 14 "build:json": "tsx scripts/build.ts", 15 "build:types": "bash -c 'shopt -s globstar && lex gen-api --yes ./dist/types ./dist/json/**/*.json'", 16 + "test": "vitest run", 17 "clean": "rm -rf dist" 18 }, 19 "devDependencies": { 20 "@atproto/lex-cli": "^0.5.0", 21 + "@types/node": "^22.0.0", 22 + "glob": "^11.0.0", 23 "tsx": "^4.0.0", 24 + "typescript": "^5.7.0", 25 + "yaml": "^2.7.0" 26 } 27 }
+7
packages/lexicon/vitest.config.ts
···
··· 1 + import { defineConfig } from "vitest/config"; 2 + 3 + export default defineConfig({ 4 + test: { 5 + environment: "node", 6 + }, 7 + });
+611
pnpm-lock.yaml
··· 14 typescript: 15 specifier: ^5.7.0 16 version: 5.9.3 17 18 apps/appview: 19 dependencies: ··· 92 '@atproto/lex-cli': 93 specifier: ^0.5.0 94 version: 0.5.7 95 glob: 96 specifier: ^11.0.0 97 version: 11.1.0 98 tsx: 99 specifier: ^4.0.0 100 version: 4.21.0 101 yaml: 102 specifier: ^2.7.0 103 version: 2.8.2 ··· 631 resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} 632 engines: {node: '>=18'} 633 634 '@nodelib/fs.scandir@2.1.5': 635 resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 636 engines: {node: '>= 8'} ··· 643 resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 644 engines: {node: '>= 8'} 645 646 '@ts-morph/common@0.17.0': 647 resolution: {integrity: sha512-RMSSvSfs9kb0VzkvQ2NWobwnj7TxCA9vI/IjR9bDHqgAyVbu2T0DN4wiKVqomyDWqO7dPr/tErSfq7urQ1Q37g==} 648 649 '@types/node@22.19.9': 650 resolution: {integrity: sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==} 651 652 ansi-styles@4.3.0: 653 resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 654 engines: {node: '>=8'} 655 656 await-lock@2.2.2: 657 resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} 658 ··· 668 669 buffer-from@1.1.2: 670 resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 671 672 chalk@4.1.2: 673 resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} ··· 796 sqlite3: 797 optional: true 798 799 esbuild-register@3.6.0: 800 resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} 801 peerDependencies: ··· 816 engines: {node: '>=18'} 817 hasBin: true 818 819 fast-glob@3.3.3: 820 resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} 821 engines: {node: '>=8.6.0'} 822 823 fastq@1.20.1: 824 resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} 825 826 fill-range@7.1.1: 827 resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} ··· 883 resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==} 884 engines: {node: 20 || >=22} 885 886 merge2@1.4.1: 887 resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 888 engines: {node: '>= 8'} ··· 914 multiformats@9.9.0: 915 resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} 916 917 package-json-from-dist@1.0.1: 918 resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} 919 ··· 928 resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} 929 engines: {node: 20 || >=22} 930 931 picomatch@2.3.1: 932 resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 933 engines: {node: '>=8.6'} 934 935 postgres@3.4.8: 936 resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==} ··· 951 resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} 952 engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 953 954 run-parallel@1.2.0: 955 resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 956 ··· 962 resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 963 engines: {node: '>=8'} 964 965 signal-exit@4.1.0: 966 resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 967 engines: {node: '>=14'} 968 969 source-map-support@0.5.21: 970 resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 971 ··· 973 resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 974 engines: {node: '>=0.10.0'} 975 976 supports-color@7.2.0: 977 resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 978 engines: {node: '>=8'} 979 980 tlds@1.261.0: 981 resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} ··· 1052 unicode-segmenter@0.14.5: 1053 resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} 1054 1055 which@2.0.2: 1056 resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 1057 engines: {node: '>= 8'} 1058 hasBin: true 1059 1060 yaml@2.8.2: ··· 1384 1385 '@isaacs/cliui@9.0.0': {} 1386 1387 '@nodelib/fs.scandir@2.1.5': 1388 dependencies: 1389 '@nodelib/fs.stat': 2.0.5 ··· 1396 '@nodelib/fs.scandir': 2.1.5 1397 fastq: 1.20.1 1398 1399 '@ts-morph/common@0.17.0': 1400 dependencies: 1401 fast-glob: 3.3.3 1402 minimatch: 5.1.6 1403 mkdirp: 1.0.4 1404 path-browserify: 1.0.1 1405 1406 '@types/node@22.19.9': 1407 dependencies: 1408 undici-types: 6.21.0 1409 1410 ansi-styles@4.3.0: 1411 dependencies: 1412 color-convert: 2.0.1 1413 1414 await-lock@2.2.2: {} 1415 ··· 1424 fill-range: 7.1.1 1425 1426 buffer-from@1.1.2: {} 1427 1428 chalk@4.1.2: 1429 dependencies: ··· 1462 drizzle-orm@0.45.1(postgres@3.4.8): 1463 optionalDependencies: 1464 postgres: 3.4.8 1465 1466 esbuild-register@3.6.0(esbuild@0.25.12): 1467 dependencies: ··· 1553 '@esbuild/win32-ia32': 0.27.3 1554 '@esbuild/win32-x64': 0.27.3 1555 1556 fast-glob@3.3.3: 1557 dependencies: 1558 '@nodelib/fs.stat': 2.0.5 ··· 1564 fastq@1.20.1: 1565 dependencies: 1566 reusify: 1.1.0 1567 1568 fill-range@7.1.1: 1569 dependencies: ··· 1616 1617 lru-cache@11.2.5: {} 1618 1619 merge2@1.4.1: {} 1620 1621 micromatch@4.0.8: ··· 1639 1640 multiformats@9.9.0: {} 1641 1642 package-json-from-dist@1.0.1: {} 1643 1644 path-browserify@1.0.1: {} ··· 1649 dependencies: 1650 lru-cache: 11.2.5 1651 minipass: 7.1.2 1652 1653 picomatch@2.3.1: {} 1654 1655 postgres@3.4.8: {} 1656 1657 prettier@3.8.1: {} ··· 1662 1663 reusify@1.1.0: {} 1664 1665 run-parallel@1.2.0: 1666 dependencies: 1667 queue-microtask: 1.2.3 ··· 1672 1673 shebang-regex@3.0.0: {} 1674 1675 signal-exit@4.1.0: {} 1676 1677 source-map-support@0.5.21: 1678 dependencies: ··· 1681 1682 source-map@0.6.1: {} 1683 1684 supports-color@7.2.0: 1685 dependencies: 1686 has-flag: 4.0.0 1687 1688 tlds@1.261.0: {} 1689 ··· 1748 1749 unicode-segmenter@0.14.5: {} 1750 1751 which@2.0.2: 1752 dependencies: 1753 isexe: 2.0.0 1754 1755 yaml@2.8.2: {} 1756
··· 14 typescript: 15 specifier: ^5.7.0 16 version: 5.9.3 17 + vitest: 18 + specifier: ^4.0.18 19 + version: 4.0.18(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2) 20 21 apps/appview: 22 dependencies: ··· 95 '@atproto/lex-cli': 96 specifier: ^0.5.0 97 version: 0.5.7 98 + '@types/node': 99 + specifier: ^22.0.0 100 + version: 22.19.9 101 glob: 102 specifier: ^11.0.0 103 version: 11.1.0 104 tsx: 105 specifier: ^4.0.0 106 version: 4.21.0 107 + typescript: 108 + specifier: ^5.7.0 109 + version: 5.9.3 110 yaml: 111 specifier: ^2.7.0 112 version: 2.8.2 ··· 640 resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} 641 engines: {node: '>=18'} 642 643 + '@jridgewell/sourcemap-codec@1.5.5': 644 + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} 645 + 646 '@nodelib/fs.scandir@2.1.5': 647 resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 648 engines: {node: '>= 8'} ··· 655 resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 656 engines: {node: '>= 8'} 657 658 + '@rollup/rollup-android-arm-eabi@4.57.1': 659 + resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} 660 + cpu: [arm] 661 + os: [android] 662 + 663 + '@rollup/rollup-android-arm64@4.57.1': 664 + resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} 665 + cpu: [arm64] 666 + os: [android] 667 + 668 + '@rollup/rollup-darwin-arm64@4.57.1': 669 + resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} 670 + cpu: [arm64] 671 + os: [darwin] 672 + 673 + '@rollup/rollup-darwin-x64@4.57.1': 674 + resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} 675 + cpu: [x64] 676 + os: [darwin] 677 + 678 + '@rollup/rollup-freebsd-arm64@4.57.1': 679 + resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} 680 + cpu: [arm64] 681 + os: [freebsd] 682 + 683 + '@rollup/rollup-freebsd-x64@4.57.1': 684 + resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} 685 + cpu: [x64] 686 + os: [freebsd] 687 + 688 + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': 689 + resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} 690 + cpu: [arm] 691 + os: [linux] 692 + 693 + '@rollup/rollup-linux-arm-musleabihf@4.57.1': 694 + resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} 695 + cpu: [arm] 696 + os: [linux] 697 + 698 + '@rollup/rollup-linux-arm64-gnu@4.57.1': 699 + resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} 700 + cpu: [arm64] 701 + os: [linux] 702 + 703 + '@rollup/rollup-linux-arm64-musl@4.57.1': 704 + resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} 705 + cpu: [arm64] 706 + os: [linux] 707 + 708 + '@rollup/rollup-linux-loong64-gnu@4.57.1': 709 + resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} 710 + cpu: [loong64] 711 + os: [linux] 712 + 713 + '@rollup/rollup-linux-loong64-musl@4.57.1': 714 + resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} 715 + cpu: [loong64] 716 + os: [linux] 717 + 718 + '@rollup/rollup-linux-ppc64-gnu@4.57.1': 719 + resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} 720 + cpu: [ppc64] 721 + os: [linux] 722 + 723 + '@rollup/rollup-linux-ppc64-musl@4.57.1': 724 + resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} 725 + cpu: [ppc64] 726 + os: [linux] 727 + 728 + '@rollup/rollup-linux-riscv64-gnu@4.57.1': 729 + resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} 730 + cpu: [riscv64] 731 + os: [linux] 732 + 733 + '@rollup/rollup-linux-riscv64-musl@4.57.1': 734 + resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} 735 + cpu: [riscv64] 736 + os: [linux] 737 + 738 + '@rollup/rollup-linux-s390x-gnu@4.57.1': 739 + resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} 740 + cpu: [s390x] 741 + os: [linux] 742 + 743 + '@rollup/rollup-linux-x64-gnu@4.57.1': 744 + resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} 745 + cpu: [x64] 746 + os: [linux] 747 + 748 + '@rollup/rollup-linux-x64-musl@4.57.1': 749 + resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} 750 + cpu: [x64] 751 + os: [linux] 752 + 753 + '@rollup/rollup-openbsd-x64@4.57.1': 754 + resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} 755 + cpu: [x64] 756 + os: [openbsd] 757 + 758 + '@rollup/rollup-openharmony-arm64@4.57.1': 759 + resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} 760 + cpu: [arm64] 761 + os: [openharmony] 762 + 763 + '@rollup/rollup-win32-arm64-msvc@4.57.1': 764 + resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} 765 + cpu: [arm64] 766 + os: [win32] 767 + 768 + '@rollup/rollup-win32-ia32-msvc@4.57.1': 769 + resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} 770 + cpu: [ia32] 771 + os: [win32] 772 + 773 + '@rollup/rollup-win32-x64-gnu@4.57.1': 774 + resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} 775 + cpu: [x64] 776 + os: [win32] 777 + 778 + '@rollup/rollup-win32-x64-msvc@4.57.1': 779 + resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} 780 + cpu: [x64] 781 + os: [win32] 782 + 783 + '@standard-schema/spec@1.1.0': 784 + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} 785 + 786 '@ts-morph/common@0.17.0': 787 resolution: {integrity: sha512-RMSSvSfs9kb0VzkvQ2NWobwnj7TxCA9vI/IjR9bDHqgAyVbu2T0DN4wiKVqomyDWqO7dPr/tErSfq7urQ1Q37g==} 788 789 + '@types/chai@5.2.3': 790 + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} 791 + 792 + '@types/deep-eql@4.0.2': 793 + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} 794 + 795 + '@types/estree@1.0.8': 796 + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 797 + 798 '@types/node@22.19.9': 799 resolution: {integrity: sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==} 800 801 + '@vitest/expect@4.0.18': 802 + resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} 803 + 804 + '@vitest/mocker@4.0.18': 805 + resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} 806 + peerDependencies: 807 + msw: ^2.4.9 808 + vite: ^6.0.0 || ^7.0.0-0 809 + peerDependenciesMeta: 810 + msw: 811 + optional: true 812 + vite: 813 + optional: true 814 + 815 + '@vitest/pretty-format@4.0.18': 816 + resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} 817 + 818 + '@vitest/runner@4.0.18': 819 + resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} 820 + 821 + '@vitest/snapshot@4.0.18': 822 + resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} 823 + 824 + '@vitest/spy@4.0.18': 825 + resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} 826 + 827 + '@vitest/utils@4.0.18': 828 + resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} 829 + 830 ansi-styles@4.3.0: 831 resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 832 engines: {node: '>=8'} 833 834 + assertion-error@2.0.1: 835 + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 836 + engines: {node: '>=12'} 837 + 838 await-lock@2.2.2: 839 resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} 840 ··· 850 851 buffer-from@1.1.2: 852 resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 853 + 854 + chai@6.2.2: 855 + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} 856 + engines: {node: '>=18'} 857 858 chalk@4.1.2: 859 resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} ··· 982 sqlite3: 983 optional: true 984 985 + es-module-lexer@1.7.0: 986 + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} 987 + 988 esbuild-register@3.6.0: 989 resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} 990 peerDependencies: ··· 1005 engines: {node: '>=18'} 1006 hasBin: true 1007 1008 + estree-walker@3.0.3: 1009 + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 1010 + 1011 + expect-type@1.3.0: 1012 + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} 1013 + engines: {node: '>=12.0.0'} 1014 + 1015 fast-glob@3.3.3: 1016 resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} 1017 engines: {node: '>=8.6.0'} 1018 1019 fastq@1.20.1: 1020 resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} 1021 + 1022 + fdir@6.5.0: 1023 + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} 1024 + engines: {node: '>=12.0.0'} 1025 + peerDependencies: 1026 + picomatch: ^3 || ^4 1027 + peerDependenciesMeta: 1028 + picomatch: 1029 + optional: true 1030 1031 fill-range@7.1.1: 1032 resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} ··· 1088 resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==} 1089 engines: {node: 20 || >=22} 1090 1091 + magic-string@0.30.21: 1092 + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 1093 + 1094 merge2@1.4.1: 1095 resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 1096 engines: {node: '>= 8'} ··· 1122 multiformats@9.9.0: 1123 resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} 1124 1125 + nanoid@3.3.11: 1126 + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 1127 + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 1128 + hasBin: true 1129 + 1130 + obug@2.1.1: 1131 + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} 1132 + 1133 package-json-from-dist@1.0.1: 1134 resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} 1135 ··· 1144 resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} 1145 engines: {node: 20 || >=22} 1146 1147 + pathe@2.0.3: 1148 + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 1149 + 1150 + picocolors@1.1.1: 1151 + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 1152 + 1153 picomatch@2.3.1: 1154 resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 1155 engines: {node: '>=8.6'} 1156 + 1157 + picomatch@4.0.3: 1158 + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} 1159 + engines: {node: '>=12'} 1160 + 1161 + postcss@8.5.6: 1162 + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} 1163 + engines: {node: ^10 || ^12 || >=14} 1164 1165 postgres@3.4.8: 1166 resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==} ··· 1181 resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} 1182 engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 1183 1184 + rollup@4.57.1: 1185 + resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} 1186 + engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1187 + hasBin: true 1188 + 1189 run-parallel@1.2.0: 1190 resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 1191 ··· 1197 resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 1198 engines: {node: '>=8'} 1199 1200 + siginfo@2.0.0: 1201 + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 1202 + 1203 signal-exit@4.1.0: 1204 resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 1205 engines: {node: '>=14'} 1206 1207 + source-map-js@1.2.1: 1208 + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1209 + engines: {node: '>=0.10.0'} 1210 + 1211 source-map-support@0.5.21: 1212 resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 1213 ··· 1215 resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 1216 engines: {node: '>=0.10.0'} 1217 1218 + stackback@0.0.2: 1219 + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 1220 + 1221 + std-env@3.10.0: 1222 + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} 1223 + 1224 supports-color@7.2.0: 1225 resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 1226 engines: {node: '>=8'} 1227 + 1228 + tinybench@2.9.0: 1229 + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} 1230 + 1231 + tinyexec@1.0.2: 1232 + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} 1233 + engines: {node: '>=18'} 1234 + 1235 + tinyglobby@0.2.15: 1236 + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} 1237 + engines: {node: '>=12.0.0'} 1238 + 1239 + tinyrainbow@3.0.3: 1240 + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} 1241 + engines: {node: '>=14.0.0'} 1242 1243 tlds@1.261.0: 1244 resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} ··· 1315 unicode-segmenter@0.14.5: 1316 resolution: {integrity: sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g==} 1317 1318 + vite@7.3.1: 1319 + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} 1320 + engines: {node: ^20.19.0 || >=22.12.0} 1321 + hasBin: true 1322 + peerDependencies: 1323 + '@types/node': ^20.19.0 || >=22.12.0 1324 + jiti: '>=1.21.0' 1325 + less: ^4.0.0 1326 + lightningcss: ^1.21.0 1327 + sass: ^1.70.0 1328 + sass-embedded: ^1.70.0 1329 + stylus: '>=0.54.8' 1330 + sugarss: ^5.0.0 1331 + terser: ^5.16.0 1332 + tsx: ^4.8.1 1333 + yaml: ^2.4.2 1334 + peerDependenciesMeta: 1335 + '@types/node': 1336 + optional: true 1337 + jiti: 1338 + optional: true 1339 + less: 1340 + optional: true 1341 + lightningcss: 1342 + optional: true 1343 + sass: 1344 + optional: true 1345 + sass-embedded: 1346 + optional: true 1347 + stylus: 1348 + optional: true 1349 + sugarss: 1350 + optional: true 1351 + terser: 1352 + optional: true 1353 + tsx: 1354 + optional: true 1355 + yaml: 1356 + optional: true 1357 + 1358 + vitest@4.0.18: 1359 + resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} 1360 + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} 1361 + hasBin: true 1362 + peerDependencies: 1363 + '@edge-runtime/vm': '*' 1364 + '@opentelemetry/api': ^1.9.0 1365 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 1366 + '@vitest/browser-playwright': 4.0.18 1367 + '@vitest/browser-preview': 4.0.18 1368 + '@vitest/browser-webdriverio': 4.0.18 1369 + '@vitest/ui': 4.0.18 1370 + happy-dom: '*' 1371 + jsdom: '*' 1372 + peerDependenciesMeta: 1373 + '@edge-runtime/vm': 1374 + optional: true 1375 + '@opentelemetry/api': 1376 + optional: true 1377 + '@types/node': 1378 + optional: true 1379 + '@vitest/browser-playwright': 1380 + optional: true 1381 + '@vitest/browser-preview': 1382 + optional: true 1383 + '@vitest/browser-webdriverio': 1384 + optional: true 1385 + '@vitest/ui': 1386 + optional: true 1387 + happy-dom: 1388 + optional: true 1389 + jsdom: 1390 + optional: true 1391 + 1392 which@2.0.2: 1393 resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 1394 engines: {node: '>= 8'} 1395 + hasBin: true 1396 + 1397 + why-is-node-running@2.3.0: 1398 + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} 1399 + engines: {node: '>=8'} 1400 hasBin: true 1401 1402 yaml@2.8.2: ··· 1726 1727 '@isaacs/cliui@9.0.0': {} 1728 1729 + '@jridgewell/sourcemap-codec@1.5.5': {} 1730 + 1731 '@nodelib/fs.scandir@2.1.5': 1732 dependencies: 1733 '@nodelib/fs.stat': 2.0.5 ··· 1740 '@nodelib/fs.scandir': 2.1.5 1741 fastq: 1.20.1 1742 1743 + '@rollup/rollup-android-arm-eabi@4.57.1': 1744 + optional: true 1745 + 1746 + '@rollup/rollup-android-arm64@4.57.1': 1747 + optional: true 1748 + 1749 + '@rollup/rollup-darwin-arm64@4.57.1': 1750 + optional: true 1751 + 1752 + '@rollup/rollup-darwin-x64@4.57.1': 1753 + optional: true 1754 + 1755 + '@rollup/rollup-freebsd-arm64@4.57.1': 1756 + optional: true 1757 + 1758 + '@rollup/rollup-freebsd-x64@4.57.1': 1759 + optional: true 1760 + 1761 + '@rollup/rollup-linux-arm-gnueabihf@4.57.1': 1762 + optional: true 1763 + 1764 + '@rollup/rollup-linux-arm-musleabihf@4.57.1': 1765 + optional: true 1766 + 1767 + '@rollup/rollup-linux-arm64-gnu@4.57.1': 1768 + optional: true 1769 + 1770 + '@rollup/rollup-linux-arm64-musl@4.57.1': 1771 + optional: true 1772 + 1773 + '@rollup/rollup-linux-loong64-gnu@4.57.1': 1774 + optional: true 1775 + 1776 + '@rollup/rollup-linux-loong64-musl@4.57.1': 1777 + optional: true 1778 + 1779 + '@rollup/rollup-linux-ppc64-gnu@4.57.1': 1780 + optional: true 1781 + 1782 + '@rollup/rollup-linux-ppc64-musl@4.57.1': 1783 + optional: true 1784 + 1785 + '@rollup/rollup-linux-riscv64-gnu@4.57.1': 1786 + optional: true 1787 + 1788 + '@rollup/rollup-linux-riscv64-musl@4.57.1': 1789 + optional: true 1790 + 1791 + '@rollup/rollup-linux-s390x-gnu@4.57.1': 1792 + optional: true 1793 + 1794 + '@rollup/rollup-linux-x64-gnu@4.57.1': 1795 + optional: true 1796 + 1797 + '@rollup/rollup-linux-x64-musl@4.57.1': 1798 + optional: true 1799 + 1800 + '@rollup/rollup-openbsd-x64@4.57.1': 1801 + optional: true 1802 + 1803 + '@rollup/rollup-openharmony-arm64@4.57.1': 1804 + optional: true 1805 + 1806 + '@rollup/rollup-win32-arm64-msvc@4.57.1': 1807 + optional: true 1808 + 1809 + '@rollup/rollup-win32-ia32-msvc@4.57.1': 1810 + optional: true 1811 + 1812 + '@rollup/rollup-win32-x64-gnu@4.57.1': 1813 + optional: true 1814 + 1815 + '@rollup/rollup-win32-x64-msvc@4.57.1': 1816 + optional: true 1817 + 1818 + '@standard-schema/spec@1.1.0': {} 1819 + 1820 '@ts-morph/common@0.17.0': 1821 dependencies: 1822 fast-glob: 3.3.3 1823 minimatch: 5.1.6 1824 mkdirp: 1.0.4 1825 path-browserify: 1.0.1 1826 + 1827 + '@types/chai@5.2.3': 1828 + dependencies: 1829 + '@types/deep-eql': 4.0.2 1830 + assertion-error: 2.0.1 1831 + 1832 + '@types/deep-eql@4.0.2': {} 1833 + 1834 + '@types/estree@1.0.8': {} 1835 1836 '@types/node@22.19.9': 1837 dependencies: 1838 undici-types: 6.21.0 1839 1840 + '@vitest/expect@4.0.18': 1841 + dependencies: 1842 + '@standard-schema/spec': 1.1.0 1843 + '@types/chai': 5.2.3 1844 + '@vitest/spy': 4.0.18 1845 + '@vitest/utils': 4.0.18 1846 + chai: 6.2.2 1847 + tinyrainbow: 3.0.3 1848 + 1849 + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2))': 1850 + dependencies: 1851 + '@vitest/spy': 4.0.18 1852 + estree-walker: 3.0.3 1853 + magic-string: 0.30.21 1854 + optionalDependencies: 1855 + vite: 7.3.1(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2) 1856 + 1857 + '@vitest/pretty-format@4.0.18': 1858 + dependencies: 1859 + tinyrainbow: 3.0.3 1860 + 1861 + '@vitest/runner@4.0.18': 1862 + dependencies: 1863 + '@vitest/utils': 4.0.18 1864 + pathe: 2.0.3 1865 + 1866 + '@vitest/snapshot@4.0.18': 1867 + dependencies: 1868 + '@vitest/pretty-format': 4.0.18 1869 + magic-string: 0.30.21 1870 + pathe: 2.0.3 1871 + 1872 + '@vitest/spy@4.0.18': {} 1873 + 1874 + '@vitest/utils@4.0.18': 1875 + dependencies: 1876 + '@vitest/pretty-format': 4.0.18 1877 + tinyrainbow: 3.0.3 1878 + 1879 ansi-styles@4.3.0: 1880 dependencies: 1881 color-convert: 2.0.1 1882 + 1883 + assertion-error@2.0.1: {} 1884 1885 await-lock@2.2.2: {} 1886 ··· 1895 fill-range: 7.1.1 1896 1897 buffer-from@1.1.2: {} 1898 + 1899 + chai@6.2.2: {} 1900 1901 chalk@4.1.2: 1902 dependencies: ··· 1935 drizzle-orm@0.45.1(postgres@3.4.8): 1936 optionalDependencies: 1937 postgres: 3.4.8 1938 + 1939 + es-module-lexer@1.7.0: {} 1940 1941 esbuild-register@3.6.0(esbuild@0.25.12): 1942 dependencies: ··· 2028 '@esbuild/win32-ia32': 0.27.3 2029 '@esbuild/win32-x64': 0.27.3 2030 2031 + estree-walker@3.0.3: 2032 + dependencies: 2033 + '@types/estree': 1.0.8 2034 + 2035 + expect-type@1.3.0: {} 2036 + 2037 fast-glob@3.3.3: 2038 dependencies: 2039 '@nodelib/fs.stat': 2.0.5 ··· 2045 fastq@1.20.1: 2046 dependencies: 2047 reusify: 1.1.0 2048 + 2049 + fdir@6.5.0(picomatch@4.0.3): 2050 + optionalDependencies: 2051 + picomatch: 4.0.3 2052 2053 fill-range@7.1.1: 2054 dependencies: ··· 2101 2102 lru-cache@11.2.5: {} 2103 2104 + magic-string@0.30.21: 2105 + dependencies: 2106 + '@jridgewell/sourcemap-codec': 1.5.5 2107 + 2108 merge2@1.4.1: {} 2109 2110 micromatch@4.0.8: ··· 2128 2129 multiformats@9.9.0: {} 2130 2131 + nanoid@3.3.11: {} 2132 + 2133 + obug@2.1.1: {} 2134 + 2135 package-json-from-dist@1.0.1: {} 2136 2137 path-browserify@1.0.1: {} ··· 2142 dependencies: 2143 lru-cache: 11.2.5 2144 minipass: 7.1.2 2145 + 2146 + pathe@2.0.3: {} 2147 + 2148 + picocolors@1.1.1: {} 2149 2150 picomatch@2.3.1: {} 2151 2152 + picomatch@4.0.3: {} 2153 + 2154 + postcss@8.5.6: 2155 + dependencies: 2156 + nanoid: 3.3.11 2157 + picocolors: 1.1.1 2158 + source-map-js: 1.2.1 2159 + 2160 postgres@3.4.8: {} 2161 2162 prettier@3.8.1: {} ··· 2167 2168 reusify@1.1.0: {} 2169 2170 + rollup@4.57.1: 2171 + dependencies: 2172 + '@types/estree': 1.0.8 2173 + optionalDependencies: 2174 + '@rollup/rollup-android-arm-eabi': 4.57.1 2175 + '@rollup/rollup-android-arm64': 4.57.1 2176 + '@rollup/rollup-darwin-arm64': 4.57.1 2177 + '@rollup/rollup-darwin-x64': 4.57.1 2178 + '@rollup/rollup-freebsd-arm64': 4.57.1 2179 + '@rollup/rollup-freebsd-x64': 4.57.1 2180 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 2181 + '@rollup/rollup-linux-arm-musleabihf': 4.57.1 2182 + '@rollup/rollup-linux-arm64-gnu': 4.57.1 2183 + '@rollup/rollup-linux-arm64-musl': 4.57.1 2184 + '@rollup/rollup-linux-loong64-gnu': 4.57.1 2185 + '@rollup/rollup-linux-loong64-musl': 4.57.1 2186 + '@rollup/rollup-linux-ppc64-gnu': 4.57.1 2187 + '@rollup/rollup-linux-ppc64-musl': 4.57.1 2188 + '@rollup/rollup-linux-riscv64-gnu': 4.57.1 2189 + '@rollup/rollup-linux-riscv64-musl': 4.57.1 2190 + '@rollup/rollup-linux-s390x-gnu': 4.57.1 2191 + '@rollup/rollup-linux-x64-gnu': 4.57.1 2192 + '@rollup/rollup-linux-x64-musl': 4.57.1 2193 + '@rollup/rollup-openbsd-x64': 4.57.1 2194 + '@rollup/rollup-openharmony-arm64': 4.57.1 2195 + '@rollup/rollup-win32-arm64-msvc': 4.57.1 2196 + '@rollup/rollup-win32-ia32-msvc': 4.57.1 2197 + '@rollup/rollup-win32-x64-gnu': 4.57.1 2198 + '@rollup/rollup-win32-x64-msvc': 4.57.1 2199 + fsevents: 2.3.3 2200 + 2201 run-parallel@1.2.0: 2202 dependencies: 2203 queue-microtask: 1.2.3 ··· 2208 2209 shebang-regex@3.0.0: {} 2210 2211 + siginfo@2.0.0: {} 2212 + 2213 signal-exit@4.1.0: {} 2214 + 2215 + source-map-js@1.2.1: {} 2216 2217 source-map-support@0.5.21: 2218 dependencies: ··· 2221 2222 source-map@0.6.1: {} 2223 2224 + stackback@0.0.2: {} 2225 + 2226 + std-env@3.10.0: {} 2227 + 2228 supports-color@7.2.0: 2229 dependencies: 2230 has-flag: 4.0.0 2231 + 2232 + tinybench@2.9.0: {} 2233 + 2234 + tinyexec@1.0.2: {} 2235 + 2236 + tinyglobby@0.2.15: 2237 + dependencies: 2238 + fdir: 6.5.0(picomatch@4.0.3) 2239 + picomatch: 4.0.3 2240 + 2241 + tinyrainbow@3.0.3: {} 2242 2243 tlds@1.261.0: {} 2244 ··· 2303 2304 unicode-segmenter@0.14.5: {} 2305 2306 + vite@7.3.1(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2): 2307 + dependencies: 2308 + esbuild: 0.27.3 2309 + fdir: 6.5.0(picomatch@4.0.3) 2310 + picomatch: 4.0.3 2311 + postcss: 8.5.6 2312 + rollup: 4.57.1 2313 + tinyglobby: 0.2.15 2314 + optionalDependencies: 2315 + '@types/node': 22.19.9 2316 + fsevents: 2.3.3 2317 + tsx: 4.21.0 2318 + yaml: 2.8.2 2319 + 2320 + vitest@4.0.18(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2): 2321 + dependencies: 2322 + '@vitest/expect': 4.0.18 2323 + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2)) 2324 + '@vitest/pretty-format': 4.0.18 2325 + '@vitest/runner': 4.0.18 2326 + '@vitest/snapshot': 4.0.18 2327 + '@vitest/spy': 4.0.18 2328 + '@vitest/utils': 4.0.18 2329 + es-module-lexer: 1.7.0 2330 + expect-type: 1.3.0 2331 + magic-string: 0.30.21 2332 + obug: 2.1.1 2333 + pathe: 2.0.3 2334 + picomatch: 4.0.3 2335 + std-env: 3.10.0 2336 + tinybench: 2.9.0 2337 + tinyexec: 1.0.2 2338 + tinyglobby: 0.2.15 2339 + tinyrainbow: 3.0.3 2340 + vite: 7.3.1(@types/node@22.19.9)(tsx@4.21.0)(yaml@2.8.2) 2341 + why-is-node-running: 2.3.0 2342 + optionalDependencies: 2343 + '@types/node': 22.19.9 2344 + transitivePeerDependencies: 2345 + - jiti 2346 + - less 2347 + - lightningcss 2348 + - msw 2349 + - sass 2350 + - sass-embedded 2351 + - stylus 2352 + - sugarss 2353 + - terser 2354 + - tsx 2355 + - yaml 2356 + 2357 which@2.0.2: 2358 dependencies: 2359 isexe: 2.0.0 2360 + 2361 + why-is-node-running@2.3.0: 2362 + dependencies: 2363 + siginfo: 2.0.0 2364 + stackback: 0.0.2 2365 2366 yaml@2.8.2: {} 2367
+3
turbo.json
··· 14 "lint": { 15 "dependsOn": ["^build"] 16 }, 17 "clean": { 18 "cache": false 19 }
··· 14 "lint": { 15 "dependsOn": ["^build"] 16 }, 17 + "test": { 18 + "dependsOn": ["^build"] 19 + }, 20 "clean": { 21 "cache": false 22 }
+8
vitest.workspace.ts
···
··· 1 + import { defineWorkspace } from "vitest/config"; 2 + 3 + export default defineWorkspace([ 4 + "apps/appview", 5 + "apps/web", 6 + "packages/db", 7 + "packages/lexicon", 8 + ]);