diff --git a/apps/admin/app/routes/_app+/$project._index/route.tsx b/apps/admin/app/routes/_app+/$project._index/route.tsx index 43c33ce..938f98e 100644 --- a/apps/admin/app/routes/_app+/$project._index/route.tsx +++ b/apps/admin/app/routes/_app+/$project._index/route.tsx @@ -1,5 +1,7 @@ +import { useJob, useRuns } from '@coji/durably-react/client' import { zx } from '@coji/zodix/v4' -import { ArrowLeftIcon, LoaderCircleIcon } from 'lucide-react' +import { ArrowLeftIcon, HistoryIcon, LoaderCircleIcon } from 'lucide-react' +import { useState } from 'react' import { Form, href, Link, useNavigate, useNavigation } from 'react-router' import { z } from 'zod' import { @@ -12,23 +14,43 @@ import { CardTitle, HStack, Label, + Progress, RadioGroup, RadioGroupItem, + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, } from '~/components/ui' import dayjs from '~/libs/dayjs' import type { Route } from './+types/route' -import { - exportFiles, - getProjectDetails, - rescanFiles, - startTranslationJob, -} from './functions.server' +import { exportFiles, getProjectDetails, rescanFiles } from './functions.server' + +// Type definitions for durably job data +type TranslationJobOutput = { + translatedCount: number + errorCount: number + totalCount: number + errors: { path: string; error: string }[] +} + +type JobProgress = { + current: number + total: number + message?: string +} export const meta = ({ loaderData }: Route.MetaArgs) => [ { title: `${loaderData?.project.id}` }, @@ -70,13 +92,6 @@ export const action = async ({ request, params }: Route.ActionArgs) => { } } - if (intent === 'start-translation-job') { - return { - intent: 'start-translation-job', - translation_result: await startTranslationJob(projectId), - } - } - if (intent === 'export-files') { return { intent: 'export-files', @@ -100,13 +115,30 @@ export default function ProjectDetail({ const isRescanInProgress = navigation.state === 'submitting' && navigation.formData?.get('intent') === 'rescan-project' - const isTranslationInProgress = - navigation.state === 'submitting' && - navigation.formData?.get('intent') === 'start-translation-job' const isExportInProgress = navigation.state === 'submitting' && navigation.formData?.get('intent') === 'export-files' + // Job history + const [historyOpen, setHistoryOpen] = useState(false) + const { runs } = useRuns({ + api: '/api/durably', + jobName: 'translate-project', + pageSize: 20, + }) + + // Use durably for translation job (autoResume and followLatest are enabled by default in v0.8.0) + const translationJob = useJob({ + api: '/api/durably', + jobName: 'translate-project', + }) + + const handleStartTranslation = () => { + translationJob.trigger({ projectId: project.id }) + } + + const isTranslationRunning = translationJob.isRunning + return ( @@ -140,11 +172,11 @@ export default function ProjectDetail({ + + + + + + + Translation History + Past translation job runs + +
+ {runs?.map((run) => ( +
+
+
+ + {run.status === 'running' && ( + + )} + {run.status} + + + {run.id.slice(0, 8)} + +
+
+ {dayjs(run.createdAt) + .utc() + .tz() + .format('YYYY-MM-DD HH:mm:ss')} +
+ {/* Progress for running jobs */} + {(run.status === 'running' || + run.status === 'pending') && + run.progress && + (() => { + const progress = run.progress as JobProgress + return ( +
+
+ {progress.current ?? 0}/{progress.total ?? 0} +
+ +
+ ) + })()} + {/* Output for completed jobs */} + {run.output && + (() => { + const output = run.output as TranslationJobOutput + return ( +
+ {output.translatedCount ?? 0} translated,{' '} + {(output.errorCount ?? 0) > 0 ? ( + + + + + {output.errorCount ?? 0} errors + + + +
    + {(output.errors ?? []).map((e, i) => ( +
  • + + {e.path} + + : {e.error} +
  • + ))} +
+
+
+
+ ) : ( + 0 errors + )} +
+ ) + })()} + {/* Error for failed jobs */} + {run.status === 'failed' && run.error && ( + + + +
+ {String(run.error)} +
+
+ + {String(run.error)} + +
+
+ )} +
+
+ ))} + {(!runs || runs.length === 0) && ( +
+ No translation history yet +
+ )} +
+
+
+
@@ -174,6 +343,47 @@ export default function ProjectDetail({ + {/* Translation Progress */} + {translationJob.status && ( +
+
+ + Translation: {translationJob.status} + {translationJob.progress && ( + + ({translationJob.progress.current}/ + {translationJob.progress.total}) + + )} + + {translationJob.progress?.message && ( + + {translationJob.progress.message} + + )} +
+ {translationJob.progress?.total && ( + + )} + {translationJob.output && + (() => { + const output = translationJob.output as TranslationJobOutput + return ( +
+ Completed: {output.translatedCount} translated,{' '} + {output.errorCount} errors +
+ ) + })()} +
+ )} +
{actionData?.intent === 'rescan-project' && actionData.rescan_result && ( @@ -220,14 +430,6 @@ export default function ProjectDetail({
)} - {actionData?.intent === 'start-translation-job' && - actionData.translation_result && ( -
-
Translation job started
-
Job ID: {actionData.translation_result.id}
-
- )} - {actionData?.intent === 'export-files' && actionData.export_result && (
diff --git a/apps/admin/app/routes/api.durably.$.ts b/apps/admin/app/routes/api.durably.$.ts new file mode 100644 index 0000000..e66e587 --- /dev/null +++ b/apps/admin/app/routes/api.durably.$.ts @@ -0,0 +1,10 @@ +import { durablyHandler } from '~/services/durably.server' +import type { Route } from './+types/api.durably.$' + +export async function loader({ request }: Route.LoaderArgs) { + return await durablyHandler.handle(request, '/api/durably') +} + +export async function action({ request }: Route.ActionArgs) { + return await durablyHandler.handle(request, '/api/durably') +} diff --git a/apps/admin/app/services/db.server.ts b/apps/admin/app/services/db.server.ts index befff19..bc59c98 100644 --- a/apps/admin/app/services/db.server.ts +++ b/apps/admin/app/services/db.server.ts @@ -12,7 +12,7 @@ const parseDbPath = (url: string): string => { return url.replace(/^sqlite:\/\//, '').replace(/^file:/, '') } -const dialect = new SqliteDialect({ +export const dialect = new SqliteDialect({ database: new Database( parseDbPath(process.env.DATABASE_URL ?? DEFAULT_DB_PATH), ), diff --git a/apps/admin/app/services/durably.server.ts b/apps/admin/app/services/durably.server.ts new file mode 100644 index 0000000..98057a6 --- /dev/null +++ b/apps/admin/app/services/durably.server.ts @@ -0,0 +1,124 @@ +import { createDurably, createDurablyHandler, defineJob } from '@coji/durably' +import { z } from 'zod' +import { db, dialect, now } from './db.server' +import { translateByGemini } from './translate-gemini' + +// Define translation job +const translationJob = defineJob({ + name: 'translate-project', + input: z.object({ projectId: z.string() }), + output: z.object({ + translatedCount: z.number(), + errorCount: z.number(), + totalCount: z.number(), + errors: z.array(z.object({ path: z.string(), error: z.string() })), + }), + run: async (step, { projectId }) => { + // Fetch files to translate (also validates project exists) + const files = await step.run('fetch-data', async () => { + // Validate project exists + await db + .selectFrom('projects') + .select('id') + .where('id', '=', projectId) + .executeTakeFirstOrThrow() + + return await db + .selectFrom('files') + .selectAll() + .where('project_id', '=', projectId) + .where('is_updated', '=', 1) + .orderBy('created_at', 'asc') + .execute() + }) + + step.log.info(`Starting translation for project: ${projectId}`) + step.log.info(`Files to translate: ${files.length}`) + + const MAX_STORED_ERRORS = 50 + let translatedCount = 0 + let errorCount = 0 + const errors: { path: string; error: string }[] = [] + + // Translate each file + for (let i = 0; i < files.length; i++) { + const file = files[i] + + const result = await step.run(`translate-file-${file.id}`, async () => { + step.log.info(`Translating: ${file.path}`) + + const ret = await translateByGemini({ + source: file.content, + prevTranslatedText: file.output ?? undefined, + }) + + if (ret.type === 'success') { + const timestamp = now() + await db + .updateTable('files') + .set({ + is_updated: 0, + output: ret.translatedText, + translated_at: timestamp, + updated_at: timestamp, + }) + .where('id', '=', file.id) + .execute() + + step.log.info(`Translated: ${file.path}`) + return { success: true as const, path: file.path } + } else { + step.log.error(`Failed: ${file.path} - ${ret.error}`) + return { success: false as const, path: file.path, error: ret.error } + } + }) + + if (result.success) { + translatedCount++ + } else { + errorCount++ + // Only store first MAX_STORED_ERRORS to prevent huge output + if (errors.length < MAX_STORED_ERRORS) { + errors.push({ path: result.path, error: result.error }) + } + } + + // Report progress after each file + step.progress( + i + 1, + files.length, + `Processed ${i + 1}/${files.length} (${translatedCount} translated, ${errorCount} errors)`, + ) + } + + step.log.info( + `Translation complete: ${translatedCount} translated, ${errorCount} errors`, + ) + + return { + translatedCount, + errorCount, + totalCount: files.length, + errors, + } + }, +}) + +// Create durably instance and register jobs +export const durably = createDurably({ + dialect, + pollingInterval: 1000, + heartbeatInterval: 5000, + staleThreshold: 30000, +}).register({ + 'translate-project': translationJob, +}) + +// Initialize durably tables +await durably.init() + +// Export jobs for type inference on client +export const jobs = durably.jobs + +// Create handler for API routes +export const durablyHandler = createDurablyHandler(durably) diff --git a/apps/admin/package.json b/apps/admin/package.json index 92a1c4b..f2ed0be 100644 --- a/apps/admin/package.json +++ b/apps/admin/package.json @@ -18,6 +18,8 @@ }, "dependencies": { "@ai-sdk/google": "catalog:", + "@coji/durably": "0.8.0", + "@coji/durably-react": "0.8.0", "@coji/zodix": "catalog:", "@conform-to/react": "catalog:", "@conform-to/zod": "catalog:", @@ -25,7 +27,7 @@ "@react-router/node": "catalog:", "@react-router/serve": "catalog:", "ai": "catalog:", - "better-sqlite3": "12.6.0", + "better-sqlite3": "catalog:", "class-variance-authority": "catalog:", "clsx": "catalog:", "dayjs": "catalog:", @@ -33,7 +35,7 @@ "fast-glob": "catalog:", "front-matter": "catalog:", "isbot": "catalog:", - "kysely": "0.28.9", + "kysely": "catalog:", "lucide-react": "catalog:", "neverthrow": "catalog:", "next-themes": "catalog:", @@ -57,12 +59,12 @@ "@react-router/dev": "catalog:", "@react-router/remix-routes-option-adapter": "catalog:", "@tailwindcss/vite": "catalog:", - "@types/better-sqlite3": "7.6.13", + "@types/better-sqlite3": "catalog:", "@types/debug": "catalog:", "@types/node": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", - "kysely-codegen": "0.19.0", + "kysely-codegen": "catalog:", "npm-run-all": "catalog:", "prettier": "catalog:", "prettier-plugin-organize-imports": "catalog:", @@ -72,6 +74,7 @@ "tailwindcss-animate": "catalog:", "typescript": "catalog:", "vite": "catalog:", + "vite-plugin-devtools-json": "catalog:", "vite-tsconfig-paths": "catalog:", "vitest": "catalog:" }, diff --git a/apps/admin/vite.config.ts b/apps/admin/vite.config.ts index 50ce515..896e62f 100644 --- a/apps/admin/vite.config.ts +++ b/apps/admin/vite.config.ts @@ -1,12 +1,14 @@ import { reactRouter } from '@react-router/dev/vite' import tailwindcss from '@tailwindcss/vite' import { defineConfig } from 'vite' +import devtoolsJson from 'vite-plugin-devtools-json' import tsconfigPaths from 'vite-tsconfig-paths' import { configDefaults } from 'vitest/config' export default defineConfig({ optimizeDeps: { exclude: ['projects'] }, plugins: [ + devtoolsJson(), tailwindcss(), reactRouter(), tsconfigPaths({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d4159b..68367dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,9 @@ catalogs: '@tailwindcss/vite': specifier: 4.1.18 version: 4.1.18 + '@types/better-sqlite3': + specifier: 7.6.13 + version: 7.6.13 '@types/debug': specifier: 4.1.12 version: 4.1.12 @@ -84,6 +87,9 @@ catalogs: ai: specifier: 6.0.27 version: 6.0.27 + better-sqlite3: + specifier: 12.6.0 + version: 12.6.0 cheerio: specifier: 1.1.2 version: 1.1.2 @@ -123,6 +129,12 @@ catalogs: kuromoji: specifier: 0.1.2 version: 0.1.2 + kysely: + specifier: 0.28.9 + version: 0.28.9 + kysely-codegen: + specifier: 0.19.0 + version: 0.19.0 lucide-react: specifier: 0.562.0 version: 0.562.0 @@ -246,6 +258,9 @@ catalogs: vite: specifier: 7.3.1 version: 7.3.1 + vite-plugin-devtools-json: + specifier: 1.0.0 + version: 1.0.0 vite-tsconfig-paths: specifier: 6.0.4 version: 6.0.4 @@ -286,6 +301,12 @@ importers: '@ai-sdk/google': specifier: 'catalog:' version: 3.0.6(zod@4.3.5) + '@coji/durably': + specifier: 0.8.0 + version: 0.8.0(kysely@0.28.9)(zod@4.3.5) + '@coji/durably-react': + specifier: 0.8.0 + version: 0.8.0(@coji/durably@0.8.0(kysely@0.28.9)(zod@4.3.5))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@coji/zodix': specifier: 'catalog:' version: 0.7.0(zod@4.3.5) @@ -308,7 +329,7 @@ importers: specifier: 'catalog:' version: 6.0.27(zod@4.3.5) better-sqlite3: - specifier: 12.6.0 + specifier: 'catalog:' version: 12.6.0 class-variance-authority: specifier: 'catalog:' @@ -332,7 +353,7 @@ importers: specifier: 'catalog:' version: 5.1.32 kysely: - specifier: 0.28.9 + specifier: 'catalog:' version: 0.28.9 lucide-react: specifier: 'catalog:' @@ -399,7 +420,7 @@ importers: specifier: 'catalog:' version: 4.1.18(vite@7.3.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) '@types/better-sqlite3': - specifier: 7.6.13 + specifier: 'catalog:' version: 7.6.13 '@types/debug': specifier: 'catalog:' @@ -414,7 +435,7 @@ importers: specifier: 'catalog:' version: 19.2.3(@types/react@19.2.8) kysely-codegen: - specifier: 0.19.0 + specifier: 'catalog:' version: 0.19.0(better-sqlite3@12.6.0)(kysely@0.28.9)(mysql2@3.15.3)(typescript@5.9.3) npm-run-all: specifier: 'catalog:' @@ -443,6 +464,9 @@ importers: vite: specifier: 'catalog:' version: 7.3.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + vite-plugin-devtools-json: + specifier: 'catalog:' + version: 1.0.0(vite@7.3.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) vite-tsconfig-paths: specifier: 'catalog:' version: 6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)) @@ -1065,6 +1089,22 @@ packages: cpu: [x64] os: [win32] + '@coji/durably-react@0.8.0': + resolution: {integrity: sha512-Yb698miCurCjxXS/Qj+xtD01JMQbRAIYp01DOtyfpoDXyb9Y2xUJl2S0IglGF8uBzm2FLwaZ97mz3xa715YLbw==} + peerDependencies: + '@coji/durably': '*' + react: '>=19.0.0' + react-dom: '>=19.0.0' + peerDependenciesMeta: + '@coji/durably': + optional: true + + '@coji/durably@0.8.0': + resolution: {integrity: sha512-/4kUWNWFrZe4wxMZp7oMDJBmAzVZoC0UxzhSCNaBzSo9A/TIbU0WrY1O6WKbiUEGVeQ0PLLKy5Byj41BSgsLZA==} + peerDependencies: + kysely: ^0.27.0 + zod: ^4.0.0 + '@coji/zodix@0.7.0': resolution: {integrity: sha512-ROIGrROdJZJnRMuVXioXAmDqj2h+upPhmztimZeSZbClluyz5zFxQoKBeALXG4/9vrn07kqu7SmZlbyz9hiJwA==} engines: {node: '>=22'} @@ -3911,6 +3951,9 @@ packages: resolution: {integrity: sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA==} engines: {node: '>=20.0.0'} + layerr@3.0.0: + resolution: {integrity: sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA==} + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -5187,6 +5230,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + ulidx@2.4.1: + resolution: {integrity: sha512-xY7c8LPyzvhvew0Fn+Ek3wBC9STZAuDI/Y5andCKi9AX6/jvfaX45PhsDX8oxgPL0YFp0Jhr8qWMbS/p9375Xg==} + engines: {node: '>=16'} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -5278,6 +5325,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + valibot@1.2.0: resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} peerDependencies: @@ -5307,6 +5358,11 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vite-plugin-devtools-json@1.0.0: + resolution: {integrity: sha512-MobvwqX76Vqt/O4AbnNMNWoXWGrKUqZbphCUle/J2KXH82yKQiunOeKnz/nqEPosPsoWWPP9FtNuPBSYpiiwkw==} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vite-tsconfig-paths@6.0.4: resolution: {integrity: sha512-iIsEJ+ek5KqRTK17pmxtgIxXtqr3qDdE6OxrP9mVeGhVDNXRJTKN/l9oMbujTQNzMLe6XZ8qmpztfbkPu2TiFQ==} peerDependencies: @@ -5757,6 +5813,19 @@ snapshots: '@cloudflare/workerd-windows-64@1.20260107.1': optional: true + '@coji/durably-react@0.8.0(@coji/durably@0.8.0(kysely@0.28.9)(zod@4.3.5))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + optionalDependencies: + '@coji/durably': 0.8.0(kysely@0.28.9)(zod@4.3.5) + + '@coji/durably@0.8.0(kysely@0.28.9)(zod@4.3.5)': + dependencies: + kysely: 0.28.9 + ulidx: 2.4.1 + zod: 4.3.5 + '@coji/zodix@0.7.0(zod@4.3.5)': optionalDependencies: zod: 4.3.5 @@ -8653,6 +8722,8 @@ snapshots: kysely@0.28.9: {} + layerr@3.0.0: {} + lightningcss-android-arm64@1.30.2: optional: true @@ -10401,6 +10472,10 @@ snapshots: typescript@5.9.3: {} + ulidx@2.4.1: + dependencies: + layerr: 3.0.0 + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -10503,6 +10578,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@11.1.0: {} + valibot@1.2.0(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 @@ -10550,6 +10627,11 @@ snapshots: - tsx - yaml + vite-plugin-devtools-json@1.0.0(vite@7.3.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + uuid: 11.1.0 + vite: 7.3.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) + vite-tsconfig-paths@6.0.4(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.6)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)): dependencies: debug: 4.4.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ca82e70..fd8f681 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,35 +1,38 @@ packages: - ./apps/* - ./packages/* - catalog: - '@ai-sdk/google': 3.0.6 - '@biomejs/biome': 2.3.11 - '@coji/zodix': 0.7.0 - '@conform-to/react': 1.15.1 - '@conform-to/zod': 1.15.1 - '@mdx-js/rollup': 3.1.1 - '@react-router/dev': 7.12.0 - '@react-router/fs-routes': 7.12.0 - '@react-router/node': 7.12.0 - '@react-router/remix-routes-option-adapter': 7.12.0 - '@react-router/serve': 7.12.0 - '@rehype-pretty/transformers': 0.13.2 - '@shikijs/transformers': 3.21.0 - '@tailwindcss/typography': 0.5.19 - '@tailwindcss/vite': 4.1.18 - '@types/debug': 4.1.12 - '@types/hast': 3.0.4 - '@types/kuromoji': 0.1.3 - '@types/mdast': 4.0.4 - '@types/node': 25.0.6 - '@types/nprogress': 0.2.3 - '@types/react': 19.2.8 - '@types/react-dom': 19.2.3 - '@types/unist': 3.0.3 - '@vercel/og': 0.8.6 + "@ai-sdk/google": 3.0.6 + "@biomejs/biome": 2.3.11 + "@coji/durably": 0.7.0 + "@coji/durably-react": 0.7.0 + "@coji/zodix": 0.7.0 + "@conform-to/react": 1.15.1 + "@conform-to/zod": 1.15.1 + "@mdx-js/rollup": 3.1.1 + "@react-router/dev": 7.12.0 + "@react-router/fs-routes": 7.12.0 + "@react-router/node": 7.12.0 + "@react-router/remix-routes-option-adapter": 7.12.0 + "@react-router/serve": 7.12.0 + "@rehype-pretty/transformers": 0.13.2 + "@shikijs/transformers": 3.21.0 + "@tailwindcss/typography": 0.5.19 + "@tailwindcss/vite": 4.1.18 + "@types/better-sqlite3": 7.6.13 + "@types/debug": 4.1.12 + "@types/hast": 3.0.4 + "@types/kuromoji": 0.1.3 + "@types/mdast": 4.0.4 + "@types/node": 25.0.6 + "@types/nprogress": 0.2.3 + "@types/react": 19.2.8 + "@types/react-dom": 19.2.3 + "@types/unist": 3.0.3 + "@vercel/og": 0.8.6 ai: 6.0.27 autoprefixer: 10.4.20 + better-sqlite3: 12.6.0 cheerio: 1.1.2 class-variance-authority: 0.7.1 clsx: 2.1.1 @@ -43,6 +46,8 @@ catalog: hast-util-to-html: 9.0.5 isbot: 5.1.32 kuromoji: 0.1.2 + kysely: 0.28.9 + kysely-codegen: 0.19.0 lucide-react: 0.562.0 mdast: 3.0.0 neverthrow: 8.2.0 @@ -88,6 +93,7 @@ catalog: unist: 0.0.1 unist-util-visit: 5.0.0 vite: 7.3.1 + vite-plugin-devtools-json: 1.0.0 vite-tsconfig-paths: 6.0.4 vitest: 4.0.16 wrangler: 4.58.0