diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 85c3d1d28..a1e252453 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -51,12 +51,14 @@ jobs: uses: actions/checkout@v6 with: persist-credentials: false + fetch-depth: 0 - name: Checkout Repository (Blacksmith) if: ${{ vars.USE_BLACKSMITH == 'true' }} uses: useblacksmith/checkout@v1 with: persist-credentials: false + fetch-depth: 0 - name: Install pnpm uses: pnpm/action-setup@v6 @@ -67,9 +69,23 @@ jobs: node-version-file: ".nvmrc" cache: "pnpm" + - name: Cache Turbo artifacts + uses: actions/cache@v5 + with: + path: .turbo/cache + key: ${{ runner.os }}-${{ runner.arch }}-turbo-${{ github.workflow }}-${{ hashFiles('pnpm-lock.yaml', '.nvmrc') }}-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-turbo-${{ github.workflow }}-${{ hashFiles('pnpm-lock.yaml', '.nvmrc') }}- + - name: Install Dependencies run: pnpm install --frozen-lockfile + - name: Check Package Boundaries + run: pnpm exec turbo boundaries + + - name: Typecheck Affected Packages + run: pnpm exec turbo run typecheck --affected + - name: Install Playwright Browser timeout-minutes: 10 run: pnpm exec playwright install --with-deps chromium diff --git a/.github/workflows/vercel.yml b/.github/workflows/vercel.yml index 78c47c0de..c41f4ea69 100644 --- a/.github/workflows/vercel.yml +++ b/.github/workflows/vercel.yml @@ -49,6 +49,14 @@ jobs: with: node-version-file: .nvmrc cache: pnpm + - name: Cache Turbo artifacts + uses: actions/cache@v5 + with: + path: .turbo/cache + key: ${{ runner.os }}-${{ runner.arch }}-turbo-${{ github.workflow }}-${{ hashFiles('pnpm-lock.yaml', '.nvmrc') }}-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-${{ runner.arch }}-turbo-${{ github.workflow }}-${{ hashFiles('pnpm-lock.yaml', '.nvmrc') }}- + - run: pnpm install --frozen-lockfile # Local project settings avoid authentication and API calls. Forks receive no cloud credentials. - name: Build Vercel artifact against isolated PostgreSQL diff --git a/AGENTS.md b/AGENTS.md index 0e6a90e61..94bf2fc28 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,6 +49,8 @@ Internal packages are source-consumed through `package.json` export maps pointin Prerequisites: **Node.js 24** (`.nvmrc`, root `engines`, and Dockerfile), **pnpm 12.8.1** (root `packageManager`; pnpm self-manages to this version), and **Docker with Docker Compose** for local infrastructure. The Dockerfile's `ARG PNPM_VERSION` chooses its base image, not the project's pnpm version. Start your Docker daemon before running Compose. +Shared dependency versions live in the default `catalog` in `pnpm-workspace.yaml`. Use `catalog:` in workspace manifests when that shared range applies; keep intentional exact pins and peer dependency ranges explicit. + Run commands from the workspace root unless stated otherwise: ```sh @@ -135,7 +137,7 @@ Multi-place changes: - **Resume data shape**: `packages/schema/src/resume/*` first, then API DTOs, importers, PDF rendering, and web forms consuming it. - **New template**: `packages/schema/src/templates.ts`, `packages/pdf/src/templates/index.ts`, source under `packages/pdf/src/templates//`, and previews under `apps/web/public/templates/{jpg,pdf}`. - **New DB column/table**: `packages/db/src/schema/*`, then `pnpm db:generate`. -- **New env var**: `packages/env/src/server.ts`, `.env.example`, **and** the `globalEnv` array in `turbo.json`. Add deployment aliases in `packages/env/src/deployment.ts` when needed. Turborepo strict env mode filters unlisted injected variables from task processes. +- **New env var**: `packages/env/src/server.ts`, `.env.example`, **and** the `globalPassThroughEnv` array and applicable test-task `env` arrays in `turbo.json`. Add deployment aliases in `packages/env/src/deployment.ts` when needed. Turborepo strict env mode filters unlisted injected variables from task processes. ## Environment and database @@ -163,7 +165,7 @@ pnpm exec turbo boundaries - Vitest tests live alongside source as `src/**/*.test.ts(x)` or `src/**/*.spec.ts(x)` (including integration tests). Paths under `pnpm --filter ` are package-relative. Pass paths directly after `test`: an extra `--` currently prevents Vitest from filtering the run. Shared settings live in `vitest.shared.mts` and setup in `vitest.setup.ts`; most packages use Node, while `packages/ui` uses `happy-dom`. - Coverage uses V8 and writes package-local `coverage/` reports. No shared minimum coverage threshold is configured. `test:ci` writes JSON/JUnit results under package-local `reports/`. -- Root `pnpm test`, `pnpm test:coverage`, and `pnpm typecheck` run workspace checks through Turbo. CI runs `pnpm exec turbo run test:ci --concurrency=1` to avoid CPU contention in PDF/rate-limit suites. +- Root `pnpm test`, `pnpm test:coverage`, and `pnpm typecheck` run workspace checks through Turbo. CI checks boundaries and affected-package typechecks, then runs all unit suites with `pnpm exec turbo run test:ci --concurrency=1` to avoid CPU contention in PDF/rate-limit suites. Unit/browser and Vercel workflows persist `.turbo/cache`; cached coverage and test reports restore to package-local output directories. - Real-database unit suites use `COVER_LETTER_TEST_DATABASE_URL` and `OAUTH_TEST_DATABASE_URL`; see `.github/workflows/e2e.yml` for isolated database setup. Never point test fixtures at production data. - After changing shared contracts, exports, or imports, check affected consumers and run `pnpm exec turbo boundaries`. diff --git a/Dockerfile b/Dockerfile index 89889a2ab..bac53fb42 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,10 +3,12 @@ # Base image only; pnpm self-manages to the `packageManager` version in package.json. ARG PNPM_VERSION=11.21.0 ARG NODE_VERSION=24 +ARG TURBO_VERSION=2.11.5 FROM ghcr.io/pnpm/pnpm:${PNPM_VERSION} AS base ARG NODE_VERSION +ARG TURBO_VERSION RUN pnpm runtime set node ${NODE_VERSION} -g --config.store-dir=/pnpm/runtime-store @@ -17,7 +19,7 @@ ENV TURBO_TELEMETRY_DISABLED=1 FROM base AS pruner COPY . . RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store,sharing=locked \ - pnpm dlx turbo@2.9.12 prune web server --docker + pnpm dlx turbo@${TURBO_VERSION} prune web server --docker FROM base AS builder COPY --from=pruner /app/out/json/ ./ @@ -31,7 +33,7 @@ RUN rm -rf apps/web/dist apps/web/dist-prerender apps/server/dist && pnpm turbo FROM base AS runtime-pruner COPY . . RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store,sharing=locked \ - pnpm dlx turbo@2.9.12 prune server --docker + pnpm dlx turbo@${TURBO_VERSION} prune server --docker FROM base AS runtime-deps COPY --from=runtime-pruner /app/out/json/ ./ diff --git a/apps/server/package.json b/apps/server/package.json index 047b718b0..dc67b7634 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -29,21 +29,21 @@ "@ai-sdk/togetherai": "^3.0.60", "@ai-sdk/xai": "^5.0.12", "@aws-sdk/client-s3": "^3.1143.0", - "@better-auth/api-key": "^1.7.6", + "@better-auth/api-key": "catalog:", "@better-auth/drizzle-adapter": "^1.7.6", - "@better-auth/infra": "^0.4.13", - "@better-auth/oauth-provider": "^1.7.6", - "@better-auth/passkey": "^1.7.6", + "@better-auth/infra": "catalog:", + "@better-auth/oauth-provider": "catalog:", + "@better-auth/passkey": "catalog:", "@bramus/specificity": "^2.4.2", "@formepdf/core": "0.25.0", "@formepdf/react": "0.25.0", "@hono/node-server": "^2.1.3", "@modelcontextprotocol/sdk": "^1.31.0", - "@orpc/client": "^1.15.4", + "@orpc/client": "catalog:", "@orpc/experimental-ratelimit": "^1.15.4", "@orpc/json-schema": "^1.15.4", "@orpc/openapi": "^1.15.4", - "@orpc/server": "^1.15.4", + "@orpc/server": "catalog:", "@orpc/zod": "^1.15.4", "@reactive-resume/api": "workspace:*", "@reactive-resume/auth": "workspace:*", @@ -54,50 +54,50 @@ "@reactive-resume/utils": "workspace:*", "@sindresorhus/slugify": "^3.0.1", "@t3-oss/env-core": "^0.13.11", - "@uiw/color-convert": "^2.10.3", + "@uiw/color-convert": "catalog:", "@vercel/blob": "^2.8.0", "@vercel/functions": "^3.9.9", - "ai": "^7.0.122", - "bcryptjs": "^3.0.3", - "better-auth": "1.7.6", + "ai": "catalog:", + "bcryptjs": "catalog:", + "better-auth": "catalog:", "cjk-regex": "^3.5.0", "css-tree": "^3.2.1", - "drizzle-orm": "1.0.0-rc.4", + "drizzle-orm": "catalog:", "drizzle-zod": "1.0.0-beta.14-a36c63d", - "es-toolkit": "^1.52.0", + "es-toolkit": "catalog:", "fast-json-patch": "^3.1.1", "fast-png": "^8.0.0", - "fflate": "^0.8.3", + "fflate": "catalog:", "firecrawl": "^4.42.0", "hono": "^4.13.11", - "ioredis": "^6.0.0", + "ioredis": "catalog:", "jose": "^6.2.12", - "jsonrepair": "^3.15.0", + "jsonrepair": "catalog:", "node-html-parser": "^9.0.4", "nodemailer": "^10.0.12", "ollama-ai-provider-v2": "^4.0.1", - "pg": "^8.23.0", - "react": "^19.3.0", + "pg": "catalog:", + "react": "catalog:", "react-email": "^6.11.0", "react-reconciler": "0.34.0", "resumable-stream": "^2.2.13", "sanitize-html": "^2.17.7", "sharp": "^0.35.5", "tokenx": "^2.1.0", - "ts-pattern": "^5.9.0", + "ts-pattern": "catalog:", "unique-names-generator": "^4.7.1", "uuid": "^14.0.2", - "zod": "^4.6.5" + "zod": "catalog:" }, "devDependencies": { "@reactive-resume/config": "workspace:*", - "@types/node": "^26.6.3", - "@types/pg": "^8.23.1", - "@types/react": "^19.3.0", - "@typescript/native-preview": "7.0.0-dev.20260707.2", + "@types/node": "catalog:", + "@types/pg": "catalog:", + "@types/react": "catalog:", + "@typescript/native-preview": "catalog:", "tsdown": "^0.23.0", "tsx": "^4.23.15", - "typescript": "^7.0.2", - "vitest": "^5.0.2" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/apps/server/turbo.json b/apps/server/turbo.json index 6b0b91d68..be0f0ad5a 100644 --- a/apps/server/turbo.json +++ b/apps/server/turbo.json @@ -2,12 +2,18 @@ "extends": ["//"], "tags": ["app:server", "runtime:server", "role:adapter"], "tasks": { - "test": { "env": ["OAUTH_TEST_DATABASE_URL"] }, - "test:coverage": { "env": ["OAUTH_TEST_DATABASE_URL"] }, - "test:agent": { "env": ["OAUTH_TEST_DATABASE_URL"] }, + "test": { + "env": ["$TURBO_EXTENDS$", "OAUTH_TEST_DATABASE_URL"] + }, + "test:coverage": { + "env": ["$TURBO_EXTENDS$", "OAUTH_TEST_DATABASE_URL"] + }, + "test:agent": { + "env": ["$TURBO_EXTENDS$", "OAUTH_TEST_DATABASE_URL"] + }, "test:ci": { "cache": false, - "env": ["OAUTH_TEST_DATABASE_URL"] + "env": ["$TURBO_EXTENDS$", "OAUTH_TEST_DATABASE_URL"] } } } diff --git a/apps/web/package.json b/apps/web/package.json index 2e5dfe84b..14e92edf8 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -18,10 +18,10 @@ "dependencies": { "@ai-sdk/react": "^4.0.125", "@base-ui/react": "^1.8.0", - "@better-auth/api-key": "^1.7.6", - "@better-auth/infra": "^0.4.13", - "@better-auth/oauth-provider": "^1.7.6", - "@better-auth/passkey": "^1.7.6", + "@better-auth/api-key": "catalog:", + "@better-auth/infra": "catalog:", + "@better-auth/oauth-provider": "catalog:", + "@better-auth/passkey": "catalog:", "@codemirror/autocomplete": "^6.20.3", "@codemirror/commands": "^6.11.1", "@codemirror/lang-css": "^6.3.1", @@ -35,8 +35,8 @@ "@lezer/highlight": "^1.2.5", "@lingui/core": "^6.8.0", "@lingui/react": "^6.8.0", - "@orpc/client": "^1.15.4", - "@orpc/server": "^1.15.4", + "@orpc/client": "catalog:", + "@orpc/server": "catalog:", "@orpc/tanstack-query": "^1.15.4", "@phosphor-icons/react": "^2.1.10", "@reactive-resume/ai": "workspace:*", @@ -66,13 +66,13 @@ "@tiptap/react": "^3.31.3", "@tiptap/starter-kit": "^3.31.3", "@types/js-cookie": "^3.0.6", - "@uiw/color-convert": "^2.10.3", + "@uiw/color-convert": "catalog:", "@uiw/react-color-colorful": "^2.10.3", - "ai": "^7.0.122", - "better-auth": "1.7.6", + "ai": "catalog:", + "better-auth": "catalog:", "cmdk": "^1.1.1", - "es-toolkit": "^1.52.0", - "fflate": "^0.8.3", + "es-toolkit": "catalog:", + "fflate": "catalog:", "fuse.js": "^7.5.0", "immer": "^11.1.18", "js-cookie": "^3.0.8", @@ -80,15 +80,15 @@ "pdfjs-dist": "6.3.289", "prettier": "^3.9.9", "qrcode.react": "^4.2.0", - "react": "^19.3.0", + "react": "catalog:", "react-dom": "^19.3.0", "react-easy-crop": "^6.2.3", "react-markdown": "^10.1.0", "react-window": "^2.3.3", "remark-gfm": "^4.0.1", - "ts-pattern": "^5.9.0", + "ts-pattern": "catalog:", "usehooks-ts": "^3.1.1", - "zod": "^4.6.5", + "zod": "catalog:", "zustand": "^5.0.15" }, "devDependencies": { @@ -105,13 +105,13 @@ "@tanstack/react-router-devtools": "^1.167.2", "@tanstack/router-plugin": "^1.168.41", "@types/babel__core": "^7.20.5", - "@types/react": "^19.3.0", - "@types/react-dom": "^19.3.0", - "@typescript/native-preview": "7.0.0-dev.20260707.2", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@typescript/native-preview": "catalog:", "@vitejs/plugin-react": "^6.1.1", "babel-plugin-macros": "^3.1.0", "babel-plugin-react-compiler": "^1.0.0", - "typescript": "^7.0.2", + "typescript": "catalog:", "vite": "^8.3.1" } } diff --git a/apps/web/src/components/input/chip-input.tsx b/apps/web/src/components/input/chip-input.tsx index 6c6b5dc87..39b1d45d3 100644 --- a/apps/web/src/components/input/chip-input.tsx +++ b/apps/web/src/components/input/chip-input.tsx @@ -233,8 +233,10 @@ export function ChipInput({ const handleEdit = React.useCallback( (index: number) => { + const chip = chips[index]; + if (chip === undefined) return; setEditingIndex(index); - setInput(chips[index]); + setInput(chip); inputRef.current?.focus(); }, [chips], @@ -244,7 +246,7 @@ export function ChipInput({ (newOrder: string[]) => { if (editingIndex !== null) { const editingChip = chips[editingIndex]; - const newIndex = newOrder.indexOf(editingChip); + const newIndex = editingChip === undefined ? -1 : newOrder.indexOf(editingChip); if (newIndex !== -1 && newIndex !== editingIndex) { setEditingIndex(newIndex); } @@ -282,6 +284,7 @@ export function ChipInput({ if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) { const newOrder = [...chips]; const [removed] = newOrder.splice(oldIndex, 1); + if (removed === undefined) return; newOrder.splice(newIndex, 0, removed); handleReorder(newOrder); } @@ -307,7 +310,7 @@ export function ChipInput({ if (newValue.includes(",")) { const parts = newValue.split(","); addChips(parts.slice(0, -1)); - setInput(parts[parts.length - 1]); + setInput(parts.at(-1) ?? ""); } else { setInput(newValue); } diff --git a/apps/web/src/components/typography/font-display.tsx b/apps/web/src/components/typography/font-display.tsx index 7de228721..93b5155aa 100644 --- a/apps/web/src/components/typography/font-display.tsx +++ b/apps/web/src/components/typography/font-display.tsx @@ -6,7 +6,7 @@ interface FontDisplayProps { family: string; label: string; type: "standard" | "web"; - url?: string; + url?: string | undefined; } const loadedFonts = new Set(); diff --git a/apps/web/src/components/typography/get-next-weights.ts b/apps/web/src/components/typography/get-next-weights.ts index d499d24c3..335edf265 100644 --- a/apps/web/src/components/typography/get-next-weights.ts +++ b/apps/web/src/components/typography/get-next-weights.ts @@ -22,7 +22,7 @@ export function getNextWeights(fontFamily: string, single = false): Weight[] | n // candidateIndex: 0 (first), 1 (last) const lastIndex = uniqueWeights.length - 1; const candidate = weights.length === 0 ? uniqueWeights[0] : uniqueWeights[lastIndex]; - if (!selectedWeights.has(candidate)) { + if (candidate !== undefined && !selectedWeights.has(candidate)) { weights.push(candidate); selectedWeights.add(candidate); } else break; diff --git a/apps/web/src/components/ui/combobox.tsx b/apps/web/src/components/ui/combobox.tsx index 47f34541f..39a438b5d 100644 --- a/apps/web/src/components/ui/combobox.tsx +++ b/apps/web/src/components/ui/combobox.tsx @@ -31,8 +31,8 @@ type ComboboxOption = { type SingleComboboxProps = { options: ComboboxOption[]; - value?: TValue | null; - defaultValue?: TValue | null; + value?: TValue | null | undefined; + defaultValue?: TValue | null | undefined; onValueChange?: (value: TValue | null) => void; multiple?: false; disabled?: boolean; @@ -47,8 +47,8 @@ type SingleComboboxProps = { type MultiComboboxProps = { options: ComboboxOption[]; - value?: TValue[] | null; - defaultValue?: TValue[] | null; + value?: TValue[] | null | undefined; + defaultValue?: TValue[] | null | undefined; onValueChange?: (value: TValue[] | null) => void; multiple: true; disabled?: boolean; diff --git a/apps/web/src/features/applications/csv.ts b/apps/web/src/features/applications/csv.ts index 22d48ac1e..9c5c45efc 100644 --- a/apps/web/src/features/applications/csv.ts +++ b/apps/web/src/features/applications/csv.ts @@ -230,7 +230,8 @@ export function mapCsvToApplications(table: string[][], mapping?: readonly (CsvF const parsed = applicationStatusSchema.safeParse(stage === "rejected" ? "closed" : stage); if (parsed.success) record.status = parsed.data; } else if (field === "stageEnteredAt") { - record.stageEnteredAt = dateOnly(value); + const date = dateOnly(value); + if (date !== undefined) record.stageEnteredAt = date; } else record[field] = value as never; }); diff --git a/apps/web/src/features/assistant/chat.ts b/apps/web/src/features/assistant/chat.ts index 1be08a454..476769f34 100644 --- a/apps/web/src/features/assistant/chat.ts +++ b/apps/web/src/features/assistant/chat.ts @@ -1,5 +1,5 @@ import type { AgentUIMessage } from "@reactive-resume/ai/tools/agent-tool-contracts"; -import type { FileUIPart, UIMessage } from "ai"; +import type { ChatTransport, FileUIPart, UIMessage } from "ai"; import { useChat } from "@ai-sdk/react"; import { eventIteratorToUnproxiedDataStream } from "@orpc/client"; import { lastAssistantMessageIsCompleteWithToolCalls, parseJsonEventStream, uiMessageChunkSchema } from "ai"; @@ -84,9 +84,9 @@ export function useAssistantChat({ threadId, initialMessages, resume, context, o contextRef.current = context; }); - const transport = useMemo( + const transport = useMemo>( () => ({ - async sendMessages(options: { messages: UIMessage[]; abortSignal?: AbortSignal; body?: object }) { + async sendMessages(options) { const message = options.messages.at(-1); if (!message) throw new Error("No message to send."); const attachmentIds = attachmentIdsOf(options.body); @@ -94,7 +94,7 @@ export function useAssistantChat({ threadId, initialMessages, resume, context, o eventIteratorToUnproxiedDataStream( await streamClient.agent.messages.send( { threadId, message, context: contextRef.current, ...(attachmentIds ? { attachmentIds } : {}) }, - { signal: options.abortSignal }, + options.abortSignal ? { signal: options.abortSignal } : {}, ), ), ); diff --git a/apps/web/src/features/assistant/conversation.tsx b/apps/web/src/features/assistant/conversation.tsx index ccb610287..066df60a2 100644 --- a/apps/web/src/features/assistant/conversation.tsx +++ b/apps/web/src/features/assistant/conversation.tsx @@ -262,6 +262,11 @@ function toProposals( const output = part.output as ProposeEditsOutput | undefined; return (output?.edits ?? []).map((edit) => ({ ...edit, + target: { + sectionId: edit.target.sectionId, + field: edit.target.field, + ...(edit.target.itemId === undefined ? {} : { itemId: edit.target.itemId }), + }, location: document.locationOf(edit) ?? edit.location, status: statuses.get(edit.id) ?? edit.status, source: "assistant", diff --git a/apps/web/src/features/auth/components/social-auth.tsx b/apps/web/src/features/auth/components/social-auth.tsx index aee5bb17e..64062042f 100644 --- a/apps/web/src/features/auth/components/social-auth.tsx +++ b/apps/web/src/features/auth/components/social-auth.tsx @@ -56,7 +56,7 @@ function SocialAuthButtons({ providers }: SocialAuthButtonsProps) { const { callbackURL } = useSearch({ from: "/auth" }); const runSignIn = async ( - fn: () => Promise<{ data?: unknown; error: { message?: string } | null }>, + fn: () => Promise<{ data?: unknown; error: { message?: string | undefined } | null }>, isPasskey = false, ) => { const toastId = toast.add({ type: "loading", description: t`Signing in...` }); diff --git a/apps/web/src/features/command-palette/index.tsx b/apps/web/src/features/command-palette/index.tsx index 379e72715..098b52c11 100644 --- a/apps/web/src/features/command-palette/index.tsx +++ b/apps/web/src/features/command-palette/index.tsx @@ -19,7 +19,7 @@ import { useCommandPaletteStore } from "./store"; export function CommandPalette() { const inputRef = useRef(null); const commandRef = useRef(null); - const [selectedValue, setSelectedValue] = useState(); + const [selectedValue, setSelectedValue] = useState(""); const { open, search, pages, setOpen, setSearch, goBack } = useCommandPaletteStore(); const isFirstPage = pages.length === 0; diff --git a/apps/web/src/features/resume/builder/draft.test.ts b/apps/web/src/features/resume/builder/draft.test.ts index d80978ab7..e2024ddc5 100644 --- a/apps/web/src/features/resume/builder/draft.test.ts +++ b/apps/web/src/features/resume/builder/draft.test.ts @@ -3,7 +3,7 @@ import type { ResumeData } from "@reactive-resume/schema/resume/data"; import type { Resume } from "./draft"; import { act, renderHook } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, assert, beforeEach, describe, expect, it, vi } from "vitest"; import { i18n } from "@lingui/core"; import { sortSectionItemsByPeriod } from "@reactive-resume/resume/section-sort"; import { defaultResumeData } from "@reactive-resume/schema/resume/default"; @@ -165,10 +165,12 @@ describe("builder resume autosave", () => { settled = true; return blocked; }); + assert.exists(completions[0]); completions[0](withBasicsName(initial, "First")); await flushMicrotasks(); expect(settled).toBe(false); expect(orpcMocks.updateResume.mock.lastCall?.[0].data.basics.name).toBe("Latest"); + assert.exists(completions[1]); completions[1](withBasicsName(initial, "Latest")); expect(await result).toBe(false); expect(useResumeStore.getState().resume?.data.basics.name).toBe("Latest"); diff --git a/apps/web/src/features/resume/builder/draft.ts b/apps/web/src/features/resume/builder/draft.ts index 1527939fe..7ed7556fd 100644 --- a/apps/web/src/features/resume/builder/draft.ts +++ b/apps/web/src/features/resume/builder/draft.ts @@ -22,9 +22,9 @@ export type Resume = { isLocked: boolean; createdAt: Date; updatedAt: Date; - hasPassword?: boolean; - isPublic?: boolean; - showDownloadButtons?: boolean; + hasPassword?: boolean | undefined; + isPublic?: boolean | undefined; + showDownloadButtons?: boolean | undefined; /** The job application this resume was made for; Check's Job match reads its posting. */ applicationId?: string | null; }; @@ -57,7 +57,7 @@ type UpdateResumeDataOptions = { type ResumeStoreState = { resume: Resume | null; - resumeId?: string; + resumeId?: string | undefined; isReady: boolean; saveStatus: SaveStatus; // Undo/redo stacks of `ResumeData` references. Immer's immutable updates share structure, so a @@ -90,21 +90,21 @@ type Runtime = { queryClient?: QueryClient; hasPendingLocalChanges: boolean; isSaving: boolean; - pendingResume?: Resume; + pendingResume?: Resume | undefined; /** The last save failed; don't loop on it. Cleared by the next edit, Retry, reconnecting or success. */ saveFailed: boolean; - slowSaveToastId?: string; + slowSaveToastId?: string | undefined; syncResume: ReturnType void>>; beforeUnloadHandler?: () => void; onlineHandler?: () => void; - deferredRemoteResume?: Resume; - deferredFocusHandler?: () => void; + deferredRemoteResume?: Resume | undefined; + deferredFocusHandler?: (() => void) | undefined; /** This visit's saves share one autosave version in History. */ sessionId: string; }; type ResumeUpdateSubscriptionOptions = { - resumeId?: string; + resumeId?: string | undefined; onUpdate: (event: ResumeUpdateEvent) => Promise | void; onError?: (error: unknown) => void; }; diff --git a/apps/web/src/features/resume/editor/write/dates-field.tsx b/apps/web/src/features/resume/editor/write/dates-field.tsx index 1e77dd15e..9f15f0771 100644 --- a/apps/web/src/features/resume/editor/write/dates-field.tsx +++ b/apps/web/src/features/resume/editor/write/dates-field.tsx @@ -26,7 +26,7 @@ type MonthYearInputProps = DateSettings & { value: YearMonth | null; disabled?: boolean; /** Shown instead of the value, e.g. "Present" while the entry is ongoing. */ - placeholderValue?: string; + placeholderValue?: string | undefined; onCommit: (value: YearMonth | null) => void; onInvalid: (invalid: boolean) => void; }; @@ -84,7 +84,7 @@ function MonthYearInput({ type DatesFieldProps = DateSettings & { dates: ResumeDates; /** Awards, certifications and publications have one date. */ - single?: boolean; + single?: boolean | undefined; onChange: (dates: ResumeDates) => void; className?: string; }; diff --git a/apps/web/src/features/resume/editor/write/entries.tsx b/apps/web/src/features/resume/editor/write/entries.tsx index 06f22a17a..38134e0a3 100644 --- a/apps/web/src/features/resume/editor/write/entries.tsx +++ b/apps/web/src/features/resume/editor/write/entries.tsx @@ -42,7 +42,7 @@ function Text({ label, wide, autoFocus, -}: FieldSetProps & { field: string; label: ReactNode; wide?: boolean; autoFocus?: boolean }) { +}: FieldSetProps & { field: string; label: ReactNode; wide?: boolean; autoFocus?: boolean | undefined }) { return ( , "value" | "onChange"> & /** Checked after the field is first left, then as you type. */ validate?: Validator; hint?: ReactNode; - wide?: boolean; + wide?: boolean | undefined; }; /** A labelled text field. Errors appear after the first blur and say how to fix them. */ @@ -109,7 +109,7 @@ export function TextField({ /> } /> - + {hint && !error && {hint}} ); @@ -137,7 +137,7 @@ export function WebsiteField({ label, value, onChange, allowInlineLink = true }: setTouched(true)} /> } /> - + {allowInlineLink && ( diff --git a/apps/web/src/features/resume/export/use-resume-export.ts b/apps/web/src/features/resume/export/use-resume-export.ts index f77dfeae4..66847aa8e 100644 --- a/apps/web/src/features/resume/export/use-resume-export.ts +++ b/apps/web/src/features/resume/export/use-resume-export.ts @@ -74,11 +74,7 @@ export async function createExportFile( const data = getResumeExportData(resume.data, target); if (format === "pdf") { - return createResumePdfBlob( - data, - undefined, - target === "cover-letter" ? { includeCoverLetterHeader: options?.includeCoverLetterHeader } : undefined, - ); + return createResumePdfBlob(data, undefined, target === "cover-letter" ? options : undefined); } const resolveTitle = await createSectionTitleResolver(data); diff --git a/apps/web/src/features/resume/preview/pdf-canvas.tsx b/apps/web/src/features/resume/preview/pdf-canvas.tsx index 8dd91fc3b..257816b3c 100644 --- a/apps/web/src/features/resume/preview/pdf-canvas.tsx +++ b/apps/web/src/features/resume/preview/pdf-canvas.tsx @@ -26,14 +26,14 @@ type PdfCanvasDocumentProps = { type PdfCanvasPageProps = { caption?: ReactNode; - className?: string; + className?: string | undefined; overlay?: ReactNode; document: PDFDocumentProxy; onLoadSuccess: (pageNumber: number, pageSize: PreviewPageSize) => void; onRenderSuccess?: () => void; pageNumber: number; pageScale: number; - pageSize?: PreviewPageSize; + pageSize?: PreviewPageSize | undefined; showPageNumbers: boolean; totalPages: number; }; diff --git a/apps/web/src/features/resume/preview/pdfjs-legacy-entrypoints.test.ts b/apps/web/src/features/resume/preview/pdfjs-legacy-entrypoints.test.ts index 5b64a7e36..f04e54974 100644 --- a/apps/web/src/features/resume/preview/pdfjs-legacy-entrypoints.test.ts +++ b/apps/web/src/features/resume/preview/pdfjs-legacy-entrypoints.test.ts @@ -42,7 +42,7 @@ vi.mock("pdfjs-dist/legacy/build/pdf.mjs", () => pdfjsMock.legacyModule); describe("PDF.js browser entrypoints", () => { beforeEach(() => { - pdfjsMock.legacyModule.GlobalWorkerOptions.workerSrc = undefined; + delete pdfjsMock.legacyModule.GlobalWorkerOptions.workerSrc; pdfjsMock.legacyModule.getDocument.mockClear(); pdfjsMock.loadingTask.destroy.mockClear(); pdfjsMock.pdfDocument.getPage.mockClear(); diff --git a/apps/web/src/features/resume/preview/preview.browser.tsx b/apps/web/src/features/resume/preview/preview.browser.tsx index 024042d5e..5eca93e6c 100644 --- a/apps/web/src/features/resume/preview/preview.browser.tsx +++ b/apps/web/src/features/resume/preview/preview.browser.tsx @@ -1,6 +1,6 @@ import type { PageMap } from "@reactive-resume/pdf/page-map"; import type { Template } from "@reactive-resume/schema/templates"; -import type { CSSProperties } from "react"; +import type { MotionStyle } from "motion/react"; import type { ResolvedResumePreviewProps } from "./preview.shared"; import type { PreviewPageSize } from "./preview.shared.utils"; import { t } from "@lingui/core/macro"; @@ -217,7 +217,7 @@ export function ResumePreviewClient({ key={visiblePdf.id} aria-hidden={visiblePdf.phase !== "active"} data-resume-preview-template={visiblePdf.template} - style={{ "--resume-preview-page-gap": resolvedPageGap } as CSSProperties} + style={{ "--resume-preview-page-gap": resolvedPageGap } as MotionStyle} className={cn("col-start-1 row-start-1", visiblePdf.phase !== "active" && "pointer-events-none")} initial={{ opacity: visiblePdf.phase === "active" ? 1 : 0 }} animate={{ opacity: visiblePdf.phase === "active" ? 1 : 0 }} diff --git a/apps/web/src/features/resume/preview/preview.shared.tsx b/apps/web/src/features/resume/preview/preview.shared.tsx index 47eaeb7b8..43f5c3a81 100644 --- a/apps/web/src/features/resume/preview/preview.shared.tsx +++ b/apps/web/src/features/resume/preview/preview.shared.tsx @@ -7,11 +7,11 @@ import { DEFAULT_PDF_PAGE_SIZE, getResumePreviewGapValue, getScaledPreviewPageSi export type ResumePreviewProps = { className?: string; - data?: ResumeData; + data?: ResumeData | undefined; pageGap?: CSSProperties["gap"]; pageLayout?: "horizontal" | "vertical"; pageScale?: number; - pageClassName?: string; + pageClassName?: string | undefined; showPageNumbers?: boolean; /** Letters show the sender's header, which a document with only a letter otherwise leaves out. */ includeCoverLetterHeader?: boolean; diff --git a/apps/web/src/features/resume/preview/resume-accessible-text.test.tsx b/apps/web/src/features/resume/preview/resume-accessible-text.test.tsx index 6c5d232bb..b6a946fdf 100644 --- a/apps/web/src/features/resume/preview/resume-accessible-text.test.tsx +++ b/apps/web/src/features/resume/preview/resume-accessible-text.test.tsx @@ -1,6 +1,6 @@ // @vitest-environment happy-dom import { render, screen } from "@testing-library/react"; -import { beforeAll, expect, it, vi } from "vitest"; +import { assert, beforeAll, expect, it, vi } from "vitest"; import { i18n } from "@lingui/core"; import { I18nProvider } from "@lingui/react"; import { sampleResumeData } from "@reactive-resume/schema/resume/sample"; @@ -19,9 +19,11 @@ function renderAccessibleText(data: typeof sampleResumeData) { it("exposes entry and subordinate-role headings with safe nested rich-text lists", () => { // Characterization before this change: item labels had no heading elements and rich-text lists were flattened into one paragraph. const data = structuredClone(sampleResumeData); + const item = data.sections.experience.items[0]; + assert.exists(item); data.sections.experience.items = [ { - ...data.sections.experience.items[0], + ...item, id: "hierarchy-item", company: "Acme Company", position: "", @@ -37,7 +39,7 @@ it("exposes entry and subordinate-role headings with safe nested rich-text lists ], }, { - ...data.sections.experience.items[0], + ...item, id: "blank-primary-item", company: "", position: "", diff --git a/apps/web/src/features/resume/preview/resume-accessible-text.tsx b/apps/web/src/features/resume/preview/resume-accessible-text.tsx index 5ba71338f..262ea88bc 100644 --- a/apps/web/src/features/resume/preview/resume-accessible-text.tsx +++ b/apps/web/src/features/resume/preview/resume-accessible-text.tsx @@ -331,7 +331,7 @@ type AccessibleSectionProps = { title: string; hidden: boolean; items: CustomSectionItem[]; - keywordLayout?: "inline" | "list"; + keywordLayout?: "inline" | "list" | undefined; }; function AccessibleSection({ type, title, hidden, items, keywordLayout }: AccessibleSectionProps) { @@ -353,7 +353,7 @@ function AccessibleSection({ type, title, hidden, items, keywordLayout }: Access } type ResumeAccessibleTextProps = { - data?: ResumeData; + data?: ResumeData | undefined; }; /** diff --git a/apps/web/src/features/resume/share/format.ts b/apps/web/src/features/resume/share/format.ts index 24d74d3cf..c672cd384 100644 --- a/apps/web/src/features/resume/share/format.ts +++ b/apps/web/src/features/resume/share/format.ts @@ -30,7 +30,7 @@ const UNITS = [ /** Time since a moment in the largest whole unit, written short: "5m", "2h", "3d", "6w", "1y". */ export function formatTimeSince(date: Date, locale: string, now = Date.now()) { const elapsed = Math.max(0, now - date.getTime()); - const { unit, ms } = UNITS.find(({ ms, below }) => elapsed / ms < below) ?? UNITS[UNITS.length - 1]; + const { unit, ms } = UNITS.find(({ ms, below }) => elapsed / ms < below) ?? UNITS[4]; return new Intl.NumberFormat(locale, { style: "unit", unit, unitDisplay: "narrow" }).format(Math.floor(elapsed / ms)); } diff --git a/apps/web/src/hooks/use-controlled-state.tsx b/apps/web/src/hooks/use-controlled-state.tsx index 0c2d0eaf4..2daa4e73a 100644 --- a/apps/web/src/hooks/use-controlled-state.tsx +++ b/apps/web/src/hooks/use-controlled-state.tsx @@ -1,12 +1,12 @@ import { useState } from "react"; interface CommonControlledStateProps { - value?: T; - defaultValue?: T; + value?: T | undefined; + defaultValue?: T | undefined; } type UseControlledStateProps = CommonControlledStateProps & { - onChange?: (value: T, ...args: Rest) => void; + onChange?: ((value: T, ...args: Rest) => void) | undefined; }; export function useControlledState( diff --git a/apps/web/src/libs/resume/move-item-round-trip.test.ts b/apps/web/src/libs/resume/move-item-round-trip.test.ts index 3f7887b6e..62f5d9240 100644 --- a/apps/web/src/libs/resume/move-item-round-trip.test.ts +++ b/apps/web/src/libs/resume/move-item-round-trip.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { assert, describe, expect, it } from "vitest"; import { i18n } from "@lingui/core"; import { produce } from "immer"; import { defaultResumeData } from "@reactive-resume/schema/resume/default"; @@ -41,12 +41,14 @@ describe("moving the last custom-section item (#3180)", () => { it("restores the original JSON after moving an experience item to a new page and back", () => { const initial = base(); const moved = split(); + const section = moved.customSections[0]; + assert.exists(section); expect(moved.metadata.layout.pages).toHaveLength(2); const restored = produce(moved, (draft) => { moveItem(draft, { itemId: "2", type: "experience", - customSectionId: moved.customSections[0].id, + customSectionId: section.id, target: { type: "section", sectionId: "experience" }, }); }); @@ -55,13 +57,16 @@ describe("moving the last custom-section item (#3180)", () => { it("preserves unrelated blank pages and pages with other section references", () => { const moved = produce(split(), (draft) => { draft.metadata.layout.pages.push({ fullWidth: true, main: [], sidebar: [] }); + assert.exists(draft.metadata.layout.pages[1]); draft.metadata.layout.pages[1].sidebar.push("skills"); }); + const section = moved.customSections[0]; + assert.exists(section); const restored = produce(moved, (draft) => { moveItem(draft, { itemId: "2", type: "experience", - customSectionId: moved.customSections[0].id, + customSectionId: section.id, target: { type: "section", sectionId: "experience" }, }); }); @@ -76,11 +81,13 @@ describe("moving the last custom-section item (#3180)", () => { const moved = produce(split(), (draft) => { draft.metadata.layout.pages.reverse(); }); + const section = moved.customSections[0]; + assert.exists(section); const restored = produce(moved, (draft) => { moveItem(draft, { itemId: "2", type: "experience", - customSectionId: moved.customSections[0].id, + customSectionId: section.id, target: { type: "section", sectionId: "experience" }, }); }); @@ -93,16 +100,19 @@ describe("moving the last custom-section item (#3180)", () => { const moved = produce(split(), (draft) => { draft.metadata.layout.pages.push({ fullWidth: true, main: [], sidebar: ["skills"] }); }); + const section = moved.customSections[0]; + assert.exists(section); const restored = produce(moved, (draft) => { moveItem(draft, { itemId: "2", type: "experience", - customSectionId: moved.customSections[0].id, + customSectionId: section.id, target: { type: "new-section", pageIndex: 2, title: "Later" }, }); }); expect(restored.metadata.layout.pages).toHaveLength(2); expect(restored.customSections).toHaveLength(1); + assert.exists(restored.customSections[0]); expect(restored.customSections[0]).toMatchObject({ title: "Later", items: [company("2")] }); expect(restored.metadata.layout.pages[1]).toEqual({ fullWidth: true, @@ -112,16 +122,20 @@ describe("moving the last custom-section item (#3180)", () => { }); it("keeps a custom section with hidden remaining items", () => { const moved = produce(split(), (draft) => { + assert.exists(draft.customSections[0]); draft.customSections[0].items.push({ ...company("3"), hidden: true }); }); + const section = moved.customSections[0]; + assert.exists(section); const restored = produce(moved, (draft) => { moveItem(draft, { itemId: "2", type: "experience", - customSectionId: moved.customSections[0].id, + customSectionId: section.id, target: { type: "section", sectionId: "experience" }, }); }); + assert.exists(restored.customSections[0]); expect(restored.customSections[0].items).toEqual([{ ...company("3"), hidden: true }]); expect(restored.metadata.layout.pages).toHaveLength(2); }); @@ -131,9 +145,11 @@ describe("moving the last custom-section item (#3180)", () => { { type: "new-section", pageIndex: 99, title: "Missing" }, ] as const)("leaves data intact for an invalid destination $type", (target) => { const moved = split(); + const section = moved.customSections[0]; + assert.exists(section); expect( produce(moved, (draft) => { - moveItem(draft, { itemId: "2", type: "experience", customSectionId: moved.customSections[0].id, target }); + moveItem(draft, { itemId: "2", type: "experience", customSectionId: section.id, target }); }), ).toEqual(moved); }); diff --git a/apps/web/src/libs/resume/move-item.ts b/apps/web/src/libs/resume/move-item.ts index 2c1e596d4..9532d8e67 100644 --- a/apps/web/src/libs/resume/move-item.ts +++ b/apps/web/src/libs/resume/move-item.ts @@ -81,8 +81,7 @@ export function getCompatibleMoveTargets( const customSectionById = new Map(resumeData.customSections.map((section) => [section.id, section])); const result: MoveTargetPage[] = []; - for (let pageIndex = 0; pageIndex < pages.length; pageIndex++) { - const page = pages[pageIndex]; + for (const [pageIndex, page] of pages.entries()) { const allSectionIds = [...page.main, ...page.sidebar]; const compatibleSections: MoveTargetSection[] = []; diff --git a/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/layout/pages.tsx b/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/layout/pages.tsx index 7505c5324..730150394 100644 --- a/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/layout/pages.tsx +++ b/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/layout/pages.tsx @@ -108,8 +108,7 @@ export function LayoutPages() { if (location) return location; // Search through all pages - for (let pageIndex = 0; pageIndex < layout.pages.length; pageIndex++) { - const page = layout.pages[pageIndex]; + for (const [pageIndex, page] of layout.pages.entries()) { const mainSections = new Set(page.main); const sidebarSections = new Set(page.sidebar); if (mainSections.has(id)) return { pageIndex, columnId: "main" }; @@ -141,6 +140,7 @@ export function LayoutPages() { // Same location, reorder within column if (activeLocation.pageIndex === overLocation.pageIndex && activeLocation.columnId === overLocation.columnId) { const page = layout.pages[activeLocation.pageIndex]; + if (!page) return; const items = page[activeLocation.columnId]; const oldIdx = items.indexOf(activeIdStr); let newIdx = items.indexOf(overIdStr); @@ -148,12 +148,9 @@ export function LayoutPages() { if (newIdx === -1) newIdx = items.length - 1; updateResumeData((draft) => { - const colOrder = draft.metadata.layout.pages[activeLocation.pageIndex][activeLocation.columnId]; - draft.metadata.layout.pages[activeLocation.pageIndex][activeLocation.columnId] = arrayMove( - colOrder, - oldIdx, - newIdx, - ); + const page = draft.metadata.layout.pages[activeLocation.pageIndex]; + if (!page) return; + page[activeLocation.columnId] = arrayMove(page[activeLocation.columnId], oldIdx, newIdx); }); return; } @@ -161,6 +158,7 @@ export function LayoutPages() { // Different location, move between columns/pages const fromPage = layout.pages[activeLocation.pageIndex]; const toPage = layout.pages[overLocation.pageIndex]; + if (!fromPage || !toPage) return; const fromItems = fromPage[activeLocation.columnId]; const toItems = toPage[overLocation.columnId]; const fromIdx = fromItems.indexOf(activeIdStr); @@ -172,6 +170,7 @@ export function LayoutPages() { updateResumeData((draft) => { const fromPageDraft = draft.metadata.layout.pages[activeLocation.pageIndex]; const toPageDraft = draft.metadata.layout.pages[overLocation.pageIndex]; + if (!fromPageDraft || !toPageDraft) return; const from = fromPageDraft[activeLocation.columnId]; const to = toPageDraft[overLocation.columnId]; @@ -201,6 +200,7 @@ export function LayoutPages() { // Find the first available page that isn't being deleted const targetPageIndex = pageIndex === 0 ? 1 : 0; const targetPage = draft.metadata.layout.pages[targetPageIndex]; + if (!pageToDelete || !targetPage) return; // Move all sections from deleted page to target page targetPage.main.push(...pageToDelete.main); @@ -216,6 +216,7 @@ export function LayoutPages() { (pageIndex: number, fullWidth: boolean) => { updateResumeData((draft) => { const page = draft.metadata.layout.pages[pageIndex]; + if (!page) return; page.fullWidth = fullWidth; if (fullWidth) { @@ -451,17 +452,20 @@ function MoveToSubmenu({ id, pageIndex, columnId }: MoveToSubmenuProps) { const moveTo = (targetPageIndex: number, targetColumnId: ColumnId) => { updateResumeData((draft) => { - const from = draft.metadata.layout.pages[pageIndex][columnId]; + const from = draft.metadata.layout.pages[pageIndex]?.[columnId]; + const to = draft.metadata.layout.pages[targetPageIndex]?.[targetColumnId]; + if (!from || !to) return; const index = from.indexOf(id); if (index === -1) return; from.splice(index, 1); - draft.metadata.layout.pages[targetPageIndex][targetColumnId].push(id); + to.push(id); }); }; const moveToNewPage = () => { updateResumeData((draft) => { - const from = draft.metadata.layout.pages[pageIndex][columnId]; + const from = draft.metadata.layout.pages[pageIndex]?.[columnId]; + if (!from) return; const index = from.indexOf(id); if (index === -1) return; from.splice(index, 1); diff --git a/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/page.tsx b/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/page.tsx index 27ec69344..5b0b6014c 100644 --- a/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/page.tsx +++ b/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/page.tsx @@ -11,7 +11,7 @@ import { } from "@reactive-resume/ui/components/input-group"; import { Switch } from "@reactive-resume/ui/components/switch"; import { Combobox } from "@/components/ui/combobox"; -import { useResume, useUpdateResumeData } from "@/features/resume/builder/draft"; +import { useCurrentResume, useUpdateResumeData } from "@/features/resume/builder/draft"; import { useSyncFormValues } from "@/hooks/use-sync-form-values"; import { useAppForm } from "@/libs/tanstack-form"; import { SectionBase } from "../shared/section-base"; @@ -34,8 +34,8 @@ const CLAMP_MAX = 100; const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); function PageSectionForm() { - const resume = useResume(); - const page = resume?.data.metadata.page; + const resume = useCurrentResume(); + const page = resume.data.metadata.page; const updateResumeData = useUpdateResumeData(); const persist = (data: FormValues) => { diff --git a/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/typography.tsx b/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/typography.tsx index 3ab859468..b0821d1b7 100644 --- a/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/typography.tsx +++ b/apps/web/src/routes/builder/$resumeId/-sidebar/right/sections/typography.tsx @@ -14,7 +14,7 @@ import { Separator } from "@reactive-resume/ui/components/separator"; import { Switch } from "@reactive-resume/ui/components/switch"; import { FontFamilyCombobox, FontWeightCombobox } from "@/components/typography/combobox"; import { getNextWeights } from "@/components/typography/get-next-weights"; -import { useResume, useUpdateResumeData } from "@/features/resume/builder/draft"; +import { useCurrentResume, useUpdateResumeData } from "@/features/resume/builder/draft"; import { useSyncFormValues } from "@/hooks/use-sync-form-values"; import { useAppForm } from "@/libs/tanstack-form"; import { SectionBase } from "../shared/section-base"; @@ -33,7 +33,7 @@ type FormValues = z.infer; type FontWeight = FormValues["body"]["fontWeights"][number]; type TypographyPrefix = "body" | "heading"; -function useTypographyForm(typography: FormValues | undefined, persist: (data: FormValues) => void) { +function useTypographyForm(typography: FormValues, persist: (data: FormValues) => void) { const form = useAppForm({ defaultValues: typography, validators: { onChange: formSchema }, @@ -48,8 +48,8 @@ function useTypographyForm(typography: FormValues | undefined, persist: (data: F type TypographyForm = ReturnType; function TypographySectionForm() { - const resume = useResume(); - const typography = resume?.data.metadata.typography; + const resume = useCurrentResume(); + const typography = resume.data.metadata.typography; const updateResumeData = useUpdateResumeData(); const persist = (data: FormValues) => { diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index dc200602e..091e6ca09 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,24 +1,13 @@ { + "extends": "@reactive-resume/config/tsconfig.base.json", "include": ["**/*.ts", "**/*.tsx"], "compilerOptions": { "target": "ES2022", "jsx": "react-jsx", - "module": "ESNext", "lib": ["ES2023", "DOM", "DOM.Iterable"], "types": ["vite/client", "@testing-library/jest-dom/vitest"], - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, "noEmit": true, - - /* Linting */ - "skipLibCheck": true, - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true, "paths": { "@/*": ["./src/*"] } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index a3114d6ff..2a6ca454c 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -82,8 +82,10 @@ export default defineConfig({ // `vite build` builds the app, then the marketing pages' server entry, then prerenders the pages with it. builder: { buildApp: async (builder) => { - await builder.build(builder.environments.client); - await builder.build(builder.environments.ssr); + const { client, ssr } = builder.environments; + if (!client || !ssr) throw new Error("Missing client or SSR build environment"); + await builder.build(client); + await builder.build(ssr); await prerenderPages(); }, }, diff --git a/docs/contributing/architecture.mdx b/docs/contributing/architecture.mdx index 6adf748df..d420dd9a6 100644 --- a/docs/contributing/architecture.mdx +++ b/docs/contributing/architecture.mdx @@ -75,6 +75,10 @@ The server checks the environment, applies database migrations, and verifies the Internal packages are consumed as source through the `exports` map in each `package.json`, which points at `src` files. Don't expect a `dist` folder unless a package builds one explicitly. +Shared dependency versions live in the default pnpm catalog in `pnpm-workspace.yaml`; workspace manifests use `catalog:` for those ranges. Apps and packages inherit strict TypeScript settings from `packages/config/tsconfig.base.json`. + +Turbo hashes shared Vitest configuration and setup files, tracks transitive source dependencies, and restores coverage and test reports from its artifact cache. Runtime variables remain available in strict environment mode through `globalPassThroughEnv`; task-level `env` declarations hash values that affect task results. CI checks package boundaries and typechecks affected packages, runs the full unit suite, and persists `.turbo/cache` between runs. + ## Where new code goes | You are changing | Put it here | @@ -89,7 +93,7 @@ Internal packages are consumed as source through the `exports` map in each `pack | An MCP tool, prompt, or resource | `packages/mcp` | | A generic UI primitive or hook | `packages/ui`; workflow-specific UI stays in its web feature | | A database column or table | `packages/db/src/schema/*`, then `pnpm db:generate` | -| A server environment variable | `packages/env/src/server.ts`, `.env.example`, and `globalEnv` in `turbo.json` | +| A server environment variable | `packages/env/src/server.ts`, `.env.example`, and `globalPassThroughEnv` plus applicable test-task `env` arrays in `turbo.json` | | A dev-only script | `tooling/` | A new template touches several places: `packages/schema/src/templates.ts`, `packages/pdf/src/templates/index.ts`, the template source under `packages/pdf/src/templates//`, and preview images under `apps/web/public/templates/{jpg,pdf}`. diff --git a/package.json b/package.json index 5be957a89..5494add9f 100644 --- a/package.json +++ b/package.json @@ -54,15 +54,15 @@ "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", "@testing-library/user-event": "^14.6.7", - "@types/node": "^26.6.3", - "@types/pg": "^8.23.1", + "@types/node": "catalog:", + "@types/pg": "catalog:", "@vitest/coverage-v8": "^5.0.2", "happy-dom": "^20.14.5", "knip": "^6.38.0", "lefthook": "^2.1.15", - "pg": "^8.23.0", + "pg": "catalog:", "turbo": "^2.11.5", - "typescript": "^7.0.2", - "vitest": "^5.0.2" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/ai/package.json b/packages/ai/package.json index dabedfd29..54bb725ec 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -22,13 +22,13 @@ "@reactive-resume/resume": "workspace:*", "@reactive-resume/schema": "workspace:*", "@reactive-resume/utils": "workspace:*", - "jsonrepair": "^3.15.0", - "zod": "^4.6.5" + "jsonrepair": "catalog:", + "zod": "catalog:" }, "devDependencies": { "@reactive-resume/config": "workspace:*", - "ai": "^7.0.122", - "@typescript/native-preview": "7.0.0-dev.20260707.2", - "typescript": "^7.0.2" + "ai": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" } } diff --git a/packages/api/package.json b/packages/api/package.json index 8e4fd5c62..c160288e1 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -38,9 +38,9 @@ "@ai-sdk/togetherai": "^3.0.60", "@ai-sdk/xai": "^5.0.12", "@aws-sdk/client-s3": "^3.1143.0", - "@orpc/client": "^1.15.4", + "@orpc/client": "catalog:", "@orpc/experimental-ratelimit": "^1.15.4", - "@orpc/server": "^1.15.4", + "@orpc/server": "catalog:", "@reactive-resume/ai": "workspace:*", "@reactive-resume/auth": "workspace:*", "@reactive-resume/db": "workspace:*", @@ -50,30 +50,30 @@ "@reactive-resume/schema": "workspace:*", "@reactive-resume/utils": "workspace:*", "@vercel/blob": "^2.8.0", - "ai": "^7.0.122", - "bcryptjs": "^3.0.3", - "better-auth": "1.7.6", - "drizzle-orm": "1.0.0-rc.4", + "ai": "catalog:", + "bcryptjs": "catalog:", + "better-auth": "catalog:", + "drizzle-orm": "catalog:", "drizzle-zod": "1.0.0-beta.14-a36c63d", - "fflate": "^0.8.3", + "fflate": "catalog:", "firecrawl": "^4.42.0", - "ioredis": "^6.0.0", - "jsonrepair": "^3.15.0", + "ioredis": "catalog:", + "jsonrepair": "catalog:", "ollama-ai-provider-v2": "^4.0.1", "resumable-stream": "^2.2.13", "sanitize-html": "^2.17.7", "sharp": "^0.35.5", "tokenx": "^2.1.0", - "ts-pattern": "^5.9.0", - "zod": "^4.6.5" + "ts-pattern": "catalog:", + "zod": "catalog:" }, "devDependencies": { "@reactive-resume/config": "workspace:*", - "@types/pg": "^8.23.1", + "@types/pg": "catalog:", "@types/sanitize-html": "^2.16.2", - "@typescript/native-preview": "7.0.0-dev.20260707.2", - "es-toolkit": "^1.52.0", - "pg": "^8.23.0", - "typescript": "^7.0.2" + "@typescript/native-preview": "catalog:", + "es-toolkit": "catalog:", + "pg": "catalog:", + "typescript": "catalog:" } } diff --git a/packages/api/turbo.json b/packages/api/turbo.json index e3c929bac..590c38377 100644 --- a/packages/api/turbo.json +++ b/packages/api/turbo.json @@ -2,12 +2,18 @@ "extends": ["//"], "tags": ["runtime:server", "role:api"], "tasks": { - "test": { "env": ["COVER_LETTER_TEST_DATABASE_URL", "INTEGRATIONS_TEST_DATABASE_URL"] }, - "test:coverage": { "env": ["COVER_LETTER_TEST_DATABASE_URL", "INTEGRATIONS_TEST_DATABASE_URL"] }, - "test:agent": { "env": ["COVER_LETTER_TEST_DATABASE_URL", "INTEGRATIONS_TEST_DATABASE_URL"] }, + "test": { + "env": ["$TURBO_EXTENDS$", "COVER_LETTER_TEST_DATABASE_URL", "INTEGRATIONS_TEST_DATABASE_URL"] + }, + "test:coverage": { + "env": ["$TURBO_EXTENDS$", "COVER_LETTER_TEST_DATABASE_URL", "INTEGRATIONS_TEST_DATABASE_URL"] + }, + "test:agent": { + "env": ["$TURBO_EXTENDS$", "COVER_LETTER_TEST_DATABASE_URL", "INTEGRATIONS_TEST_DATABASE_URL"] + }, "test:ci": { "cache": false, - "env": ["COVER_LETTER_TEST_DATABASE_URL", "INTEGRATIONS_TEST_DATABASE_URL"] + "env": ["$TURBO_EXTENDS$", "COVER_LETTER_TEST_DATABASE_URL", "INTEGRATIONS_TEST_DATABASE_URL"] } } } diff --git a/packages/auth/package.json b/packages/auth/package.json index 7607ee3bf..a9af15858 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -15,25 +15,25 @@ "test:agent": "vitest run --reporter=agent --reporter=json --outputFile.json=reports/vitest-results.json --passWithNoTests" }, "dependencies": { - "@better-auth/api-key": "^1.7.6", + "@better-auth/api-key": "catalog:", "@better-auth/drizzle-adapter": "^1.7.6", - "@better-auth/infra": "^0.4.13", - "@better-auth/oauth-provider": "^1.7.6", - "@better-auth/passkey": "^1.7.6", + "@better-auth/infra": "catalog:", + "@better-auth/oauth-provider": "catalog:", + "@better-auth/passkey": "catalog:", "@reactive-resume/db": "workspace:*", "@reactive-resume/email": "workspace:*", "@reactive-resume/env": "workspace:*", "@reactive-resume/utils": "workspace:*", - "bcryptjs": "^3.0.3", - "better-auth": "1.7.6", - "drizzle-orm": "1.0.0-rc.4", + "bcryptjs": "catalog:", + "better-auth": "catalog:", + "drizzle-orm": "catalog:", "jose": "^6.2.12", - "react": "^19.3.0" + "react": "catalog:" }, "devDependencies": { "@reactive-resume/config": "workspace:*", - "@types/react": "^19.3.0", - "@typescript/native-preview": "7.0.0-dev.20260707.2", - "typescript": "^7.0.2" + "@types/react": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" } } diff --git a/packages/db/package.json b/packages/db/package.json index 76690c752..c75916745 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -28,15 +28,15 @@ "@reactive-resume/env": "workspace:*", "@reactive-resume/schema": "workspace:*", "@reactive-resume/utils": "workspace:*", - "drizzle-orm": "1.0.0-rc.4", - "ioredis": "^6.0.0", - "pg": "^8.23.0" + "drizzle-orm": "catalog:", + "ioredis": "catalog:", + "pg": "catalog:" }, "devDependencies": { "@reactive-resume/config": "workspace:*", - "@types/pg": "^8.23.1", - "@typescript/native-preview": "7.0.0-dev.20260707.2", + "@types/pg": "catalog:", + "@typescript/native-preview": "catalog:", "drizzle-kit": "1.0.0-rc.4", - "typescript": "^7.0.2" + "typescript": "catalog:" } } diff --git a/packages/docx/package.json b/packages/docx/package.json index 6af7f7ad1..310461965 100644 --- a/packages/docx/package.json +++ b/packages/docx/package.json @@ -20,7 +20,7 @@ }, "devDependencies": { "@reactive-resume/config": "workspace:*", - "@typescript/native-preview": "7.0.0-dev.20260707.2", - "typescript": "^7.0.2" + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" } } diff --git a/packages/dsh-plugin/package.json b/packages/dsh-plugin/package.json index b74332e43..18114abdc 100644 --- a/packages/dsh-plugin/package.json +++ b/packages/dsh-plugin/package.json @@ -62,10 +62,10 @@ "@deepseek-ai/schemastery": "^3.18.4", "@reactive-resume/config": "workspace:*", "@reactive-resume/mcp": "workspace:*", - "@typescript/native-preview": "7.0.0-dev.20260707.2", + "@typescript/native-preview": "catalog:", "tsdown": "^0.23.0", - "typescript": "^7.0.2", - "vitest": "^5.0.2" + "typescript": "catalog:", + "vitest": "catalog:" }, "dsh": { "bundle": { diff --git a/packages/email/package.json b/packages/email/package.json index 73a5fdbad..4fd383239 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -18,15 +18,15 @@ "dependencies": { "@reactive-resume/env": "workspace:*", "nodemailer": "^10.0.12", - "react": "^19.3.0", + "react": "catalog:", "react-email": "^6.11.0" }, "devDependencies": { "@react-email/ui": "^6.11.0", "@reactive-resume/config": "workspace:*", "@types/nodemailer": "^8.0.2", - "@types/react": "^19.3.0", - "@typescript/native-preview": "7.0.0-dev.20260707.2", - "typescript": "^7.0.2" + "@types/react": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" } } diff --git a/packages/env/package.json b/packages/env/package.json index 0c4164151..343ccac4b 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -17,12 +17,12 @@ "@reactive-resume/ai": "workspace:*", "@reactive-resume/utils": "workspace:*", "@t3-oss/env-core": "^0.13.11", - "zod": "^4.6.5" + "zod": "catalog:" }, "devDependencies": { "@reactive-resume/config": "workspace:*", - "@types/node": "^26.6.3", - "@typescript/native-preview": "7.0.0-dev.20260707.2", - "typescript": "^7.0.2" + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" } } diff --git a/packages/fonts/package.json b/packages/fonts/package.json index a02310d2e..fc4da4902 100644 --- a/packages/fonts/package.json +++ b/packages/fonts/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@reactive-resume/config": "workspace:*", - "@typescript/native-preview": "7.0.0-dev.20260707.2", - "typescript": "^7.0.2" + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" } } diff --git a/packages/import/package.json b/packages/import/package.json index 371fd54e9..cad689baf 100644 --- a/packages/import/package.json +++ b/packages/import/package.json @@ -21,12 +21,12 @@ "@reactive-resume/resume": "workspace:*", "@reactive-resume/schema": "workspace:*", "@reactive-resume/utils": "workspace:*", - "fflate": "^0.8.3", - "zod": "^4.6.5" + "fflate": "catalog:", + "zod": "catalog:" }, "devDependencies": { "@reactive-resume/config": "workspace:*", - "@typescript/native-preview": "7.0.0-dev.20260707.2", - "typescript": "^7.0.2" + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" } } diff --git a/packages/mcp/package.json b/packages/mcp/package.json index e730005cb..d8f9a31c8 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -17,17 +17,17 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.31.0", - "@orpc/server": "^1.15.4", + "@orpc/server": "catalog:", "@reactive-resume/ai": "workspace:*", "@reactive-resume/api": "workspace:*", "@reactive-resume/env": "workspace:*", "@reactive-resume/schema": "workspace:*", - "zod": "^4.6.5" + "zod": "catalog:" }, "devDependencies": { "@reactive-resume/config": "workspace:*", - "@typescript/native-preview": "7.0.0-dev.20260707.2", - "typescript": "^7.0.2", - "vitest": "^5.0.2" + "@typescript/native-preview": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/pdf/package.json b/packages/pdf/package.json index 86e2e1e63..23e2da9c2 100644 --- a/packages/pdf/package.json +++ b/packages/pdf/package.json @@ -33,18 +33,18 @@ "@reactive-resume/utils": "workspace:*", "cjk-regex": "^3.5.0", "fast-png": "^8.0.0", - "fflate": "^0.8.3", + "fflate": "catalog:", "node-html-parser": "^9.0.4", - "react": "^19.3.0", + "react": "catalog:", "react-reconciler": "0.34.0" }, "devDependencies": { "@napi-rs/canvas": "1.0.9", "@reactive-resume/config": "workspace:*", - "@types/react": "^19.3.0", + "@types/react": "catalog:", "@types/react-reconciler": "^0.33.1", - "@typescript/native-preview": "7.0.0-dev.20260707.2", + "@typescript/native-preview": "catalog:", "pdfjs-dist": "6.3.289", - "typescript": "^7.0.2" + "typescript": "catalog:" } } diff --git a/packages/resume/package.json b/packages/resume/package.json index 6121695d7..69f0c59ad 100644 --- a/packages/resume/package.json +++ b/packages/resume/package.json @@ -32,12 +32,12 @@ "css-tree": "^3.2.1", "fast-json-patch": "^3.1.1", "wink-porter2-stemmer": "2.0.1", - "zod": "^4.6.5" + "zod": "catalog:" }, "devDependencies": { "@reactive-resume/config": "workspace:*", - "@typescript/native-preview": "7.0.0-dev.20260707.2", + "@typescript/native-preview": "catalog:", "fast-check": "^4.10.2", - "typescript": "^7.0.2" + "typescript": "catalog:" } } diff --git a/packages/schema/package.json b/packages/schema/package.json index 1af7a1211..19edc36cf 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -19,11 +19,11 @@ "test:agent": "vitest run --reporter=agent --reporter=json --outputFile.json=reports/vitest-results.json --passWithNoTests" }, "dependencies": { - "zod": "^4.6.5" + "zod": "catalog:" }, "devDependencies": { "@reactive-resume/config": "workspace:*", - "@typescript/native-preview": "7.0.0-dev.20260707.2", - "typescript": "^7.0.2" + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" } } diff --git a/packages/ui/package.json b/packages/ui/package.json index 12f31ba69..7a72cecd0 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -25,7 +25,7 @@ "@reactive-resume/utils": "workspace:*", "class-variance-authority": "^0.7.1", "cmdk": "^1.1.1", - "react": "^19.3.0", + "react": "catalog:", "react-dom": "^19.3.0", "shadcn": "^4.21.0", "tw-animate-css": "^1.4.0" @@ -34,11 +34,11 @@ "@reactive-resume/config": "workspace:*", "@tailwindcss/postcss": "^4.3.3", "@tailwindcss/typography": "^0.5.20", - "@types/react": "^19.3.0", - "@types/react-dom": "^19.3.0", - "@typescript/native-preview": "7.0.0-dev.20260707.2", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@typescript/native-preview": "catalog:", "postcss": "^8.5.28", "tailwindcss": "^4.3.3", - "typescript": "^7.0.2" + "typescript": "catalog:" } } diff --git a/packages/utils/package.json b/packages/utils/package.json index b0585c8d7..1ec2fd7bb 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -22,16 +22,16 @@ }, "dependencies": { "@sindresorhus/slugify": "^3.0.1", - "@uiw/color-convert": "^2.10.3", + "@uiw/color-convert": "catalog:", "cn": "^0.4.0", "unique-names-generator": "^4.7.1", "uuid": "^14.0.2", - "zod": "^4.6.5" + "zod": "catalog:" }, "devDependencies": { "@reactive-resume/config": "workspace:*", - "@types/node": "^26.6.3", - "@typescript/native-preview": "7.0.0-dev.20260707.2", - "typescript": "^7.0.2" + "@types/node": "catalog:", + "@typescript/native-preview": "catalog:", + "typescript": "catalog:" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 200d50da7..052a8a3ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,6 +162,87 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +catalogs: + default: + '@better-auth/api-key': + specifier: ^1.7.6 + version: 1.7.6 + '@better-auth/infra': + specifier: ^0.4.13 + version: 0.4.13 + '@better-auth/oauth-provider': + specifier: ^1.7.6 + version: 1.7.6 + '@better-auth/passkey': + specifier: ^1.7.6 + version: 1.7.6 + '@orpc/client': + specifier: ^1.15.4 + version: 1.15.4 + '@orpc/server': + specifier: ^1.15.4 + version: 1.15.4 + '@types/node': + specifier: ^26.6.3 + version: 26.6.3 + '@types/pg': + specifier: ^8.23.1 + version: 8.23.1 + '@types/react': + specifier: ^19.3.0 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.3.0 + version: 19.3.0 + '@typescript/native-preview': + specifier: 7.0.0-dev.20260707.2 + version: 7.0.0-dev.20260707.2 + '@uiw/color-convert': + specifier: ^2.10.3 + version: 2.10.3 + ai: + specifier: ^7.0.122 + version: 7.0.122 + bcryptjs: + specifier: ^3.0.3 + version: 3.0.3 + better-auth: + specifier: 1.7.6 + version: 1.7.6 + drizzle-orm: + specifier: 1.0.0-rc.4 + version: 1.0.0-rc.4 + es-toolkit: + specifier: ^1.52.0 + version: 1.52.0 + fflate: + specifier: ^0.8.3 + version: 0.8.3 + ioredis: + specifier: ^6.0.0 + version: 6.0.0 + jsonrepair: + specifier: ^3.15.0 + version: 3.15.0 + pg: + specifier: ^8.23.0 + version: 8.23.0 + react: + specifier: ^19.3.0 + version: 19.3.0 + ts-pattern: + specifier: ^5.9.0 + version: 5.9.0 + typescript: + specifier: ^7.0.2 + version: 7.0.2 + vitest: + specifier: ^5.0.2 + version: 5.0.2 + zod: + specifier: ^4.6.5 + version: 4.6.5 + overrides: '@better-auth/infra>@better-fetch/fetch': 1.3.2 esbuild@>=0.17.0 <0.28.1: ^0.28.1 @@ -201,10 +282,10 @@ importers: specifier: ^14.6.7 version: 14.6.7(@testing-library/dom@10.4.2) '@types/node': - specifier: ^26.6.3 + specifier: 'catalog:' version: 26.6.3 '@types/pg': - specifier: ^8.23.1 + specifier: 'catalog:' version: 8.23.1 '@vitest/coverage-v8': specifier: ^5.0.2 @@ -219,16 +300,16 @@ importers: specifier: ^2.1.15 version: 2.1.15 pg: - specifier: ^8.23.0 + specifier: 'catalog:' version: 8.23.0 turbo: specifier: ^2.11.5 version: 2.11.5 typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 vitest: - specifier: ^5.0.2 + specifier: 'catalog:' version: 5.0.2(@types/node@26.6.3)(@vitest/coverage-v8@5.0.2)(happy-dom@20.14.5)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) apps/server: @@ -276,19 +357,19 @@ importers: specifier: ^3.1143.0 version: 3.1143.0 '@better-auth/api-key': - specifier: ^1.7.6 + specifier: 'catalog:' version: 1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(better-auth@1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2))(better-call@1.4.0(zod@4.6.5)) '@better-auth/drizzle-adapter': specifier: ^1.7.6 version: 1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5)) '@better-auth/infra': - specifier: ^0.4.13 + specifier: 'catalog:' version: 0.4.13(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(better-auth@1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2))(zod@4.6.5) '@better-auth/oauth-provider': - specifier: ^1.7.6 + specifier: 'catalog:' version: 1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-auth@1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2))(better-call@1.4.0(zod@4.6.5)) '@better-auth/passkey': - specifier: ^1.7.6 + specifier: 'catalog:' version: 1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-auth@1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2))(better-call@1.4.0(zod@4.6.5))(nanostores@1.5.4) '@bramus/specificity': specifier: ^2.4.2 @@ -306,7 +387,7 @@ importers: specifier: ^1.31.0 version: 1.31.0(supports-color@7.2.0)(zod@4.6.5) '@orpc/client': - specifier: ^1.15.4 + specifier: 'catalog:' version: 1.15.4 '@orpc/experimental-ratelimit': specifier: ^1.15.4 @@ -318,7 +399,7 @@ importers: specifier: ^1.15.4 version: 1.15.4(ws@8.21.3) '@orpc/server': - specifier: ^1.15.4 + specifier: 'catalog:' version: 1.15.4(ws@8.21.3) '@orpc/zod': specifier: ^1.15.4 @@ -351,7 +432,7 @@ importers: specifier: ^0.13.11 version: 0.13.11(typescript@7.0.2)(zod@4.6.5) '@uiw/color-convert': - specifier: ^2.10.3 + specifier: 'catalog:' version: 2.10.3(@babel/runtime@7.29.7) '@vercel/blob': specifier: ^2.8.0 @@ -360,13 +441,13 @@ importers: specifier: ^3.9.9 version: 3.9.9(@aws-sdk/credential-provider-web-identity@3.972.78)(ws@8.21.3) ai: - specifier: ^7.0.122 + specifier: 'catalog:' version: 7.0.122(zod@4.6.5) bcryptjs: - specifier: ^3.0.3 + specifier: 'catalog:' version: 3.0.3 better-auth: - specifier: 1.7.6 + specifier: 'catalog:' version: 1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2) cjk-regex: specifier: ^3.5.0 @@ -375,13 +456,13 @@ importers: specifier: ^3.2.1 version: 3.2.1 drizzle-orm: - specifier: 1.0.0-rc.4 + specifier: 'catalog:' version: 1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5) drizzle-zod: specifier: 1.0.0-beta.14-a36c63d version: 1.0.0-beta.14-a36c63d(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(zod@4.6.5) es-toolkit: - specifier: ^1.52.0 + specifier: 'catalog:' version: 1.52.0 fast-json-patch: specifier: ^3.1.1 @@ -390,7 +471,7 @@ importers: specifier: ^8.0.0 version: 8.0.0 fflate: - specifier: ^0.8.3 + specifier: 'catalog:' version: 0.8.3 firecrawl: specifier: ^4.42.0 @@ -399,13 +480,13 @@ importers: specifier: ^4.13.11 version: 4.13.11 ioredis: - specifier: ^6.0.0 + specifier: 'catalog:' version: 6.0.0(supports-color@7.2.0) jose: specifier: ^6.2.12 version: 6.2.12 jsonrepair: - specifier: ^3.15.0 + specifier: 'catalog:' version: 3.15.0 node-html-parser: specifier: ^9.0.4 @@ -417,10 +498,10 @@ importers: specifier: ^4.0.1 version: 4.0.1(ai@7.0.122(zod@4.6.5))(zod@4.6.5) pg: - specifier: ^8.23.0 + specifier: 'catalog:' version: 8.23.0 react: - specifier: ^19.3.0 + specifier: 'catalog:' version: 19.3.0 react-email: specifier: ^6.11.0 @@ -441,7 +522,7 @@ importers: specifier: ^2.1.0 version: 2.1.0 ts-pattern: - specifier: ^5.9.0 + specifier: 'catalog:' version: 5.9.0 unique-names-generator: specifier: ^4.7.1 @@ -450,23 +531,23 @@ importers: specifier: ^14.0.2 version: 14.0.2 zod: - specifier: ^4.6.5 + specifier: 'catalog:' version: 4.6.5 devDependencies: '@reactive-resume/config': specifier: workspace:* version: link:../../packages/config '@types/node': - specifier: ^26.6.3 + specifier: 'catalog:' version: 26.6.3 '@types/pg': - specifier: ^8.23.1 + specifier: 'catalog:' version: 8.23.1 '@types/react': - specifier: ^19.3.0 + specifier: 'catalog:' version: 19.3.0 '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 tsdown: specifier: ^0.23.0 @@ -475,10 +556,10 @@ importers: specifier: ^4.23.15 version: 4.23.15 typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 vitest: - specifier: ^5.0.2 + specifier: 'catalog:' version: 5.0.2(@types/node@26.6.3)(@vitest/coverage-v8@5.0.2)(happy-dom@20.14.5)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) apps/web: @@ -490,16 +571,16 @@ importers: specifier: ^1.8.0 version: 1.8.0(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@better-auth/api-key': - specifier: ^1.7.6 + specifier: 'catalog:' version: 1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(better-auth@1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2))(better-call@1.4.0(zod@4.6.5)) '@better-auth/infra': - specifier: ^0.4.13 + specifier: 'catalog:' version: 0.4.13(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(better-auth@1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2))(zod@4.6.5) '@better-auth/oauth-provider': - specifier: ^1.7.6 + specifier: 'catalog:' version: 1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-auth@1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2))(better-call@1.4.0(zod@4.6.5)) '@better-auth/passkey': - specifier: ^1.7.6 + specifier: 'catalog:' version: 1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-auth@1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2))(better-call@1.4.0(zod@4.6.5))(nanostores@1.5.4) '@codemirror/autocomplete': specifier: ^6.20.3 @@ -541,10 +622,10 @@ importers: specifier: ^6.8.0 version: 6.8.0(@babel/core@7.29.7(supports-color@7.2.0))(babel-plugin-macros@3.1.0)(react@19.3.0) '@orpc/client': - specifier: ^1.15.4 + specifier: 'catalog:' version: 1.15.4 '@orpc/server': - specifier: ^1.15.4 + specifier: 'catalog:' version: 1.15.4(ws@8.21.3) '@orpc/tanstack-query': specifier: ^1.15.4 @@ -634,25 +715,25 @@ importers: specifier: ^3.0.6 version: 3.0.6 '@uiw/color-convert': - specifier: ^2.10.3 + specifier: 'catalog:' version: 2.10.3(@babel/runtime@7.29.7) '@uiw/react-color-colorful': specifier: ^2.10.3 version: 2.10.3(@babel/runtime@7.29.7)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) ai: - specifier: ^7.0.122 + specifier: 'catalog:' version: 7.0.122(zod@4.6.5) better-auth: - specifier: 1.7.6 + specifier: 'catalog:' version: 1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2) cmdk: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) es-toolkit: - specifier: ^1.52.0 + specifier: 'catalog:' version: 1.52.0 fflate: - specifier: ^0.8.3 + specifier: 'catalog:' version: 0.8.3 fuse.js: specifier: ^7.5.0 @@ -676,7 +757,7 @@ importers: specifier: ^4.2.0 version: 4.2.0(react@19.3.0) react: - specifier: ^19.3.0 + specifier: 'catalog:' version: 19.3.0 react-dom: specifier: ^19.3.0 @@ -694,13 +775,13 @@ importers: specifier: ^4.0.1 version: 4.0.1(supports-color@7.2.0) ts-pattern: - specifier: ^5.9.0 + specifier: 'catalog:' version: 5.9.0 usehooks-ts: specifier: ^3.1.1 version: 3.1.1(react@19.3.0) zod: - specifier: ^4.6.5 + specifier: 'catalog:' version: 4.6.5 zustand: specifier: ^5.0.15 @@ -746,13 +827,13 @@ importers: specifier: ^7.20.5 version: 7.20.5 '@types/react': - specifier: ^19.3.0 + specifier: 'catalog:' version: 19.3.0 '@types/react-dom': - specifier: ^19.3.0 + specifier: 'catalog:' version: 19.3.0(@types/react@19.3.0) '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 '@vitejs/plugin-react': specifier: ^6.1.1 @@ -764,7 +845,7 @@ importers: specifier: ^1.0.0 version: 1.0.0 typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 vite: specifier: ^8.3.1 @@ -782,23 +863,23 @@ importers: specifier: workspace:* version: link:../utils jsonrepair: - specifier: ^3.15.0 + specifier: 'catalog:' version: 3.15.0 zod: - specifier: ^4.6.5 + specifier: 'catalog:' version: 4.6.5 devDependencies: '@reactive-resume/config': specifier: workspace:* version: link:../config '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 ai: - specifier: ^7.0.122 + specifier: 'catalog:' version: 7.0.122(zod@4.6.5) typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 packages/api: @@ -846,13 +927,13 @@ importers: specifier: ^3.1143.0 version: 3.1143.0 '@orpc/client': - specifier: ^1.15.4 + specifier: 'catalog:' version: 1.15.4 '@orpc/experimental-ratelimit': specifier: ^1.15.4 version: 1.15.4(ws@8.21.3) '@orpc/server': - specifier: ^1.15.4 + specifier: 'catalog:' version: 1.15.4(ws@8.21.3) '@reactive-resume/ai': specifier: workspace:* @@ -882,31 +963,31 @@ importers: specifier: ^2.8.0 version: 2.8.0 ai: - specifier: ^7.0.122 + specifier: 'catalog:' version: 7.0.122(zod@4.6.5) bcryptjs: - specifier: ^3.0.3 + specifier: 'catalog:' version: 3.0.3 better-auth: - specifier: 1.7.6 + specifier: 'catalog:' version: 1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2) drizzle-orm: - specifier: 1.0.0-rc.4 + specifier: 'catalog:' version: 1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5) drizzle-zod: specifier: 1.0.0-beta.14-a36c63d version: 1.0.0-beta.14-a36c63d(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(zod@4.6.5) fflate: - specifier: ^0.8.3 + specifier: 'catalog:' version: 0.8.3 firecrawl: specifier: ^4.42.0 version: 4.42.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) ioredis: - specifier: ^6.0.0 + specifier: 'catalog:' version: 6.0.0(supports-color@7.2.0) jsonrepair: - specifier: ^3.15.0 + specifier: 'catalog:' version: 3.15.0 ollama-ai-provider-v2: specifier: ^4.0.1 @@ -924,50 +1005,50 @@ importers: specifier: ^2.1.0 version: 2.1.0 ts-pattern: - specifier: ^5.9.0 + specifier: 'catalog:' version: 5.9.0 zod: - specifier: ^4.6.5 + specifier: 'catalog:' version: 4.6.5 devDependencies: '@reactive-resume/config': specifier: workspace:* version: link:../config '@types/pg': - specifier: ^8.23.1 + specifier: 'catalog:' version: 8.23.1 '@types/sanitize-html': specifier: ^2.16.2 version: 2.16.2 '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 es-toolkit: - specifier: ^1.52.0 + specifier: 'catalog:' version: 1.52.0 pg: - specifier: ^8.23.0 + specifier: 'catalog:' version: 8.23.0 typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 packages/auth: dependencies: '@better-auth/api-key': - specifier: ^1.7.6 + specifier: 'catalog:' version: 1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(better-auth@1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2))(better-call@1.4.0(zod@4.6.5)) '@better-auth/drizzle-adapter': specifier: ^1.7.6 version: 1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5)) '@better-auth/infra': - specifier: ^0.4.13 + specifier: 'catalog:' version: 0.4.13(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(better-auth@1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2))(zod@4.6.5) '@better-auth/oauth-provider': - specifier: ^1.7.6 + specifier: 'catalog:' version: 1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-auth@1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2))(better-call@1.4.0(zod@4.6.5)) '@better-auth/passkey': - specifier: ^1.7.6 + specifier: 'catalog:' version: 1.7.6(@better-auth/core@1.7.6(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-call@1.4.0(zod@4.6.5))(jose@6.2.12)(kysely@0.29.6)(nanostores@1.5.4))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(better-auth@1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2))(better-call@1.4.0(zod@4.6.5))(nanostores@1.5.4) '@reactive-resume/db': specifier: workspace:* @@ -982,32 +1063,32 @@ importers: specifier: workspace:* version: link:../utils bcryptjs: - specifier: ^3.0.3 + specifier: 'catalog:' version: 3.0.3 better-auth: - specifier: 1.7.6 + specifier: 'catalog:' version: 1.7.6(drizzle-kit@1.0.0-rc.4)(drizzle-orm@1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5))(next@16.3.3(@babel/core@7.29.7)(@playwright/test@1.63.0)(@types/node@26.6.3)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(pg@8.23.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(solid-js@1.9.15)(vitest@5.0.2) drizzle-orm: - specifier: 1.0.0-rc.4 + specifier: 'catalog:' version: 1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5) jose: specifier: ^6.2.12 version: 6.2.12 react: - specifier: ^19.3.0 + specifier: 'catalog:' version: 19.3.0 devDependencies: '@reactive-resume/config': specifier: workspace:* version: link:../config '@types/react': - specifier: ^19.3.0 + specifier: 'catalog:' version: 19.3.0 '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 packages/config: {} @@ -1024,29 +1105,29 @@ importers: specifier: workspace:* version: link:../utils drizzle-orm: - specifier: 1.0.0-rc.4 + specifier: 'catalog:' version: 1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5) ioredis: - specifier: ^6.0.0 + specifier: 'catalog:' version: 6.0.0(supports-color@7.2.0) pg: - specifier: ^8.23.0 + specifier: 'catalog:' version: 8.23.0 devDependencies: '@reactive-resume/config': specifier: workspace:* version: link:../config '@types/pg': - specifier: ^8.23.1 + specifier: 'catalog:' version: 8.23.1 '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 drizzle-kit: specifier: 1.0.0-rc.4 version: 1.0.0-rc.4 typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 packages/docx: @@ -1065,10 +1146,10 @@ importers: specifier: workspace:* version: link:../config '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 packages/dsh-plugin: @@ -1092,16 +1173,16 @@ importers: specifier: workspace:* version: link:../mcp '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 tsdown: specifier: ^0.23.0 version: 0.23.0(@typescript/native-preview@7.0.0-dev.20260707.2)(oxc-resolver@11.24.2)(tsx@4.23.15)(typescript@7.0.2) typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 vitest: - specifier: ^5.0.2 + specifier: 'catalog:' version: 5.0.2(@types/node@26.6.3)(@vitest/coverage-v8@5.0.2)(happy-dom@20.14.5)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) packages/email: @@ -1113,7 +1194,7 @@ importers: specifier: ^10.0.12 version: 10.0.12 react: - specifier: ^19.3.0 + specifier: 'catalog:' version: 19.3.0 react-email: specifier: ^6.11.0 @@ -1129,13 +1210,13 @@ importers: specifier: ^8.0.2 version: 8.0.2 '@types/react': - specifier: ^19.3.0 + specifier: 'catalog:' version: 19.3.0 '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 packages/env: @@ -1150,20 +1231,20 @@ importers: specifier: ^0.13.11 version: 0.13.11(typescript@7.0.2)(zod@4.6.5) zod: - specifier: ^4.6.5 + specifier: 'catalog:' version: 4.6.5 devDependencies: '@reactive-resume/config': specifier: workspace:* version: link:../config '@types/node': - specifier: ^26.6.3 + specifier: 'catalog:' version: 26.6.3 '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 packages/fonts: @@ -1176,10 +1257,10 @@ importers: specifier: workspace:* version: link:../config '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 packages/import: @@ -1194,20 +1275,20 @@ importers: specifier: workspace:* version: link:../utils fflate: - specifier: ^0.8.3 + specifier: 'catalog:' version: 0.8.3 zod: - specifier: ^4.6.5 + specifier: 'catalog:' version: 4.6.5 devDependencies: '@reactive-resume/config': specifier: workspace:* version: link:../config '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 packages/mcp: @@ -1216,7 +1297,7 @@ importers: specifier: ^1.31.0 version: 1.31.0(supports-color@7.2.0)(zod@4.6.5) '@orpc/server': - specifier: ^1.15.4 + specifier: 'catalog:' version: 1.15.4(ws@8.21.3) '@reactive-resume/ai': specifier: workspace:* @@ -1231,20 +1312,20 @@ importers: specifier: workspace:* version: link:../schema zod: - specifier: ^4.6.5 + specifier: 'catalog:' version: 4.6.5 devDependencies: '@reactive-resume/config': specifier: workspace:* version: link:../config '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 vitest: - specifier: ^5.0.2 + specifier: 'catalog:' version: 5.0.2(@types/node@26.6.3)(@vitest/coverage-v8@5.0.2)(happy-dom@20.14.5)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) packages/pdf: @@ -1274,13 +1355,13 @@ importers: specifier: ^8.0.0 version: 8.0.0 fflate: - specifier: ^0.8.3 + specifier: 'catalog:' version: 0.8.3 node-html-parser: specifier: ^9.0.4 version: 9.0.4 react: - specifier: ^19.3.0 + specifier: 'catalog:' version: 19.3.0 react-reconciler: specifier: 0.34.0 @@ -1293,19 +1374,19 @@ importers: specifier: workspace:* version: link:../config '@types/react': - specifier: ^19.3.0 + specifier: 'catalog:' version: 19.3.0 '@types/react-reconciler': specifier: ^0.33.1 version: 0.33.1(@types/react@19.3.0) '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 pdfjs-dist: specifier: 6.3.289 version: 6.3.289 typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 packages/resume: @@ -1329,36 +1410,36 @@ importers: specifier: 2.0.1 version: 2.0.1 zod: - specifier: ^4.6.5 + specifier: 'catalog:' version: 4.6.5 devDependencies: '@reactive-resume/config': specifier: workspace:* version: link:../config '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 fast-check: specifier: ^4.10.2 version: 4.10.2 typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 packages/schema: dependencies: zod: - specifier: ^4.6.5 + specifier: 'catalog:' version: 4.6.5 devDependencies: '@reactive-resume/config': specifier: workspace:* version: link:../config '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 packages/ui: @@ -1385,7 +1466,7 @@ importers: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) react: - specifier: ^19.3.0 + specifier: 'catalog:' version: 19.3.0 react-dom: specifier: ^19.3.0 @@ -1407,13 +1488,13 @@ importers: specifier: ^0.5.20 version: 0.5.20(tailwindcss@4.3.3) '@types/react': - specifier: ^19.3.0 + specifier: 'catalog:' version: 19.3.0 '@types/react-dom': - specifier: ^19.3.0 + specifier: 'catalog:' version: 19.3.0(@types/react@19.3.0) '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 postcss: specifier: ^8.5.28 @@ -1422,7 +1503,7 @@ importers: specifier: ^4.3.3 version: 4.3.3 typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 packages/utils: @@ -1431,7 +1512,7 @@ importers: specifier: ^3.0.1 version: 3.0.1 '@uiw/color-convert': - specifier: ^2.10.3 + specifier: 'catalog:' version: 2.10.3(@babel/runtime@7.29.7) cn: specifier: ^0.4.0 @@ -1443,20 +1524,20 @@ importers: specifier: ^14.0.2 version: 14.0.2 zod: - specifier: ^4.6.5 + specifier: 'catalog:' version: 4.6.5 devDependencies: '@reactive-resume/config': specifier: workspace:* version: link:../config '@types/node': - specifier: ^26.6.3 + specifier: 'catalog:' version: 26.6.3 '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 typescript: - specifier: ^7.0.2 + specifier: 'catalog:' version: 7.0.2 tooling: @@ -1481,22 +1562,22 @@ importers: specifier: workspace:* version: link:../packages/resume '@types/pg': - specifier: ^8.23.1 + specifier: 'catalog:' version: 8.23.1 '@types/react': - specifier: ^19.3.0 + specifier: 'catalog:' version: 19.3.0 '@types/react-dom': - specifier: ^19.3.0 + specifier: 'catalog:' version: 19.3.0(@types/react@19.3.0) '@typescript/native-preview': - specifier: 7.0.0-dev.20260707.2 + specifier: 'catalog:' version: 7.0.0-dev.20260707.2 drizzle-orm: - specifier: 1.0.0-rc.4 + specifier: 'catalog:' version: 1.0.0-rc.4(@types/pg@8.23.1)(pg@8.23.0)(zod@4.6.5) pg: - specifier: ^8.23.0 + specifier: 'catalog:' version: 8.23.0 react: specifier: 19.3.0 @@ -1508,7 +1589,7 @@ importers: specifier: ^4.23.15 version: 4.23.15 vitest: - specifier: ^5.0.2 + specifier: 'catalog:' version: 5.0.2(@types/node@26.6.3)(@vitest/coverage-v8@5.0.2)(happy-dom@20.14.5)(vite@8.3.1(@types/node@26.6.3)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) packages: @@ -3001,9 +3082,6 @@ packages: '@lezer/css@1.3.8': resolution: {integrity: sha512-EJn1zcL9qoDptief6ipWKZKLiOpXkxSe0+t8CH9oiMVcZlq7NBWrjCqnc/41EIjeo/ITj1gFFiATdTkaJDL+Og==} - '@lezer/highlight@1.2.4': - resolution: {integrity: sha512-4UeRVFO0lsBaM7GA32zOqbmqNPme9jedo8Ey+5c5656MU3MLitMLIA8aCZTYjUMEpEsuSeXEm78Uea6Iq3bvSg==} - '@lezer/highlight@1.2.5': resolution: {integrity: sha512-O1GMVKgtf5YspFaRzpmqkVgMtIk2HG9uMAbH6wdAIdNh3TVzCsch/+cRi+6l2//UGDKe6NB9BW3cvuR5ezMQ0w==} @@ -9600,7 +9678,7 @@ snapshots: '@codemirror/state': 6.7.6 '@codemirror/view': 6.43.13 '@lezer/common': 1.5.3 - '@lezer/highlight': 1.2.4 + '@lezer/highlight': 1.2.5 '@lezer/lr': 1.4.10 style-mod: 4.1.4 @@ -10379,13 +10457,9 @@ snapshots: '@lezer/css@1.3.8': dependencies: '@lezer/common': 1.5.3 - '@lezer/highlight': 1.2.4 + '@lezer/highlight': 1.2.5 '@lezer/lr': 1.4.10 - '@lezer/highlight@1.2.4': - dependencies: - '@lezer/common': 1.5.3 - '@lezer/highlight@1.2.5': dependencies: '@lezer/common': 1.5.3 @@ -10536,7 +10610,7 @@ snapshots: '@modelcontextprotocol/sdk@1.31.0(supports-color@7.2.0)(zod@4.6.5)': dependencies: - '@hono/node-server': 2.1.1(hono@4.13.9) + '@hono/node-server': 2.1.3(hono@4.13.11) ajv: 8.20.0 ajv-formats: 3.0.1 content-type: 1.0.5 @@ -10546,7 +10620,7 @@ snapshots: eventsource-parser: 3.1.1 express: 5.2.1(supports-color@7.2.0) express-rate-limit: 8.7.0(express@5.2.1(supports-color@7.2.0))(supports-color@7.2.0) - hono: 4.13.9 + hono: 4.13.11 jose: 6.2.12 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 559ce6c52..e554192ce 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,34 @@ packages: - apps/* - packages/* - tooling +catalogMode: prefer +catalog: + "@better-auth/api-key": "^1.7.6" + "@better-auth/infra": "^0.4.13" + "@better-auth/oauth-provider": "^1.7.6" + "@better-auth/passkey": "^1.7.6" + "@orpc/client": "^1.15.4" + "@orpc/server": "^1.15.4" + "@types/node": "^26.6.3" + "@types/pg": "^8.23.1" + "@types/react": "^19.3.0" + "@types/react-dom": "^19.3.0" + "@typescript/native-preview": "7.0.0-dev.20260707.2" + "@uiw/color-convert": "^2.10.3" + "ai": "^7.0.122" + "bcryptjs": "^3.0.3" + "better-auth": "1.7.6" + "drizzle-orm": "1.0.0-rc.4" + "es-toolkit": "^1.52.0" + "fflate": "^0.8.3" + "ioredis": "^6.0.0" + "jsonrepair": "^3.15.0" + "pg": "^8.23.0" + "react": "^19.3.0" + "ts-pattern": "^5.9.0" + "typescript": "^7.0.2" + "vitest": "^5.0.2" + "zod": "^4.6.5" allowBuilds: esbuild: true lefthook: true diff --git a/tooling/package.json b/tooling/package.json index 42bbeb4b1..e8e662db4 100644 --- a/tooling/package.json +++ b/tooling/package.json @@ -22,15 +22,15 @@ "@reactive-resume/config": "workspace:*", "@reactive-resume/env": "workspace:*", "@reactive-resume/resume": "workspace:*", - "@types/pg": "^8.23.1", - "@types/react": "^19.3.0", - "@types/react-dom": "^19.3.0", - "@typescript/native-preview": "7.0.0-dev.20260707.2", - "drizzle-orm": "1.0.0-rc.4", - "pg": "^8.23.0", + "@types/pg": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@typescript/native-preview": "catalog:", + "drizzle-orm": "catalog:", + "pg": "catalog:", "react": "19.3.0", "react-dom": "19.3.0", "tsx": "^4.23.15", - "vitest": "^5.0.2" + "vitest": "catalog:" } } diff --git a/tooling/turbo/source-cache.test.ts b/tooling/turbo/source-cache.test.ts index df67292b4..cb549fd90 100644 --- a/tooling/turbo/source-cache.test.ts +++ b/tooling/turbo/source-cache.test.ts @@ -12,12 +12,12 @@ type DryTask = { taskId: string; task: string; hash: string }; let directory: string; let baseline: Map; -function dryRun() { +function dryRun(environment: NodeJS.ProcessEnv = {}) { const output = execFileSync(process.execPath, [turbo, "run", ...tasks, "--dry=json"], { cwd: directory, encoding: "utf8", maxBuffer: 8 * 1024 * 1024, - env: { ...process.env, TURBO_TELEMETRY_DISABLED: "1" }, + env: { ...process.env, ...environment, TURBO_TELEMETRY_DISABLED: "1" }, }); const result = JSON.parse(output) as { tasks: DryTask[] }; return new Map(result.tasks.map((task) => [task.taskId, task])); @@ -31,8 +31,11 @@ beforeAll(async () => { JSON.stringify({ private: true, packageManager: packageJson.packageManager }), ); await writeFile(join(directory, "pnpm-workspace.yaml"), 'packages: ["packages/*"]\n'); - await writeFile(join(directory, ".gitignore"), "node_modules/\n.turbo/\n"); + await writeFile(join(directory, ".gitignore"), "node_modules/\n.turbo/\ncoverage/\nreports/\n"); await writeFile(join(directory, "turbo.json"), await readFile(join(root, "turbo.json"))); + for (const file of ["vitest.shared.mts", "vitest.setup.ts"]) { + await writeFile(join(directory, file), "export {};\n"); + } // app -> api -> pdf mirrors a source-consumed dependency with no build script. const dependencies: Record = { app: ["api"], api: ["pdf"], pdf: [] }; @@ -48,7 +51,18 @@ beforeAll(async () => { version: "0.0.0", private: true, dependencies: Object.fromEntries(deps.map((dependency) => [dependency, "workspace:*"])), - scripts: name === "pdf" ? {} : Object.fromEntries(tasks.map((task) => [task, "node -e ''"])), + scripts: + name === "pdf" + ? {} + : Object.fromEntries( + tasks.map((task) => { + const output = task === "test:coverage" ? "coverage" : "reports"; + return [ + task, + `node -e 'const fs = require("node:fs"); fs.mkdirSync("${output}", { recursive: true }); fs.writeFileSync("${output}/${task.replace(":", "-")}.txt", "complete");'`, + ]; + }), + ), }), ); lock += ` packages/${name}:`; @@ -89,3 +103,43 @@ it("invalidates every cached consumer task after a transitive source-only packag it("keeps cache keys stable when sources are unchanged", () => { expect(dryRun()).toEqual(baseline); }); + +it.each(["vitest.shared.mts", "vitest.setup.ts"])("invalidates consumer tests when %s changes", async (file) => { + const path = join(directory, file); + const original = await readFile(path); + try { + await writeFile(path, "export const changed = true;\n"); + const result = dryRun(); + for (const task of tasks.filter((task) => task.startsWith("test"))) { + const id = `app#${task}`; + expect(result.get(id)?.hash, id).not.toBe(baseline.get(id)?.hash); + } + } finally { + await writeFile(path, original); + } +}); + +it("hashes runtime secrets for tests without invalidating builds or typechecks", () => { + const before = dryRun({ AUTH_SECRET: "cache-audit-before" }); + const after = dryRun({ AUTH_SECRET: "cache-audit-after" }); + for (const task of tasks) { + const id = `app#${task}`; + if (task.startsWith("test")) expect(after.get(id)?.hash, id).not.toBe(before.get(id)?.hash); + else expect(after.get(id)?.hash, id).toBe(before.get(id)?.hash); + } +}); + +it.each(["test:coverage", "test:ci", "test:agent"])("restores %s reports from cache", async (task) => { + const output = task === "test:coverage" ? "coverage" : "reports"; + const path = join(directory, "packages", "app", output, `${task.replace(":", "-")}.txt`); + const run = () => + execFileSync(process.execPath, [turbo, "run", task, "--filter=app", "--cache=local:rw"], { + cwd: directory, + encoding: "utf8", + env: { ...process.env, TURBO_TELEMETRY_DISABLED: "1" }, + }); + run(); + await rm(path); + expect(run()).toContain("cache hit"); + expect(await readFile(path, "utf8")).toBe("complete"); +}); diff --git a/turbo.json b/turbo.json index c276714e1..629ff8e3b 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,5 @@ { - "$schema": "https://v2-10-7.turborepo.dev/schema.json", + "$schema": "https://v2-11-5.turborepo.dev/schema.json", "ui": "stream", "boundaries": { "dependencies": { @@ -44,7 +44,198 @@ } } }, - "globalEnv": [ + "tasks": { + "transit": { + "dependsOn": ["^transit"] + }, + "build": { + "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/.env*"], + "outputs": ["dist/**", ".vercel/**"], + "dependsOn": ["transit"], + "env": ["NODE_ENV"] + }, + "typecheck": { + "dependsOn": ["transit"] + }, + "test": { + "dependsOn": ["transit"], + "env": [ + "PORT", + "SERVER_PORT", + "APP_URL", + "ROOT_RESUME_ID", + "DATABASE_*", + "AUTH_SECRET", + "BETTER_AUTH_*", + "GOOGLE_CLIENT_*", + "GITHUB_CLIENT_*", + "LINKEDIN_CLIENT_*", + "OAUTH_*", + "SMTP_*", + "LOCAL_STORAGE_PATH", + "S3_*", + "REDIS_URL", + "ENCRYPTION_SECRET", + "FIRECRAWL_*", + "AI_*", + "FLAG_*", + "STRICT_SCHEMA_CHECK", + "VERCEL", + "VERCEL_ENV", + "VERCEL_URL", + "VERCEL_PROJECT_PRODUCTION_URL", + "VERCEL_BRANCH_URL", + "POSTGRES_*", + "KV_URL", + "STORAGE_BACKEND", + "BLOB_*", + "DEPLOYMENT_NAMESPACE", + "ALLOW_PREVIEW_MIGRATIONS", + "NODE_ENV" + ], + "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/.env*"] + }, + "test:coverage": { + "dependsOn": ["transit"], + "env": [ + "PORT", + "SERVER_PORT", + "APP_URL", + "ROOT_RESUME_ID", + "DATABASE_*", + "AUTH_SECRET", + "BETTER_AUTH_*", + "GOOGLE_CLIENT_*", + "GITHUB_CLIENT_*", + "LINKEDIN_CLIENT_*", + "OAUTH_*", + "SMTP_*", + "LOCAL_STORAGE_PATH", + "S3_*", + "REDIS_URL", + "ENCRYPTION_SECRET", + "FIRECRAWL_*", + "AI_*", + "FLAG_*", + "STRICT_SCHEMA_CHECK", + "VERCEL", + "VERCEL_ENV", + "VERCEL_URL", + "VERCEL_PROJECT_PRODUCTION_URL", + "VERCEL_BRANCH_URL", + "POSTGRES_*", + "KV_URL", + "STORAGE_BACKEND", + "BLOB_*", + "DEPLOYMENT_NAMESPACE", + "ALLOW_PREVIEW_MIGRATIONS", + "NODE_ENV" + ], + "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/.env*"], + "outputs": ["coverage/**"] + }, + "test:ci": { + "dependsOn": ["transit"], + "env": [ + "PORT", + "SERVER_PORT", + "APP_URL", + "ROOT_RESUME_ID", + "DATABASE_*", + "AUTH_SECRET", + "BETTER_AUTH_*", + "GOOGLE_CLIENT_*", + "GITHUB_CLIENT_*", + "LINKEDIN_CLIENT_*", + "OAUTH_*", + "SMTP_*", + "LOCAL_STORAGE_PATH", + "S3_*", + "REDIS_URL", + "ENCRYPTION_SECRET", + "FIRECRAWL_*", + "AI_*", + "FLAG_*", + "STRICT_SCHEMA_CHECK", + "VERCEL", + "VERCEL_ENV", + "VERCEL_URL", + "VERCEL_PROJECT_PRODUCTION_URL", + "VERCEL_BRANCH_URL", + "POSTGRES_*", + "KV_URL", + "STORAGE_BACKEND", + "BLOB_*", + "DEPLOYMENT_NAMESPACE", + "ALLOW_PREVIEW_MIGRATIONS", + "NODE_ENV" + ], + "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/.env*"], + "outputs": ["reports/**"] + }, + "test:agent": { + "dependsOn": ["transit"], + "env": [ + "PORT", + "SERVER_PORT", + "APP_URL", + "ROOT_RESUME_ID", + "DATABASE_*", + "AUTH_SECRET", + "BETTER_AUTH_*", + "GOOGLE_CLIENT_*", + "GITHUB_CLIENT_*", + "LINKEDIN_CLIENT_*", + "OAUTH_*", + "SMTP_*", + "LOCAL_STORAGE_PATH", + "S3_*", + "REDIS_URL", + "ENCRYPTION_SECRET", + "FIRECRAWL_*", + "AI_*", + "FLAG_*", + "STRICT_SCHEMA_CHECK", + "VERCEL", + "VERCEL_ENV", + "VERCEL_URL", + "VERCEL_PROJECT_PRODUCTION_URL", + "VERCEL_BRANCH_URL", + "POSTGRES_*", + "KV_URL", + "STORAGE_BACKEND", + "BLOB_*", + "DEPLOYMENT_NAMESPACE", + "ALLOW_PREVIEW_MIGRATIONS", + "NODE_ENV" + ], + "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/.env*"], + "outputs": ["reports/**"] + }, + "dev": { + "cache": false, + "persistent": true + }, + "start": { + "cache": false, + "persistent": true + }, + "db:generate": { + "cache": false + }, + "db:migrate": { + "cache": false + }, + "db:studio": { + "cache": false, + "persistent": true + }, + "lingui:extract": { + "cache": false + } + }, + "globalDependencies": ["vitest.shared.mts", "vitest.setup.ts"], + "globalPassThroughEnv": [ "PORT", "SERVER_PORT", "APP_URL", @@ -112,52 +303,7 @@ "BLOB_READ_WRITE_TOKEN", "BLOB_STORE_ID", "DEPLOYMENT_NAMESPACE", - "ALLOW_PREVIEW_MIGRATIONS" - ], - "tasks": { - "transit": { - "dependsOn": ["^transit"] - }, - "build": { - "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/.env*"], - "outputs": ["dist/**", ".vercel/**"], - "dependsOn": ["transit"] - }, - "typecheck": { - "dependsOn": ["transit"] - }, - "test": { - "dependsOn": ["transit"] - }, - "test:coverage": { - "dependsOn": ["transit"] - }, - "test:ci": { - "dependsOn": ["transit"] - }, - "test:agent": { - "dependsOn": ["transit"] - }, - "dev": { - "cache": false, - "persistent": true - }, - "start": { - "cache": false, - "persistent": true - }, - "db:generate": { - "cache": false - }, - "db:migrate": { - "cache": false - }, - "db:studio": { - "cache": false, - "persistent": true - }, - "lingui:extract": { - "cache": false - } - } + "ALLOW_PREVIEW_MIGRATIONS", + "NODE_ENV" + ] }