diff --git a/apps/desktop/configs/vite.electron-render.config.ts b/apps/desktop/configs/vite.electron-render.config.ts index 395f0ccc3..0bbe6a3ef 100644 --- a/apps/desktop/configs/vite.electron-render.config.ts +++ b/apps/desktop/configs/vite.electron-render.config.ts @@ -1,18 +1,13 @@ import { fileURLToPath } from "node:url" import { dirname, resolve } from "pathe" -import { tsImport } from "tsx/esm/api" import type { UserConfig } from "vite" +import { routeBuilderPlugin } from "vite-plugin-route-builder" import { cleanupUnnecessaryFilesPlugin } from "../plugins/vite/cleanup" import { createPlatformSpecificImportPlugin } from "../plugins/vite/specific-import" import { viteRenderBaseConfig } from "./vite.render.config" -const routeBuilderPluginV2 = await tsImport( - "@follow-app/vite-plugin-route-builder", - import.meta.url, -).then((m) => m.default) - const root = resolve(fileURLToPath(dirname(import.meta.url)), "..") const VITE_ROOT = resolve(root, "layer/renderer") @@ -26,7 +21,7 @@ export default { plugins: [ ...viteRenderBaseConfig.plugins, createPlatformSpecificImportPlugin("electron"), - routeBuilderPluginV2({ + routeBuilderPlugin({ pagePattern: "src/pages/**/*.tsx", outputPath: "src/generated-routes.ts", enableInDev: true, diff --git a/apps/desktop/layer/renderer/package.json b/apps/desktop/layer/renderer/package.json index 4db300a89..85841d36f 100644 --- a/apps/desktop/layer/renderer/package.json +++ b/apps/desktop/layer/renderer/package.json @@ -12,16 +12,18 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@ai-sdk/openai": "2.0.0-beta.5", - "@ai-sdk/react": "2.0.0-beta.11", + "@ai-sdk/openai": "2.0.0-beta.11", + "@ai-sdk/react": "2.0.0-beta.25", "@dnd-kit/core": "6.3.1", "@dnd-kit/sortable": "10.0.0", "@electron-toolkit/preload": "3.0.2", + "@follow-app/client-sdk": "catalog:", "@follow/database": "workspace:*", "@follow/electron-main": "workspace:*", "@follow/shared": "workspace:*", "@follow/store": "workspace:*", "@follow/tracker": "workspace:*", + "@folo-services/constants": "0.1.6", "@fontsource/sn-pro": "5.2.5", "@hcaptcha/react-hcaptcha": "1.12.0", "@headlessui/react": "2.2.4", @@ -48,7 +50,7 @@ "@use-gesture/react": "10.3.1", "@welldone-software/why-did-you-render": "10.0.1", "@yornaath/batshit": "0.10.1", - "ai": "5.0.0-beta.11", + "ai": "5.0.0-beta.25", "camelcase-keys": "9.1.3", "class-variance-authority": "0.7.1", "clsx": "2.1.1", @@ -114,6 +116,7 @@ "@follow/models": "workspace:*", "@follow/types": "workspace:*", "@follow/utils": "workspace:*", + "@folo-services/ai-tools": "0.2.14", "@types/node": "24.0.10", "@vite-pwa/assets-generator": "1.0.0", "fake-indexeddb": "6.0.1", diff --git a/apps/desktop/layer/renderer/src/atoms/server-configs.ts b/apps/desktop/layer/renderer/src/atoms/server-configs.ts index 418dc7191..312f7c253 100644 --- a/apps/desktop/layer/renderer/src/atoms/server-configs.ts +++ b/apps/desktop/layer/renderer/src/atoms/server-configs.ts @@ -1,11 +1,11 @@ -import type { ServerConfigs } from "@follow/models/types" +import type { ExtractResponseData, GetStatusConfigsResponse } from "@follow-app/client-sdk" import PKG from "@pkg" import { atom } from "jotai" import { createAtomHooks } from "~/lib/jotai" export const [, , useServerConfigs, , getServerConfigs, setServerConfigs] = createAtomHooks( - atom>(null), + atom>>(null), ) export const useIsInMASReview = () => { diff --git a/apps/desktop/layer/renderer/src/lib/api-client.ts b/apps/desktop/layer/renderer/src/lib/api-client.ts new file mode 100644 index 000000000..cada22f55 --- /dev/null +++ b/apps/desktop/layer/renderer/src/lib/api-client.ts @@ -0,0 +1,98 @@ +import { env } from "@follow/shared/env.desktop" +import { userActions } from "@follow/store/user/store" +import { createDesktopAPIHeaders } from "@follow/utils/headers" +import { FollowClient } from "@follow-app/client-sdk" +import PKG from "@pkg" +import { createElement } from "react" +import { toast } from "sonner" + +import { NetworkStatus, setApiStatus } from "~/atoms/network" +import { setLoginModalShow } from "~/atoms/user" +import { NeedActivationToast } from "~/modules/activation/NeedActivationToast" + +import { getClientId, getSessionId } from "./client-session" + +export const followClient = new FollowClient({ + credentials: "include", + timeout: 10000, + baseURL: env.VITE_API_URL, + fetch: async (input, options = {}) => + fetch(input.toString(), { + ...options, + cache: "no-store", + }), +}) + +export const followApi = followClient.api +followClient.addRequestInterceptor(async (ctx) => { + const { options } = ctx + const header = options.headers || {} + header["X-Client-Id"] = getClientId() + header["X-Session-Id"] = getSessionId() + + const apiHeader = createDesktopAPIHeaders({ version: PKG.version }) + + options.headers = { + ...header, + ...apiHeader, + } + return ctx +}) + +followClient.addResponseInterceptor(({ response }) => { + setApiStatus(NetworkStatus.ONLINE) + return response +}) + +followClient.addErrorInterceptor(async ({ error, response }) => { + const { router } = window + + // If api is down + if ((!response || response.status === 0) && navigator.onLine) { + setApiStatus(NetworkStatus.OFFLINE) + } else { + setApiStatus(NetworkStatus.ONLINE) + } + + if (!response) { + return error + } + + if (response.status === 401) { + // Or we can present LoginModal here. + // router.navigate("/login") + // If any response status is 401, we can set auth fail. Maybe some bug, but if navigate to login page, had same issues + setLoginModalShow(true) + userActions.removeCurrentUser() + } + try { + const json = await response.clone().json() + + if (response.status === 400 && json.code === 1003) { + router.navigate("/invitation") + } + if (json.code.toString().startsWith("11")) { + setTimeout(() => { + const toastId = toast.error( + createElement(NeedActivationToast, { + dimiss: () => { + toast.dismiss(toastId) + }, + }), + { + closeButton: true, + duration: 10e4, + + classNames: { + content: tw`w-full`, + }, + }, + ) + }, 500) + } + } catch { + // ignore + } + + return error +}) diff --git a/apps/desktop/layer/renderer/src/lib/features.tsx b/apps/desktop/layer/renderer/src/lib/features.tsx index a1001f0ff..7de933ade 100644 --- a/apps/desktop/layer/renderer/src/lib/features.tsx +++ b/apps/desktop/layer/renderer/src/lib/features.tsx @@ -1,11 +1,12 @@ -import type { ServerConfigs } from "@follow/models/types" +import type { ExtractResponseData, GetStatusConfigsResponse } from "@follow-app/client-sdk" import type { FC } from "react" import { useFeature } from "~/hooks/biz/useFeature" -export const featureConfigMap: Record = { - ai: "AI_CHAT_ENABLED", -} +export const featureConfigMap: Record> = + { + ai: "AI_CHAT_ENABLED", + } export const withFeature = (feature: keyof typeof featureConfigMap) => diff --git a/apps/desktop/layer/renderer/src/main.tsx b/apps/desktop/layer/renderer/src/main.tsx index 283988fef..20c75d171 100644 --- a/apps/desktop/layer/renderer/src/main.tsx +++ b/apps/desktop/layer/renderer/src/main.tsx @@ -4,9 +4,10 @@ import "./styles/main.css" import { IN_ELECTRON, WEB_BUILD } from "@follow/shared/constants" import { - apiClientSimpleContext, - authClientSimpleContext, - queryClientSimpleContext, + apiClientContext, + apiContext, + authClientContext, + queryClientContext, } from "@follow/store/context" import { getOS } from "@follow/utils/utils" import * as React from "react" @@ -20,12 +21,14 @@ import { setAppIsReady } from "./atoms/app" import { ElECTRON_CUSTOM_TITLEBAR_HEIGHT } from "./constants" import { initializeApp } from "./initialize" import { registerAppGlobalShortcuts } from "./initialize/global-shortcuts" +import { followApi } from "./lib/api-client" import { queryClient } from "./lib/query-client" import { router } from "./router" -apiClientSimpleContext.provide(apiClient) -authClientSimpleContext.provide(authClient) -queryClientSimpleContext.provide(queryClient) +apiClientContext.provide(apiClient) +authClientContext.provide(authClient) +queryClientContext.provide(queryClient) +apiContext.provide(followApi) initializeApp().finally(() => { import("./push-notification").then(({ registerWebPushNotifications }) => { diff --git a/apps/desktop/layer/renderer/src/modules/ai/chat/__internal__/types.ts b/apps/desktop/layer/renderer/src/modules/ai/chat/__internal__/types.ts index 84c7fcb7a..3a86ccf9d 100644 --- a/apps/desktop/layer/renderer/src/modules/ai/chat/__internal__/types.ts +++ b/apps/desktop/layer/renderer/src/modules/ai/chat/__internal__/types.ts @@ -1,5 +1,4 @@ -import type { tools as honoTools } from "@follow/shared/hono" -import type { Tool, UIDataTypes, UIMessage } from "ai" +import type { BizUITools, ToolWithState } from "@folo-services/ai-tools" export interface AIChatContextBlock { id: string @@ -18,35 +17,9 @@ export interface AIChatContextBlocks { blocks: AIChatContextBlock[] } -// TypeScript utility to transform Tool to { input: Input, output: Output } -type TransformTool = - T extends Tool - ? { - input: Input - output: Output - } - : never - -// Transform the tools object to UITools format -type TransformTools = { - [K in keyof T]: TransformTool -} - -// Apply the transformation to the hono tools -export type BizUITools = TransformTools - -export type BizUIMetadata = { - startTime?: string - finishTime?: string - totalTokens?: number - duration?: number -} - -export type BizUIMessage = UIMessage -type ToolWithState = T & { - state: "input-streaming" | "input-available" | "output-available" | "output-error" -} export type AIDisplayAnalyticsTool = ToolWithState export type AIDisplayFeedsTool = ToolWithState export type AIDisplayEntriesTool = ToolWithState export type AIDisplaySubscriptionsTool = ToolWithState + +export { type BizUIMessage, type BizUIMetadata } from "@folo-services/ai-tools" diff --git a/apps/desktop/layer/renderer/src/modules/settings/utils.ts b/apps/desktop/layer/renderer/src/modules/settings/utils.ts index e3ae649fe..19e85c989 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/utils.ts +++ b/apps/desktop/layer/renderer/src/modules/settings/utils.ts @@ -1,5 +1,5 @@ import type { UserRole } from "@follow/constants" -import type { ServerConfigs } from "@follow/models" +import type { ExtractResponseData, GetStatusConfigsResponse } from "@follow-app/client-sdk" export interface SettingPageContext { role: Nullable @@ -17,10 +17,13 @@ export interface SettingPageConfig { title?: I18nKeysForSettings priority: number headerIcon?: string | React.ReactNode - hideIf?: (ctx: SettingPageContext, serverConfigs?: ServerConfigs | null) => boolean + hideIf?: ( + ctx: SettingPageContext, + serverConfigs?: ExtractResponseData | null, + ) => boolean disableIf?: ( ctx: SettingPageContext, - serverConfigs?: ServerConfigs | null, + serverConfigs?: ExtractResponseData | null, ) => [boolean, DisableWhy] viewportClassName?: string } diff --git a/apps/desktop/layer/renderer/src/modules/subscription-column/CategoryRenameContent.tsx b/apps/desktop/layer/renderer/src/modules/subscription-column/CategoryRenameContent.tsx deleted file mode 100644 index 6ee79fba5..000000000 --- a/apps/desktop/layer/renderer/src/modules/subscription-column/CategoryRenameContent.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { Button } from "@follow/components/ui/button/index.js" -import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from "@follow/components/ui/form/index.jsx" -import { Input } from "@follow/components/ui/input/index.js" -import { subscriptionSyncService } from "@follow/store/subscription/store" -import { zodResolver } from "@hookform/resolvers/zod" -import { useMutation } from "@tanstack/react-query" -import { useEffect } from "react" -import { useForm } from "react-hook-form" -import { z } from "zod" - -import { useCurrentModal } from "~/components/ui/modal/stacked/hooks" -import { apiClient } from "~/lib/api-fetch" - -const formSchema = z.object({ - category: z.string(), -}) - -export function CategoryRenameContent({ - feedIdList, - onSuccess, - category, -}: { - feedIdList: string[] - onSuccess?: () => void - category: string -}) { - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - category, - }, - }) - - const renameMutation = useMutation({ - mutationFn: async (values: z.infer) => - apiClient.categories.$patch({ - json: { - feedIdList, - category: values.category, - }, - }), - onSuccess: () => { - subscriptionSyncService.fetch() - - onSuccess?.() - }, - }) - - function onSubmit(values: z.infer) { - renameMutation.mutate(values) - } - - const { setClickOutSideToDismiss } = useCurrentModal() - - useEffect(() => { - setClickOutSideToDismiss(!form.formState.isDirty) - }, [form.formState.isDirty]) - return ( -
- - ( - - - - - - - )} - /> -
- -
- - - ) -} diff --git a/apps/desktop/layer/renderer/src/modules/subscription-column/FeedCategory.tsx b/apps/desktop/layer/renderer/src/modules/subscription-column/FeedCategory.tsx index 828aa0cb7..d8205e157 100644 --- a/apps/desktop/layer/renderer/src/modules/subscription-column/FeedCategory.tsx +++ b/apps/desktop/layer/renderer/src/modules/subscription-column/FeedCategory.tsx @@ -3,6 +3,7 @@ import { useMobile } from "@follow/components/hooks/useMobile.js" import { MotionButtonBase } from "@follow/components/ui/button/index.js" import { LoadingCircle } from "@follow/components/ui/loading/index.jsx" import { useScrollViewElement } from "@follow/components/ui/scroll-area/hooks.js" +import { ShrinkingFocusBorder } from "@follow/components/ui/shrinking-focus-border/index.js" import type { FeedViewType } from "@follow/constants" import { views } from "@follow/constants" import { useInputComposition, useRefValue } from "@follow/hooks" @@ -16,7 +17,7 @@ import { subscriptionActions, subscriptionSyncService } from "@follow/store/subs import { getDefaultCategory } from "@follow/store/subscription/utils" import { useSortedIdsByUnread, useUnreadByIds } from "@follow/store/unread/hooks" import { unreadSyncService } from "@follow/store/unread/store" -import { stopPropagation } from "@follow/utils/dom" +import { nextFrame, stopPropagation } from "@follow/utils/dom" import { cn, sortByAlphabet } from "@follow/utils/utils" import { useMutation } from "@tanstack/react-query" import { AnimatePresence, m } from "motion/react" @@ -410,6 +411,8 @@ const RenameCategoryForm: FC<{ }, }) const formRef = useRef(null) + const [isFocused, setIsFocused] = useState(false) + useOnClickOutside( formRef as React.RefObject, () => { @@ -419,7 +422,10 @@ const RenameCategoryForm: FC<{ ) const inputRef = useRef(null) useEffect(() => { - inputRef.current?.focus() + nextFrame(() => { + inputRef.current?.focus() + setIsFocused(true) + }) }, []) const compositionInputProps = useInputComposition({ onKeyDown: (e) => { @@ -429,33 +435,37 @@ const RenameCategoryForm: FC<{ }, }) return ( -
{ - e.preventDefault() +
+ + { + e.preventDefault() - return renameMutation.mutateAsync({ - lastCategory: currentCategory!, - newCategory: e.currentTarget.category.value, - }) - }} - > - - - - - + setIsFocused(true)} + onBlur={() => setIsFocused(false)} + /> + + + + +
) } diff --git a/apps/desktop/layer/renderer/src/queries/server-configs.ts b/apps/desktop/layer/renderer/src/queries/server-configs.ts index 272ff62ed..1fdc4ff19 100644 --- a/apps/desktop/layer/renderer/src/queries/server-configs.ts +++ b/apps/desktop/layer/renderer/src/queries/server-configs.ts @@ -1,11 +1,11 @@ import { useQuery } from "@tanstack/react-query" -import { apiClient } from "~/lib/api-fetch" +import { followApi } from "~/lib/api-client" export const useServerConfigsQuery = () => { const { data } = useQuery({ queryKey: ["server-configs"], - queryFn: () => apiClient.status.configs.$get(), + queryFn: () => followApi.status.getConfigs(), }) return data?.data } diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 4a5ee3bb2..579d5a5c4 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -44,7 +44,6 @@ "@electron-forge/plugin-fuses": "7.8.0", "@electron-forge/publisher-github": "7.8.0", "@electron-toolkit/tsconfig": "1.0.1", - "@follow-app/vite-plugin-route-builder": "workspace:*", "@follow/components": "workspace:*", "@follow/configs": "workspace:*", "@follow/constants": "workspace:*", @@ -64,7 +63,7 @@ "bufferutil": "4.0.9", "code-inspector-plugin": "0.20.15", "cssnano": "7.0.7", - "drizzle-orm": "0.44.2", + "drizzle-orm": "0.44.3", "electron": "37.2.0", "electron-devtools-installer": "4.0.0", "electron-packager-languages": "0.6.0", @@ -86,6 +85,7 @@ "vite-bundle-analyzer": "1.0.0", "vite-plugin-mkcert": "1.17.8", "vite-plugin-pwa": "1.0.1", + "vite-plugin-route-builder": "0.3.0", "vite-tsconfig-paths": "5.1.4" }, "productName": "Folo", diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index 8aec3aba3..0887af7ba 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -7,12 +7,12 @@ import { minify as htmlMinify } from "html-minifier-terser" import { cyan, dim, green } from "kolorist" import { parseHTML } from "linkedom" import { resolve } from "pathe" -import { tsImport } from "tsx/esm/api" import type { PluginOption, ResolvedConfig, ViteDevServer } from "vite" import { defineConfig, loadEnv } from "vite" import { analyzer } from "vite-bundle-analyzer" import mkcert from "vite-plugin-mkcert" import { VitePWA } from "vite-plugin-pwa" +import { routeBuilderPlugin } from "vite-plugin-route-builder" import { viteRenderBaseConfig } from "./configs/vite.render.config" import { createDependencyChunksPlugin } from "./plugins/vite/deps" @@ -21,11 +21,6 @@ import { localesPlugin } from "./plugins/vite/locales" import manifestPlugin from "./plugins/vite/manifest" import { createPlatformSpecificImportPlugin } from "./plugins/vite/specific-import" -const routeBuilderPluginV2 = await tsImport( - "@follow-app/vite-plugin-route-builder", - import.meta.url, -).then((m) => m.default) - const __dirname = fileURLToPath(new URL(".", import.meta.url)) const isCI = process.env.CI === "true" || process.env.CI === "1" const ROOT = resolve(__dirname, "./layer/renderer") @@ -142,7 +137,7 @@ export default ({ mode }) => { plugins: [ ...((viteRenderBaseConfig.plugins ?? []) as any), - routeBuilderPluginV2({ + routeBuilderPlugin({ pagePattern: "src/pages/**/*.tsx", outputPath: "src/generated-routes.ts", enableInDev: true, diff --git a/apps/ssr/package.json b/apps/ssr/package.json index d47e7a94d..cf2f2470f 100644 --- a/apps/ssr/package.json +++ b/apps/ssr/package.json @@ -43,7 +43,6 @@ "zod": "3.25.75" }, "devDependencies": { - "@follow-app/vite-plugin-route-builder": "workspace:*", "@follow/components": "workspace:*", "@follow/configs": "workspace:*", "@follow/constants": "workspace:*", @@ -70,6 +69,7 @@ "tsdown": "0.12.9", "tsx": "4.20.3", "typescript": "catalog:", - "vite": "7.0.2" + "vite": "7.0.2", + "vite-plugin-route-builder": "0.3.0" } } diff --git a/apps/ssr/vite.config.mts b/apps/ssr/vite.config.mts index dd7bd7038..ea9dc82e2 100644 --- a/apps/ssr/vite.config.mts +++ b/apps/ssr/vite.config.mts @@ -3,17 +3,12 @@ import { fileURLToPath } from "node:url" import react from "@vitejs/plugin-react" import { codeInspectorPlugin } from "code-inspector-plugin" import { dirname, resolve } from "pathe" -import { tsImport } from "tsx/esm/api" import { defineConfig } from "vite" +import { routeBuilderPlugin } from "vite-plugin-route-builder" import { viteRenderBaseConfig } from "../desktop/configs/vite.render.config" import { astPlugin } from "../desktop/plugins/vite/ast" -const routeBuilderPluginV2 = await tsImport( - "@follow-app/vite-plugin-route-builder", - import.meta.url, -).then((m) => m.default) - const __dirname = dirname(fileURLToPath(import.meta.url)) export default defineConfig({ @@ -38,7 +33,7 @@ export default defineConfig({ }, }, plugins: [ - routeBuilderPluginV2({ + routeBuilderPlugin({ pagePattern: "client/pages/**/*.tsx", outputPath: "client/generated-routes.ts", enableInDev: true, diff --git a/packages/internal/components/src/ui/shrinking-focus-border/index.tsx b/packages/internal/components/src/ui/shrinking-focus-border/index.tsx new file mode 100644 index 000000000..19eab7a49 --- /dev/null +++ b/packages/internal/components/src/ui/shrinking-focus-border/index.tsx @@ -0,0 +1,262 @@ +import type { FC } from "react" +import { useEffect, useRef, useState } from "react" + +import { RootPortal } from "../portal" + +export interface ShrinkingFocusBorderProps { + isVisible: boolean + containerRef: React.RefObject + persistBorder?: boolean + radius?: number +} + +export const ShrinkingFocusBorder: FC = ({ + isVisible, + containerRef, + persistBorder = false, + radius = 6, +}) => { + const canvasRef = useRef(null) + const animationFrameRef = useRef(undefined) + const startTimeRef = useRef(undefined) + const resizeObserverRef = useRef(undefined) + const [currentRect, setCurrentRect] = useState(null) + const [isAnimating, setIsAnimating] = useState(false) + const transitionStartRef = useRef(undefined) + const previousRectRef = useRef(null) + + // Reset animation state when visibility changes + useEffect(() => { + if (isVisible) { + setIsAnimating(true) + previousRectRef.current = null + } else { + setIsAnimating(false) + setCurrentRect(null) + } + }, [isVisible]) + + // Setup resize observer for persistent border + useEffect(() => { + if (!persistBorder || !containerRef.current || !canvasRef.current) { + return + } + + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + const newRect = entry.target.getBoundingClientRect() + if ( + currentRect && + (Math.abs(newRect.width - currentRect.width) > 1 || + Math.abs(newRect.height - currentRect.height) > 1 || + Math.abs(newRect.left - currentRect.left) > 1 || + Math.abs(newRect.top - currentRect.top) > 1) + ) { + previousRectRef.current = currentRect + setCurrentRect(newRect) + transitionStartRef.current = Date.now() + } + } + }) + + observer.observe(containerRef.current) + resizeObserverRef.current = observer + + return () => { + observer.disconnect() + } + }, [persistBorder, containerRef, currentRect]) + + useEffect(() => { + if (!isVisible || !containerRef.current || !canvasRef.current) { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current) + } + return + } + + // Delay animation start to ensure proper positioning + const animationTimeout = setTimeout(() => { + const canvas = canvasRef.current + const container = containerRef.current + + if (!canvas || !container) return + + const ctx = canvas.getContext("2d") + if (!ctx) return + + // Get fresh rect after DOM has settled + const rect = container.getBoundingClientRect() + setCurrentRect(rect) + + // Canvas positioned fixed to viewport + canvas.width = rect.width + 100 + canvas.height = rect.height + 100 + + // Position canvas relative to viewport + canvas.style.left = `${rect.left - 50}px` + canvas.style.top = `${rect.top - 50}px` + + startTimeRef.current = Date.now() + + const drawBorder = () => { + if (!ctx || !canvas) return + + const now = Date.now() + const elapsed = (now - (startTimeRef.current || 0)) / 1000 + const duration = 0.4 // Animation duration in seconds + + // Clear canvas + ctx.clearRect(0, 0, canvas.width, canvas.height) + + let borderWidth = rect.width + let borderHeight = rect.height + let borderX = canvas.width / 2 - borderWidth / 2 + let borderY = canvas.height / 2 - borderHeight / 2 + + if (isAnimating) { + if (elapsed >= duration) { + // Animation complete + setIsAnimating(false) + if (!persistBorder) { + // Stop animation completely for non-persistent border + return + } + // For persistent border, continue with final dimensions + borderWidth = rect.width + borderHeight = rect.height + borderX = canvas.width / 2 - borderWidth / 2 + borderY = canvas.height / 2 - borderHeight / 2 + } else { + // Initial shrinking animation in progress + const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3) + const progress = Math.min(elapsed / duration, 1) + const easedProgress = easeOutCubic(progress) + + const startWidth = rect.width + 80 + const startHeight = rect.height + 80 + + borderWidth = startWidth - (startWidth - rect.width) * easedProgress + borderHeight = startHeight - (startHeight - rect.height) * easedProgress + borderX = canvas.width / 2 - borderWidth / 2 + borderY = canvas.height / 2 - borderHeight / 2 + } + } else if (persistBorder && previousRectRef.current && transitionStartRef.current) { + // Resize transition animation + const transitionElapsed = (now - transitionStartRef.current) / 1000 + const transitionDuration = 0.3 + + if (transitionElapsed <= transitionDuration) { + const progress = Math.min(transitionElapsed / transitionDuration, 1) + const easeInOutCubic = (t: number) => + t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2 + const easedProgress = easeInOutCubic(progress) + + const prevRect = previousRectRef.current + borderWidth = prevRect.width + (rect.width - prevRect.width) * easedProgress + borderHeight = prevRect.height + (rect.height - prevRect.height) * easedProgress + + // Update canvas position during transition + const currentLeft = prevRect.left + (rect.left - prevRect.left) * easedProgress + const currentTop = prevRect.top + (rect.top - prevRect.top) * easedProgress + + canvas.style.left = `${currentLeft - 50}px` + canvas.style.top = `${currentTop - 50}px` + canvas.width = borderWidth + 100 + canvas.height = borderHeight + 100 + + borderX = canvas.width / 2 - borderWidth / 2 + borderY = canvas.height / 2 - borderHeight / 2 + + if (progress >= 1) { + previousRectRef.current = null + transitionStartRef.current = undefined + } + } + } else if (persistBorder) { + // Update canvas position for current rect + canvas.style.left = `${rect.left - 50}px` + canvas.style.top = `${rect.top - 50}px` + canvas.width = rect.width + 100 + canvas.height = rect.height + 100 + borderX = canvas.width / 2 - borderWidth / 2 + borderY = canvas.height / 2 - borderHeight / 2 + } + + // Draw border only if animating or persistBorder is true + if (isAnimating || persistBorder) { + // Get dynamic color from CSS variable + const computedStyle = getComputedStyle(document.documentElement) + const foColor = computedStyle.getPropertyValue("--fo-a").trim() + + // Parse HSL string (e.g., "21.6 100% 50%") and convert to usable format + const hslMatch = foColor.match(/^(\d+(?:\.\d+)?)\s+(\d+)%\s+(\d+)%$/) + let strokeColor = "rgba(59, 130, 246, 0.8)" // fallback + let shadowColor = "rgba(59, 130, 246, 0.5)" // fallback + + if (hslMatch) { + const [, h, s, l] = hslMatch + strokeColor = `hsla(${h}, ${s}%, ${l}%, 0.8)` + shadowColor = `hsla(${h}, ${s}%, ${l}%, 0.5)` + } + + // Border style + ctx.strokeStyle = strokeColor + ctx.lineWidth = 2 + ctx.shadowColor = shadowColor + ctx.shadowBlur = 6 + + // Draw rounded rectangle border + ctx.beginPath() + ctx.roundRect(borderX, borderY, borderWidth, borderHeight, radius) + ctx.stroke() + } + + // Continue animation if needed + if ( + isAnimating || + (persistBorder && previousRectRef.current && transitionStartRef.current) || + (persistBorder && elapsed > duration) + ) { + animationFrameRef.current = requestAnimationFrame(drawBorder) + } + } + + drawBorder() + }, 16) // One frame delay to ensure DOM positioning + + return () => { + clearTimeout(animationTimeout) + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current) + } + } + }, [isVisible, containerRef, isAnimating, persistBorder, radius]) + + // Cleanup + useEffect(() => { + return () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current) + } + if (resizeObserverRef.current) { + resizeObserverRef.current.disconnect() + } + } + }, []) + + if (!isVisible) return null + + // If not persisting border and animation is complete, don't render + if (!persistBorder && !isAnimating) return null + + return ( + + + + ) +} diff --git a/packages/internal/constants/package.json b/packages/internal/constants/package.json index d7996c1fc..c95788465 100644 --- a/packages/internal/constants/package.json +++ b/packages/internal/constants/package.json @@ -11,6 +11,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@follow-app/client-sdk": "catalog:", "@follow/configs": "workspace:*", "@follow/types": "workspace:*" } diff --git a/packages/internal/constants/src/enums.ts b/packages/internal/constants/src/enums.ts index be03cc7fe..f3159b55c 100644 --- a/packages/internal/constants/src/enums.ts +++ b/packages/internal/constants/src/enums.ts @@ -1,12 +1,4 @@ -export enum FeedViewType { - Articles = 0, - SocialMedia = 1, - Pictures = 2, - Videos = 3, - Audios = 4, - Notifications = 5, -} - +export { FeedViewType } from "@follow-app/client-sdk" export enum Routes { Timeline = "/timeline", Discover = "/discover", diff --git a/packages/internal/database/package.json b/packages/internal/database/package.json index 5f1cc1727..7616ab968 100644 --- a/packages/internal/database/package.json +++ b/packages/internal/database/package.json @@ -32,11 +32,12 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@follow-app/client-sdk": "catalog:", "@follow/constants": "workspace:*", "@follow/models": "workspace:*", "@follow/shared": "workspace:*", "ai": "5.0.0-beta.7", - "drizzle-orm": "0.44.2", + "drizzle-orm": "0.44.3", "expo-sqlite": "15.2.12", "sqlocal": "npm:@hyoban/sqlocal@0.14.1-fork.4", "wa-sqlite": "git+https://github.com/rhashimoto/wa-sqlite.git#v1.0.8" diff --git a/packages/internal/database/src/schemas/index.ts b/packages/internal/database/src/schemas/index.ts index 8f8685021..f188197f5 100644 --- a/packages/internal/database/src/schemas/index.ts +++ b/packages/internal/database/src/schemas/index.ts @@ -1,6 +1,6 @@ import type { FeedViewType } from "@follow/constants" -import type { ActionSettings } from "@follow/models/types" import type { SupportedActionLanguage } from "@follow/shared/language" +import type { EntrySettings } from "@follow-app/client-sdk" import type { UIMessage } from "ai" import { sql } from "drizzle-orm" import { integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core" @@ -82,7 +82,6 @@ export const usersTable = sqliteTable("users", { discord?: string }>(), }) - export const entriesTable = sqliteTable("entries", { id: text("id").primaryKey(), title: text("title"), @@ -108,7 +107,7 @@ export const entriesTable = sqliteTable("entries", { inboxHandle: text("inbox_handle"), read: integer("read", { mode: "boolean" }), sources: text("sources", { mode: "json" }).$type(), - settings: text("settings", { mode: "json" }).$type(), + settings: text("settings", { mode: "json" }).$type(), }) export const collectionsTable = sqliteTable("collections", { diff --git a/packages/internal/models/package.json b/packages/internal/models/package.json index 7517b3cd6..04e170cb8 100644 --- a/packages/internal/models/package.json +++ b/packages/internal/models/package.json @@ -20,6 +20,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@follow-app/client-sdk": "catalog:", "@follow/constants": "workspace:*", "@follow/shared": "workspace:*", "@follow/types": "workspace:*", diff --git a/packages/internal/models/src/types.ts b/packages/internal/models/src/types.ts index 75ed8b217..e1aee62d8 100644 --- a/packages/internal/models/src/types.ts +++ b/packages/internal/models/src/types.ts @@ -65,8 +65,6 @@ export type EntriesResponse = Array< | Exclude>["data"], undefined> >[number] -export type ActionSettings = Exclude - export type CombinedEntryModel = Omit & { entries: { content?: string | null diff --git a/packages/internal/shared/package.json b/packages/internal/shared/package.json index 58befecbc..d97ca7267 100644 --- a/packages/internal/shared/package.json +++ b/packages/internal/shared/package.json @@ -38,7 +38,7 @@ "@t3-oss/env-core": "0.13.8", "ai": "5.0.0-beta.6", "better-auth": "1.2.9", - "drizzle-orm": "0.44.2", + "drizzle-orm": "0.44.3", "hono": "4.8.1", "sonner": "2.0.6", "stripe": "18.2.1", diff --git a/packages/internal/store/package.json b/packages/internal/store/package.json index dbbf4a267..deb554d6f 100644 --- a/packages/internal/store/package.json +++ b/packages/internal/store/package.json @@ -114,6 +114,7 @@ "react": "19.0.0" }, "dependencies": { + "@follow-app/client-sdk": "catalog:", "@follow/configs": "workspace:*", "@follow/constants": "workspace:*", "@follow/database": "workspace:*", diff --git a/packages/internal/store/src/context.ts b/packages/internal/store/src/context.ts index 720efea67..908be5e2d 100644 --- a/packages/internal/store/src/context.ts +++ b/packages/internal/store/src/context.ts @@ -1,12 +1,12 @@ import type { AuthClient } from "@follow/shared/auth" import type { QueryClient } from "@tanstack/react-query" -import type { APIClient } from "./types" +import type { APIClient, FollowAPI } from "./types" const NO_VALUE_DEFAULT = Symbol("NO_VALUE_DEFAULT") type ContextValue = T | typeof NO_VALUE_DEFAULT -function createSimpleContext() { +function createJSContext() { let contextValue: ContextValue = NO_VALUE_DEFAULT const provide = (value: T) => { @@ -26,9 +26,11 @@ function createSimpleContext() { } } -export const apiClientSimpleContext = createSimpleContext() -export const authClientSimpleContext = createSimpleContext() -export const queryClientSimpleContext = createSimpleContext() -export const apiClient = apiClientSimpleContext.consumer -export const authClient = authClientSimpleContext.consumer -export const queryClient = queryClientSimpleContext.consumer +export const apiClientContext = createJSContext() +export const apiContext = createJSContext() +export const authClientContext = createJSContext() +export const queryClientContext = createJSContext() +export const apiClient = apiClientContext.consumer +export const api = apiContext.consumer +export const authClient = authClientContext.consumer +export const queryClient = queryClientContext.consumer diff --git a/packages/internal/store/src/entry/store.ts b/packages/internal/store/src/entry/store.ts index e76d47632..cab745e23 100644 --- a/packages/internal/store/src/entry/store.ts +++ b/packages/internal/store/src/entry/store.ts @@ -5,10 +5,11 @@ import { debounce } from "es-toolkit/compat" import { clearAllFeedUnreadDirty, clearFeedUnreadDirty } from "../atoms/feed" import { collectionActions } from "../collection/store" -import { apiClient } from "../context" +import { api, apiClient } from "../context" import { feedActions } from "../feed/store" import type { Hydratable, Resetable } from "../internal/base" import { createImmerSetter, createTransaction, createZustandStore } from "../internal/helper" +import { apiMorph } from "../morph/api" import { dbStoreMorph } from "../morph/db-store" import { honoMorph } from "../morph/hono" import { storeDbMorph } from "../morph/store-db" @@ -471,25 +472,21 @@ class EntrySyncServices { }) const res = params.inboxId - ? await apiClient().entries.inbox.$post({ - json: { - publishedAfter: pageParam, - read, - limit, - isCollection, - inboxId: params.inboxId, - ...params, - }, + ? await api().entries.inbox.list({ + publishedAfter: pageParam, + read, + limit, + isCollection, + inboxId: params.inboxId, + ...params, }) - : await apiClient().entries.$post({ - json: { - publishedAfter: pageParam, - read, - limit, - isCollection, - excludePrivate, - ...params, - }, + : await api().entries.list({ + publishedAfter: pageParam, + read, + limit, + isCollection, + excludePrivate, + ...params, }) // Mark feed unread dirty, so re-fetch the unread data when view feed unread entires in the next time @@ -507,7 +504,7 @@ class EntrySyncServices { } } - const entries = honoMorph.toEntryList(res.data) + const entries = apiMorph.toEntryList(res.data) const entriesInDB = await EntryService.getEntryMany(entries.map((e) => e.id)) for (const entry of entries) { const entryInDB = entriesInDB.find((e) => e.id === entry.id) @@ -521,7 +518,7 @@ class EntrySyncServices { await entryActions.upsertMany(entries) if (typeof view === "number") { - const { collections, entryIdsNotInCollections } = honoMorph.toCollections(res.data, view) + const { collections, entryIdsNotInCollections } = apiMorph.toCollections(res.data, view) await collectionActions.upsertMany(collections, { reset: params.isCollection && !pageParam, }) @@ -529,7 +526,7 @@ class EntrySyncServices { } const dataFeeds = res.data?.map((e) => e.feeds).filter((f) => f.type === "feed") - const feeds = dataFeeds?.map((f) => honoMorph.toFeed(f)) ?? [] + const feeds = dataFeeds?.map((f) => apiMorph.toFeed(f)) ?? [] const users = dataFeeds?.flatMap((f) => f.tipUsers).filter((u) => !!u) ?? [] feedActions.upsertMany(feeds) userActions.upsertMany( diff --git a/packages/internal/store/src/morph/api.ts b/packages/internal/store/src/morph/api.ts new file mode 100644 index 000000000..5af75f512 --- /dev/null +++ b/packages/internal/store/src/morph/api.ts @@ -0,0 +1,184 @@ +import type { FeedSchema, InboxSchema } from "@follow/database/schemas/types" +import type { + EntryListResponse, + EntryWithFeed, + ExtractResponseData, + FeedViewType, + InboxListEntry, + InboxListEntryResponse, + InboxSubscriptionResponse, + ListSubscriptionResponse, + SubscriptionWithFeed, +} from "@follow-app/client-sdk" + +import type { CollectionModel } from "../collection/types" +import type { EntryModel } from "../entry/types" +import type { FeedModel } from "../feed/types" +import type { ListModel } from "../list/types" +import type { SubscriptionModel } from "../subscription/types" + +class APIMorph { + toSubscription( + data: (SubscriptionWithFeed | ListSubscriptionResponse | InboxSubscriptionResponse)[], + ) { + const subscriptions: SubscriptionModel[] = [] + + const collections = { + feeds: [], + inboxes: [], + lists: [], + } as { + feeds: FeedSchema[] + inboxes: InboxSchema[] + lists: ListModel[] + } + + for (const item of data) { + const baseSubscription = { + category: item.category!, + + userId: item.userId, + view: item.view, + isPrivate: item.isPrivate, + title: item.title, + createdAt: item.createdAt, + } as SubscriptionModel + + if ("feeds" in item) { + baseSubscription.feedId = item.feedId + baseSubscription.type = "feed" + const feed = item.feeds + collections.feeds.push({ + description: feed.description!, + id: feed.id, + errorAt: feed.errorAt!, + errorMessage: feed.errorMessage!, + image: feed.image!, + ownerUserId: feed.ownerUserId!, + siteUrl: feed.siteUrl!, + title: feed.title!, + url: feed.url, + }) + } + + if ("inboxes" in item) { + baseSubscription.inboxId = item.inboxId + baseSubscription.type = "inbox" + const inbox = item.inboxes + + collections.inboxes.push({ + id: inbox.id, + title: inbox.title, + secret: inbox.secret, + }) + } + + if ("lists" in item) { + baseSubscription.listId = item.listId + baseSubscription.type = "list" + const list = item.lists + if (list.owner) + collections.lists.push({ + id: list.id, + title: list.title!, + userId: list.owner!.id, + description: list.description!, + view: list.view, + image: list.image!, + ownerUserId: list.owner.id, + feedIds: list.feedIds!, + fee: list.fee!, + subscriptionCount: null, + purchaseAmount: null, + type: "list", + }) + } + + subscriptions.push(baseSubscription) + } + return { subscriptions, collections } + } + + toCollections( + data: ExtractResponseData, + view: FeedViewType, + ): { + collections: CollectionModel[] + entryIdsNotInCollections: string[] + } { + if (!data) return { collections: [], entryIdsNotInCollections: [] } + + const collections: CollectionModel[] = [] + const entryIdsNotInCollections: string[] = [] + for (const item of data) { + if (!("collections" in item)) { + entryIdsNotInCollections.push((item as EntryWithFeed).entries.id) + continue + } + if (item.collections) + collections.push({ + createdAt: item.collections.createdAt, + entryId: item.entries.id, + feedId: item.feeds.id, + view, + }) + } + + return { + collections, + entryIdsNotInCollections, + } + } + + toEntryList(data?: InboxListEntry[] | EntryWithFeed[]): EntryModel[] { + const entries: EntryModel[] = [] + for (const item of data ?? []) { + entries.push({ + id: item.entries.id, + title: item.entries.title, + url: item.entries.url, + content: null, + readabilityContent: null, + description: item.entries.description, + guid: item.entries.guid, + author: item.entries.author, + authorUrl: item.entries.authorUrl, + authorAvatar: item.entries.authorAvatar, + insertedAt: new Date(item.entries.insertedAt), + publishedAt: new Date(item.entries.publishedAt), + media: item.entries.media ?? null, + categories: item.entries.categories ?? null, + attachments: item.entries.attachments ?? null, + extra: item.entries.extra + ? { + links: item.entries.extra.links ?? undefined, + } + : null, + language: item.entries.language, + feedId: item.feeds.id, + inboxHandle: item.feeds.type === "inbox" ? item.feeds.id : null, + read: item.read, + sources: "from" in item ? (item.from ?? null) : null, + settings: item.settings ?? null, + }) + } + return entries + } + + toFeed(data: EntryWithFeed["feeds"]): FeedModel { + return { + type: "feed", + id: data.id, + title: data.title, + url: data.url, + image: data.image, + description: data.description, + ownerUserId: data.ownerUserId, + errorAt: data.errorAt, + errorMessage: data.errorMessage, + siteUrl: data.siteUrl, + tipUserIds: data.tipUsers ? data.tipUsers.map((user) => user.id) : [], + } + } +} +export const apiMorph = new APIMorph() diff --git a/packages/internal/store/src/morph/hono.ts b/packages/internal/store/src/morph/hono.ts index be9bf2e20..2ca429a12 100644 --- a/packages/internal/store/src/morph/hono.ts +++ b/packages/internal/store/src/morph/hono.ts @@ -1,95 +1,13 @@ -import type { FeedViewType } from "@follow/constants" -import type { FeedSchema, InboxSchema } from "@follow/database/schemas/types" - -import type { CollectionModel } from "../collection/types" import type { EntryModel } from "../entry/types" import type { FeedModel } from "../feed/types" import type { ListModel } from "../list/types" -import type { SubscriptionModel } from "../subscription/types" import type { MeModel } from "../user/store" import type { HonoApiClient } from "./types" -class Morph { - toSubscription(data: HonoApiClient.Subscription_Get) { - const subscriptions: SubscriptionModel[] = [] - - // TODO list inbox - const collections = { - feeds: [], - inboxes: [], - lists: [], - } as { - feeds: FeedSchema[] - inboxes: InboxSchema[] - lists: ListModel[] - } - - for (const item of data) { - const baseSubscription = { - category: item.category!, - - userId: item.userId, - view: item.view, - isPrivate: item.isPrivate, - title: item.title, - createdAt: item.createdAt, - } as SubscriptionModel - - if ("feeds" in item) { - baseSubscription.feedId = item.feedId - baseSubscription.type = "feed" - const feed = item.feeds - collections.feeds.push({ - description: feed.description!, - id: feed.id, - errorAt: feed.errorAt!, - errorMessage: feed.errorMessage!, - image: feed.image!, - ownerUserId: feed.ownerUserId!, - siteUrl: feed.siteUrl!, - title: feed.title!, - url: feed.url, - }) - } - - if ("inboxes" in item) { - baseSubscription.inboxId = item.inboxId - baseSubscription.type = "inbox" - const inbox = item.inboxes - - collections.inboxes.push({ - id: inbox.id, - title: inbox.title, - secret: inbox.secret, - }) - } - - if ("lists" in item) { - baseSubscription.listId = item.listId - baseSubscription.type = "list" - const list = item.lists - if (list.owner) - collections.lists.push({ - id: list.id, - title: list.title!, - userId: list.owner!.id, - description: list.description!, - view: list.view, - image: list.image!, - ownerUserId: list.owner.id, - feedIds: list.feedIds!, - fee: list.fee!, - subscriptionCount: null, - purchaseAmount: null, - type: "list", - }) - } - - subscriptions.push(baseSubscription) - } - return { subscriptions, ...collections } - } - +/** + * @deprecated + */ +class LegacyHonoMorph { toList(data: HonoApiClient.List_Get["list"] | HonoApiClient.List_List_Get): ListModel { return { id: data.id, @@ -110,71 +28,6 @@ class Morph { } } - toEntryList(data?: HonoApiClient.Entry_Post | HonoApiClient.Entry_Inbox_Post): EntryModel[] { - const entries: EntryModel[] = [] - for (const item of data ?? []) { - entries.push({ - id: item.entries.id, - title: item.entries.title, - url: item.entries.url, - content: null, - readabilityContent: null, - description: item.entries.description, - guid: item.entries.guid, - author: item.entries.author, - authorUrl: item.entries.authorUrl, - authorAvatar: item.entries.authorAvatar, - insertedAt: new Date(item.entries.insertedAt), - publishedAt: new Date(item.entries.publishedAt), - media: item.entries.media ?? null, - categories: item.entries.categories ?? null, - attachments: item.entries.attachments ?? null, - extra: item.entries.extra - ? { - links: item.entries.extra.links ?? undefined, - } - : null, - language: item.entries.language, - feedId: item.feeds.id, - inboxHandle: item.feeds.type === "inbox" ? item.feeds.id : null, - read: item.read, - sources: "from" in item ? (item.from ?? null) : null, - settings: item.settings ?? null, - }) - } - return entries - } - - toCollections( - data: HonoApiClient.Entry_Post | HonoApiClient.Entry_Inbox_Post | undefined, - view: FeedViewType, - ): { - collections: CollectionModel[] - entryIdsNotInCollections: string[] - } { - if (!data) return { collections: [], entryIdsNotInCollections: [] } - - const collections: CollectionModel[] = [] - const entryIdsNotInCollections: string[] = [] - for (const item of data) { - if (!item.collections) { - entryIdsNotInCollections.push(item.entries.id) - continue - } - collections.push({ - createdAt: item.collections.createdAt, - entryId: item.entries.id, - feedId: item.feeds.id, - view, - }) - } - - return { - collections, - entryIdsNotInCollections, - } - } - toEntry(data?: HonoApiClient.Entry_Get | HonoApiClient.Entry_Inbox_Get): EntryModel | null { if (!data) return null @@ -240,5 +93,7 @@ class Morph { } } } - -export const honoMorph = new Morph() +/** + * @deprecated + */ +export const honoMorph = new LegacyHonoMorph() diff --git a/packages/internal/store/src/subscription/store.ts b/packages/internal/store/src/subscription/store.ts index 10195b78d..dc41632b6 100644 --- a/packages/internal/store/src/subscription/store.ts +++ b/packages/internal/store/src/subscription/store.ts @@ -3,7 +3,7 @@ import { SubscriptionService } from "@follow/database/services/subscription" import { tracker } from "@follow/tracker" import { omit } from "es-toolkit" -import { apiClient } from "../context" +import { api } from "../context" import { invalidateEntriesQuery } from "../entry/hooks" import { getFeedById } from "../feed/getter" import { feedActions } from "../feed/store" @@ -12,8 +12,8 @@ import type { Hydratable, Resetable } from "../internal/base" import { createImmerSetter, createTransaction, createZustandStore } from "../internal/helper" import { getListById } from "../list/getters" import { listActions } from "../list/store" +import { apiMorph } from "../morph/api" import { dbStoreMorph } from "../morph/db-store" -import { honoMorph } from "../morph/hono" import { buildSubscriptionDbId, storeDbMorph } from "../morph/store-db" import { whoami } from "../user/getters" import { getCategoryFeedIds } from "./getter" @@ -182,30 +182,23 @@ class SubscriptionActions implements Hydratable, Resetable { class SubscriptionSyncService { async fetch(view?: FeedViewType) { - const res = await apiClient().subscriptions.$get({ - query: { - view: view !== undefined ? String(view) : undefined, - }, + const { data } = await api().subscriptions.get({ + view: view !== undefined ? view : undefined, }) - const { subscriptions, feeds, lists, inboxes } = honoMorph.toSubscription(res.data) + const { subscriptions, collections } = apiMorph.toSubscription(data) - await SubscriptionService.deleteNotExists( - subscriptions.map((s) => buildSubscriptionDbId(s)), - view, - ) - - feedActions.upsertMany(feeds) + feedActions.upsertMany(collections.feeds) subscriptionActions.upsertMany(subscriptions, { resetBeforeUpsert: typeof view === "number" ? view : true, }) - listActions.upsertMany(lists) + listActions.upsertMany(collections.lists) - inboxActions.upsertMany(inboxes) + inboxActions.upsertMany(collections.inboxes) return { subscriptions, - feeds, + feeds: collections.feeds, } } @@ -251,15 +244,13 @@ class SubscriptionSyncService { }) }) tx.request(async () => { - await apiClient().subscriptions.$patch({ - json: { - view: subscription.view, - feedId: subscription.feedId ?? undefined, - isPrivate: subscription.isPrivate ?? undefined, - listId: subscription.listId ?? undefined, - category: subscription.category ?? undefined, - title: subscription.title ?? undefined, - }, + await api().subscriptions.update({ + view: subscription.view, + feedId: subscription.feedId ?? undefined, + isPrivate: subscription.isPrivate ?? undefined, + listId: subscription.listId ?? undefined, + category: subscription.category ?? undefined, + title: subscription.title ?? undefined, }) }) @@ -271,15 +262,14 @@ class SubscriptionSyncService { } async subscribe(subscription: SubscriptionForm) { - const data = await apiClient().subscriptions.$post({ - json: { - url: subscription.url, - view: subscription.view, - category: subscription.category, - isPrivate: subscription.isPrivate, - title: subscription.title, - listId: subscription.listId, - }, + const { data } = await api().subscriptions.create({ + url: subscription.url, + + view: subscription.view, + category: subscription.category, + isPrivate: subscription.isPrivate, + title: subscription.title, + listId: subscription.listId, }) if (data.feed) { @@ -346,11 +336,9 @@ class SubscriptionSyncService { tx.request(async () => { const feedIdList = feedSubscriptions.map((s) => s.feedId).filter((i) => typeof i === "string") - await apiClient().subscriptions.$delete({ - json: { - feedIdList: feedIdList.length > 0 ? feedIdList : undefined, - listId: listSubscriptions.at(0)?.listId || undefined, - }, + await api().subscriptions.delete({ + feedIdList: feedIdList.length > 0 ? feedIdList : undefined, + listId: listSubscriptions.at(0)?.listId || undefined, }) }) @@ -426,12 +414,10 @@ class SubscriptionSyncService { }) tx.request(async () => { - await apiClient().subscriptions.batch.$patch({ - json: { - feedIds, - category: newCategory, - view: newView, - }, + await api().subscriptions.batchUpdate({ + feedIds, + category: newCategory, + view: newView, }) }) @@ -494,11 +480,9 @@ class SubscriptionSyncService { }) tx.request(async () => { - await apiClient().subscriptions.$patch({ - json: { - view, - listId, - }, + await api().subscriptions.update({ + view, + listId, }) }) @@ -542,11 +526,9 @@ class SubscriptionSyncService { }) tx.request(async () => { - await apiClient().categories.$delete({ - json: { - feedIdList: feedIds, - deleteSubscriptions: false, - }, + await api().categories.delete({ + feedIdList: feedIds, + deleteSubscriptions: false, }) }) @@ -626,11 +608,9 @@ class SubscriptionSyncService { }) tx.request(async () => { - await apiClient().categories.$patch({ - json: { - feedIdList: feedIds, - category: newCategory, - }, + await api().categories.update({ + feedIdList: feedIds, + category: newCategory, }) }) diff --git a/packages/internal/store/src/types.ts b/packages/internal/store/src/types.ts index f4423eeb3..62b901031 100644 --- a/packages/internal/store/src/types.ts +++ b/packages/internal/store/src/types.ts @@ -1,4 +1,5 @@ import type { AppType } from "@follow/shared/hono" +import type { ModuleAPIs } from "@follow-app/client-sdk" import type { hc } from "hono/client" export type APIClient = ReturnType> @@ -11,3 +12,5 @@ export type GeneralMutationOptions = { export type GeneralQueryOptions = { enabled?: boolean } + +export type FollowAPI = ModuleAPIs diff --git a/packages/vite-plugin-route-builder/README.test.md b/packages/vite-plugin-route-builder/README.test.md deleted file mode 100644 index 06cbadc78..000000000 --- a/packages/vite-plugin-route-builder/README.test.md +++ /dev/null @@ -1,92 +0,0 @@ -# Vite Plugin Route Builder - Tests - -This document describes the test suite for the `@follow/vite-plugin-route-builder` package. - -## Test Structure - -### Core Route Building (`src/__tests__/route-builder.test.ts`) - -Tests the core route building logic from `utils/route-builder.ts`: - -- **Simple file structure**: Basic index and named pages -- **Grouped routes**: Routes with `(group)` syntax -- **Nested routes**: Multi-level directory structures -- **Dynamic routes**: `[param]` and `[...spread]` syntax -- **Sync loading**: `.sync.tsx` files that should be loaded synchronously -- **File prioritization**: Sync files take precedence over async files - -### Vite Plugin Integration (`src/__tests__/vite-plugin.test.ts`) - -Tests the Vite plugin interface and lifecycle: - -- **Plugin creation**: Default and custom options -- **File discovery**: Finding page files with glob patterns -- **Build lifecycle**: `configResolved` and `buildStart` hooks -- **Dev mode**: File watching and hot reload -- **Code generation**: Output file creation and logging - -### Code Generation (`src/__tests__/code-generation.test.ts`) - -Tests the route code generation logic: - -- **Import statements**: Sync (`import * as`) vs async (`const lazy = () => import()`) -- **Route transformation**: Converting internal structures to React Router format -- **String processing**: Template replacement and cleanup -- **File content**: Complete generated file structure - -### Route Builder Utils (`src/utils/route-builder.test.ts`) - -Tests the utility functions: - -- **Basic routing**: Simple file-to-route mapping -- **Sync flag**: Ensuring `.sync.tsx` files get marked correctly - -## Test Features - -### Mocking - -- File system operations (`writeFileSync`) -- Fast-glob for file discovery -- Vite plugin hooks and configuration - -### Sync vs Async Testing - -Key feature of the plugin is distinguishing between: - -- **Async files** (`.tsx`): Lazy loaded with `() => import()` -- **Sync files** (`.sync.tsx`): Synchronously imported with `import * as` - -### Code Generation Validation - -Tests ensure generated code: - -- Has correct import syntax for both sync and async -- Removes undefined loader properties -- Formats route objects properly -- Includes necessary TypeScript annotations - -## Running Tests - -```bash -# Run all tests -npm run test - -# Run tests in watch mode -npm run test:run - -# Run specific test file -npm run test:run src/__tests__/route-builder.test.ts -``` - -## Test Coverage - -The test suite covers: - -- ✅ Core route building logic -- ✅ Plugin lifecycle and integration -- ✅ Code generation and templating -- ✅ Sync vs async file handling -- ✅ Error scenarios and edge cases -- ✅ File watching and dev mode - -All tests use Vitest with Node.js environment for proper file system mocking. diff --git a/packages/vite-plugin-route-builder/example.config.ts b/packages/vite-plugin-route-builder/example.config.ts deleted file mode 100644 index 3d8e8e462..000000000 --- a/packages/vite-plugin-route-builder/example.config.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { defineConfig } from "vite" - -import { routeBuilderPluginV2 } from "./src/vite-plugin-route-builder" - -export default defineConfig({ - plugins: [ - routeBuilderPluginV2({ - // Glob pattern for page files - pagePattern: "./src/pages/**/*.{tsx,sync.tsx}", - - // Output path for generated routes - outputPath: "./src/generated-routes.ts", - - // Enable in dev mode for hot reload - enableInDev: true, - - // Debug logging - debug: true, - - // Custom segment group ordering: main routes first, then admin, then external - // Both formats supported: with or without parentheses - segmentGroupOrder: ["(main)", "(admin)", "(external)"], - - // Optional: transform file paths - transformPath: (path: string) => { - // Example: rename admin pages - return path.replace("/admin/", "/dashboard/") - }, - }), - ], -}) - -/* -File structure example: - -src/pages/ -├── index.tsx → / (lazy loaded) -├── about.tsx → /about (lazy loaded) -├── critical.sync.tsx → /critical (sync loaded) -├── settings/ -│ ├── layout.tsx → /settings (lazy loaded) -│ ├── index.tsx → /settings/ (lazy loaded) -│ └── profile.sync.tsx → /settings/profile (sync loaded) -├── (main)/ → Route group (sorted first due to segmentGroupOrder) -│ ├── layout.tsx → / (lazy loaded) -│ └── dashboard.tsx → /dashboard (lazy loaded) -├── (admin)/ → Route group (sorted second due to segmentGroupOrder) -│ ├── layout.tsx → / (lazy loaded) -│ └── users.tsx → /users (lazy loaded) -└── (external)/ → Route group (sorted third due to segmentGroupOrder) - ├── layout.tsx → / (lazy loaded) - └── api-docs.tsx → /api-docs (lazy loaded) - -Generated output (route groups ordered by segmentGroupOrder): - -import * as SyncComponent0 from "./pages/critical.sync" -import * as SyncComponent1 from "./pages/settings/profile.sync" -const lazy0 = () => import("./pages/index") -const lazy1 = () => import("./pages/about") -const lazy2 = () => import("./pages/(main)/layout") -const lazy3 = () => import("./pages/(admin)/layout") -const lazy4 = () => import("./pages/(external)/layout") -// ... more imports - -export const routes: RouteObject[] = [ - // Non-grouped routes first - { - path: "", - lazy: lazy0, - }, - { - path: "critical", - Component: SyncComponent0.Component, - loader: SyncComponent0.loader, - }, - // Route groups ordered by segmentGroupOrder: main, admin, external - { - path: "", - lazy: lazy2, // (main) group layout - children: [] // main routes - }, - { - path: "", - lazy: lazy3, // (admin) group layout - children: [] // admin routes - }, - { - path: "", - lazy: lazy4, // (external) group layout - children: [] // external routes - }, - // ... more routes -] -*/ diff --git a/packages/vite-plugin-route-builder/package.json b/packages/vite-plugin-route-builder/package.json deleted file mode 100644 index 0333775b1..000000000 --- a/packages/vite-plugin-route-builder/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "@follow-app/vite-plugin-route-builder", - "type": "module", - "version": "0.1.1", - "exports": { - ".": "./src/index.ts" - }, - "main": "src/index.ts", - "types": "src/index.ts", - "files": [ - "src" - ], - "scripts": { - "build": "tsdown", - "test": "vitest", - "test:run": "vitest run" - }, - "devDependencies": { - "@types/node": "24.0.10", - "es-toolkit": "1.39.6", - "fast-glob": "3.3.3", - "happy-dom": "18.0.1", - "react-router": "7.6.3", - "tsdown": "0.12.9", - "typescript": "catalog:", - "vite": "7.0.2", - "vite-tsconfig-paths": "5.1.4", - "vitest": "3.2.4" - }, - "publishConfig": { - "access": "public", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - } - } -} diff --git a/packages/vite-plugin-route-builder/readme.md b/packages/vite-plugin-route-builder/readme.md deleted file mode 100644 index 23a35708e..000000000 --- a/packages/vite-plugin-route-builder/readme.md +++ /dev/null @@ -1,472 +0,0 @@ -# Vite Route Builder Plugin - Technical Documentation - -## Overview - -The Vite Route Builder plugin is a build-time code generation tool that creates React Router route configurations from a file system-based routing structure, similar to Next.js App Router. It transforms a `pages/` directory structure into optimized route objects with lazy loading and proper component imports. - -## Core Architecture - -### Plugin Flow - -1. **Build-time Generation**: The plugin runs during Vite's build process to generate static route configurations -2. **File System Scanning**: Uses glob patterns to discover page and layout files -3. **Route Tree Building**: Leverages the existing `route-builder.ts` logic to create route hierarchy -4. **Code Generation**: Produces a TypeScript file with lazy-loaded route objects - -### Key Components - -- **`vite-plugin-route-builder.ts`**: Main Vite plugin implementation -- **`route-builder.ts`**: Core routing logic for transforming file paths to route objects -- **`generated-routes.ts`**: Auto-generated output file containing route configurations - -## File System Routing Conventions - -### Directory Structure - -``` -pages/ -├── (main)/ # Route group (doesn't affect URL path) -│ ├── layout.tsx # Layout component for grouped routes -│ ├── index.tsx # / route -│ └── discover.tsx # /discover route -├── settings/ -│ ├── layout.tsx # Layout for /settings/* -│ ├── index.tsx # /settings route -│ └── profile.tsx # /settings/profile route -├── (settings)/ # Another route group -│ ├── layout.tsx # Layout component -│ └── general.tsx # /general route -└── [...404].tsx # Catch-all route -``` - -### Route Mapping Rules - -1. **Index Routes**: `index.tsx` files create routes for their parent directory path -2. **Named Routes**: File names become route paths (e.g., `profile.tsx` → `/profile`) -3. **Grouped Routes**: Directories with parentheses `(name)` group routes without affecting URL paths -4. **Dynamic Routes**: `[param].tsx` creates dynamic parameter routes -5. **Catch-all Routes**: `[...name].tsx` creates catch-all routes -6. **Layouts**: `layout.tsx` files provide wrapper components for child routes -7. **Sync Loading**: Files with `.sync.tsx` extension are loaded synchronously instead of lazy loading - -### Synchronous vs Asynchronous Loading - -The plugin supports two loading strategies: - -- **Asynchronous Loading (default)**: Files with `.tsx` extension are lazy-loaded for optimal performance -- **Synchronous Loading**: Files with `.sync.tsx` extension are imported directly and loaded synchronously - -``` -pages/ -├── home.tsx # Lazy loaded: const lazy1 = () => import("./pages/home") -├── critical.sync.tsx # Sync loaded: import SyncComponent1 from "./pages/critical" -└── settings/ - ├── layout.sync.tsx # Sync loaded layout - └── profile.tsx # Lazy loaded page -``` - -#### When to Use Sync Loading - -Use `.sync.tsx` extension for: - -- Critical above-the-fold components -- Small, lightweight components that don't benefit from code splitting -- Components that need to be immediately available (no loading state) -- Layout components that are always needed - -## Implementation Details - -### 1. Route Discovery Process - -```typescript -// Plugin discovers files using glob patterns -const pageFiles = glob.sync("./src/pages/**/*.{ts,tsx}", { - ignore: ["**/*.d.ts", "**/*.test.*", "**/*.spec.*"], -}) -``` - -### 2. Route Tree Generation - -The plugin uses the existing `route-builder.ts` logic: - -```typescript -import { buildRoute } from "../route-builder" - -// Transform file paths to route objects -const routes = buildRoute(pageFiles) -``` - -### 3. Path Resolution Strategy - -The plugin implements sophisticated path matching to connect route objects with their corresponding files: - -```typescript -function findFileForRoute(route: RouteObject, pageFiles: string[]): string | null { - const routePath = route.handle?.fs as string - if (!routePath) return null - - // Strategy 1: Direct file match (fs.tsx) - let targetFile = `./src/pages/${routePath}.tsx` - if (pageFiles.includes(targetFile)) return targetFile - - // Strategy 2: Layout file for grouped routes (fs/layout.tsx) - targetFile = `./src/pages/${routePath}/layout.tsx` - if (pageFiles.includes(targetFile)) return targetFile - - // Strategy 3: Index file (fs/index.tsx) - targetFile = `./src/pages/${routePath}/index.tsx` - if (pageFiles.includes(targetFile)) return targetFile - - // Strategy 4: Handle paths ending with '/' (index pages) - if (routePath.endsWith("/")) { - const cleanPath = routePath.slice(0, -1) - targetFile = `./src/pages/${cleanPath}/index.tsx` - if (pageFiles.includes(targetFile)) return targetFile - } - - return null -} -``` - -### 4. Loading Strategy Implementation - -The plugin generates different import strategies based on file extensions: - -#### Asynchronous Loading (.tsx files) - -```typescript -// Generate lazy imports for .tsx files -const lazyComponents = new Map() -let lazyCounter = 1 - -function collectLazyFunctions(route: RouteObject, pageFiles: string[]) { - const file = findFileForRoute(route, pageFiles) - if (file && !file.endsWith(".sync.tsx") && !lazyComponents.has(file)) { - const relativePath = path.relative( - path.dirname("./src/generated-routes.ts"), - file.replace("./src/", "./src/"), - ) - const varName = `LazyComponent${lazyCounter++}` - lazyComponents.set(file, varName) - - return `const ${varName} = () => import("${relativePath}")` - } -} -``` - -#### Synchronous Loading (.sync.tsx files) - -```typescript -// Generate direct imports for .sync.tsx files -const syncComponents = new Map() -let syncCounter = 1 - -function collectSyncImports(route: RouteObject, pageFiles: string[]) { - const file = findFileForRoute(route, pageFiles) - if (file && file.endsWith(".sync.tsx") && !syncComponents.has(file)) { - const relativePath = path.relative( - path.dirname("./src/generated-routes.ts"), - file.replace("./src/", "./src/"), - ) - const varName = `SyncComponent${syncCounter++}` - syncComponents.set(file, varName) - - return `import ${varName} from "${relativePath}"` - } -} -``` - -#### Route Object Assignment - -Routes use different properties based on loading strategy: - -```typescript -function assignComponentToRoute(route: RouteObject, file: string) { - if (file.endsWith(".sync.tsx")) { - // Sync components use Component property - route.Component = syncComponents.get(file) - } else { - // Async components use lazy property - route.lazy = lazyComponents.get(file) - } -} -``` - -### 5. Code Generation - -The final step generates a complete TypeScript file: - -```typescript -const output = ` -// Do not edit manually -/* eslint-disable */ -// @ts-nocheck - -import type { RouteObject } from "react-router" -import { lazy } from "react" - -${lazyImports.join("\n")} - -export const routes: RouteObject[] = ${serializedRoutes} -` -``` - -## Route Object Structure - -### Generated Route Format - -```typescript -interface RouteObject { - path?: string - index?: boolean - children?: RouteObject[] - element?: React.ComponentType - // Internal properties removed in final output - handle?: { fs: string } // Removed during serialization -} -``` - -### Example Generated Output - -```typescript -// Synchronous imports (loaded immediately) -import SyncComponent1 from "./pages/(main)/layout" -import SyncComponent2 from "./pages/critical-page" - -// Asynchronous imports (lazy loaded) -const LazyComponent1 = () => import("./pages/(main)/index") -const LazyComponent2 = () => import("./pages/settings/layout") -const LazyComponent3 = () => import("./pages/settings/profile") - -export const routes: RouteObject[] = [ - { - path: "/", - Component: SyncComponent1, // Sync loaded layout - children: [ - { - index: true, - lazy: LazyComponent1, // Lazy loaded page - }, - { - path: "critical", - Component: SyncComponent2, // Sync loaded critical page - }, - ], - }, - { - path: "/settings", - lazy: LazyComponent2, // Lazy loaded layout - children: [ - { - path: "profile", - lazy: LazyComponent3, // Lazy loaded page - }, - ], - }, -] -``` - -## Problem Solving Approach - -### Common Issues and Solutions - -1. **Path Mismatch**: Routes generated without corresponding lazy functions - - **Solution**: Enhanced file matching with multiple strategies and path normalization - -2. **Incorrect Import Paths**: Absolute paths causing import failures - - **Solution**: Proper relative path calculation using `path.relative()` - -3. **Unused Lazy Variables**: Too many lazy imports for non-existent files - - **Solution**: Only generate lazy imports for routes with actual file matches - -4. **Index Route Handling**: Paths ending with `/` causing mapping issues - - **Solution**: Special handling for index pages and path cleaning - -### Debugging Strategy - -The plugin includes comprehensive logging for troubleshooting: - -```typescript -console.log("📁 Page files found:", pageFiles.length) -console.log("🎯 Routes with lazy functions:", routesWithLazy) -console.log("📝 Generated lazy imports:", lazyComponents.size) -``` - -## Performance Considerations - -### Build-time Optimization - -- **Static Generation**: All routing logic runs at build time, not runtime -- **Lazy Loading**: Components are loaded on-demand, reducing initial bundle size -- **Tree Shaking**: Unused routes and components are eliminated during bundling - -### Memory Efficiency - -- **File Caching**: Plugin processes files once and caches results -- **Selective Import**: Only imports files that are actually used in routes - -## Configuration Options - -The plugin accepts the following configuration options: - -### RouteBuilderPluginOptions - -```typescript -interface RouteBuilderPluginOptions { - /** Page files glob pattern */ - pagePattern?: string - /** Output path for generated routes */ - outputPath?: string - /** Whether to enable in dev mode */ - enableInDev?: boolean - /** Custom file to route path transformation logic */ - transformPath?: (path: string) => string - /** Whether to disable logging */ - debug?: boolean - /** Custom order for segment groups in route tree. Array of group names (without parentheses). Default: filesystem order */ - segmentGroupOrder?: string[] -} -``` - -### Option Descriptions - -- **`pagePattern`** (default: `"./pages/**/*.{tsx,sync.tsx}"`): Glob pattern for discovering page files -- **`outputPath`** (default: `"./src/generated-routes.ts"`): Output path for the generated route configuration -- **`enableInDev`** (default: `true`): Whether to enable route generation in development mode -- **`transformPath`**: Custom function to transform file paths before route generation -- **`debug`** (default: `false`): Enable detailed logging for troubleshooting -- **`segmentGroupOrder`** (default: `[]`): Custom ordering for route segment groups - -### Segment Group Ordering - -By default, route groups (directories with parentheses like `(main)`, `(external)`) are ordered alphabetically based on the filesystem. The `segmentGroupOrder` option allows you to specify a custom order: - -```typescript -// Example: Place (main) routes before (external) routes -routeBuilder({ - segmentGroupOrder: ["main", "external"], // Without parentheses -}) - -// Or with parentheses (both formats supported) -routeBuilder({ - segmentGroupOrder: ["(main)", "(external)"], // With parentheses -}) -``` - -#### Default Behavior (Filesystem Order) - -``` -pages/ -├── (admin)/ # Third -├── (external)/ # First (alphabetically) -├── (main)/ # Second -└── (settings)/ # Fourth -``` - -#### With Custom Order - -```typescript -// Configuration -segmentGroupOrder: ['main', 'admin'] - -// Result: -pages/ -├── (main)/ # First (specified in order) -├── (admin)/ # Second (specified in order) -├── (external)/ # Third (not specified, filesystem order) -└── (settings)/ # Fourth (not specified, filesystem order) -``` - -#### Usage Examples - -```typescript -// Basic usage with default options -import routeBuilder from "./vite-plugin-route-builder" - -export default defineConfig({ - plugins: [routeBuilder()], -}) - -// Advanced configuration with parentheses format -export default defineConfig({ - plugins: [ - routeBuilder({ - pagePattern: "./src/pages/**/*.{tsx,sync.tsx}", - outputPath: "./src/router/generated-routes.ts", - enableInDev: true, - debug: true, - segmentGroupOrder: ["(main)", "(dashboard)", "(settings)", "(external)"], - transformPath: (path) => path.replace(/\.sync\.tsx$/, ".tsx"), - }), - ], -}) - -// Mixed format is also supported -export default defineConfig({ - plugins: [ - routeBuilder({ - segmentGroupOrder: ["(main)", "dashboard", "settings", "(external)"], - }), - ], -}) -``` - -## Integration Points - -### Vite Integration - -```typescript -// vite.config.ts -import { defineConfig } from "vite" -import routeBuilder from "./plugins/vite/vite-plugin-route-builder" - -export default defineConfig({ - plugins: [ - routeBuilder(), // Add the route builder plugin - // ... other plugins - ], -}) -``` - -### React Router Integration - -```typescript -// App.tsx -import { createBrowserRouter, RouterProvider } from 'react-router-dom' -import { routes } from './generated-routes' - -const router = createBrowserRouter(routes) - -export default function App() { - return -} -``` - -## Future Enhancements - -### Potential Improvements - -1. **Watch Mode**: Real-time route regeneration during development -2. **TypeScript Validation**: Compile-time route validation -3. **Route Metadata**: Support for route-level metadata and guards -4. **Custom Conventions**: Configurable file naming conventions -5. **Nested Layouts**: Support for multiple layout levels - -### Extensibility - -The plugin architecture allows for easy extension: - -- Custom file processors for different route types -- Pluggable path resolution strategies -- Configurable code generation templates -- Integration with other meta-frameworks - -## Conclusion - -The Vite Route Builder plugin successfully transforms file system-based routing into optimized React Router configurations. By leveraging build-time generation, it provides excellent performance while maintaining developer ergonomics similar to Next.js App Router. The robust path matching and lazy loading implementation ensures reliable route generation for complex application structures. - -## License - -2025 © Innei, Released under the MIT License. - -> [Personal Website](https://innei.in/) · GitHub [@Innei](https://github.com/innei/) diff --git a/packages/vite-plugin-route-builder/src/__tests__/__snapshots__/vite-plugin.test.ts.snap b/packages/vite-plugin-route-builder/src/__tests__/__snapshots__/vite-plugin.test.ts.snap deleted file mode 100644 index 82b58326c..000000000 --- a/packages/vite-plugin-route-builder/src/__tests__/__snapshots__/vite-plugin.test.ts.snap +++ /dev/null @@ -1,230 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`routeBuilderPluginV2 > Generated Routes Snapshots > should generate correct routes for mixed sync and async files > mixed-routes-structure 1`] = ` -"// This file is auto-generated by vite-plugin-route-builder -// Do not edit manually -/* eslint-disable */ -// @ts-nocheck - -import type { RouteObject } from "react-router" - -// Imports for page components -import * as SyncComponent0 from ".//project/src/pages/about.sync" -import * as SyncComponent1 from ".//project/src/pages/dashboard/admin.sync" -import * as SyncComponent2 from ".//project/src/pages/settings/profile.sync" -const lazy0 = () => import(".//project/src/pages/blog/[id]") -const lazy1 = () => import(".//project/src/pages/settings/layout") -const lazy2 = () => import(".//project/src/pages/index") - -// Generated route configuration -export const routes: RouteObject[] = [ - { - "path": "about", - "Component": SyncComponent0.Component, - "loader": SyncComponent0.loader - }, - { - "path": "blog", - "children": [ - { - "path": ":id", - "lazy": lazy0 - } - ] - }, - { - "path": "dashboard", - "children": [ - { - "path": "admin", - "Component": SyncComponent1.Component, - "loader": SyncComponent1.loader - } - ] - }, - { - "path": "settings", - "children": [ - { - "path": "", - "lazy": lazy1, - "children": [ - { - "path": "profile", - "Component": SyncComponent2.Component, - "loader": SyncComponent2.loader - } - ] - } - ] - }, - { - "path": "", - "lazy": lazy2 - } -] - -export default routes -" -`; - -exports[`routeBuilderPluginV2 > Generated Routes Snapshots > should generate correct routes for only async files > async-only-routes 1`] = ` -"// This file is auto-generated by vite-plugin-route-builder -// Do not edit manually -/* eslint-disable */ -// @ts-nocheck - -import type { RouteObject } from "react-router" - -// Imports for page components -const lazy0 = () => import(".//project/src/pages/about") -const lazy1 = () => import(".//project/src/pages/blog/[slug]") -const lazy2 = () => import(".//project/src/pages/contact") -const lazy3 = () => import(".//project/src/pages/index") - -// Generated route configuration -export const routes: RouteObject[] = [ - { - "path": "about", - "lazy": lazy0 - }, - { - "path": "blog", - "children": [ - { - "path": ":slug", - "lazy": lazy1 - } - ] - }, - { - "path": "contact", - "lazy": lazy2 - }, - { - "path": "", - "lazy": lazy3 - } -] - -export default routes -" -`; - -exports[`routeBuilderPluginV2 > Generated Routes Snapshots > should generate correct routes for only sync files > sync-only-routes 1`] = ` -"// This file is auto-generated by vite-plugin-route-builder -// Do not edit manually -/* eslint-disable */ -// @ts-nocheck - -import type { RouteObject } from "react-router" - -// Imports for page components -import * as SyncComponent0 from ".//project/src/pages/critical.sync" -import * as SyncComponent1 from ".//project/src/pages/important.sync" -import * as SyncComponent2 from ".//project/src/pages/settings/config.sync" - -// Generated route configuration -export const routes: RouteObject[] = [ - { - "path": "critical", - "Component": SyncComponent0.Component, - "loader": SyncComponent0.loader - }, - { - "path": "important", - "Component": SyncComponent1.Component, - "loader": SyncComponent1.loader - }, - { - "path": "settings", - "children": [ - { - "path": "config", - "Component": SyncComponent2.Component, - "loader": SyncComponent2.loader - } - ] - } -] - -export default routes -" -`; - -exports[`routeBuilderPluginV2 > Generated Routes Snapshots > should generate correct routes with custom transformPath > custom-transform-routes 1`] = ` -"// This file is auto-generated by vite-plugin-route-builder -// Do not edit manually -/* eslint-disable */ -// @ts-nocheck - -import type { RouteObject } from "react-router" - -// Imports for page components - - -// Generated route configuration -export const routes: RouteObject[] = [ - { - "path": "", - "children": [ - { - "path": "demo" - }, - { - "path": "test" - } - ] - } -] - -export default routes -" -`; - -exports[`routeBuilderPluginV2 > Generated Routes Snapshots > should generate empty routes array when no files found > empty-routes 1`] = ` -"// This file is auto-generated by vite-plugin-route-builder -// Do not edit manually -/* eslint-disable */ -// @ts-nocheck - -import type { RouteObject } from "react-router" - -// Imports for page components - - -// Generated route configuration -export const routes: RouteObject[] = [] - -export default routes -" -`; - -exports[`routeBuilderPluginV2 > should generate correct import statements for sync files > sync-and-async-routes 1`] = ` -"// This file is auto-generated by vite-plugin-route-builder -// Do not edit manually -/* eslint-disable */ -// @ts-nocheck - -import type { RouteObject } from "react-router" - -// Imports for page components -import * as SyncComponent0 from ".//project/src/pages/critical.sync" -const lazy0 = () => import(".//project/src/pages/normal") - -// Generated route configuration -export const routes: RouteObject[] = [ - { - "path": "critical", - "Component": SyncComponent0.Component, - "loader": SyncComponent0.loader - }, - { - "path": "normal", - "lazy": lazy0 - } -] - -export default routes -" -`; diff --git a/packages/vite-plugin-route-builder/src/__tests__/code-generation.test.ts b/packages/vite-plugin-route-builder/src/__tests__/code-generation.test.ts deleted file mode 100644 index b2e6c28fa..000000000 --- a/packages/vite-plugin-route-builder/src/__tests__/code-generation.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { describe, expect, it } from "vitest" - -// We'll test the internal logic by creating a simplified version -// since the actual generateRouteFileContent is internal to the plugin -describe("Code Generation", () => { - it("should generate correct imports for sync files", () => { - const syncImports = new Set(["./pages/critical.sync.tsx"]) - const syncImportMap = new Map([["./pages/critical.sync.tsx", "SyncComponent0"]]) - const fileToImportMap: Record = { - "./pages/critical.sync.tsx": "./pages/critical.sync", - } - - const imports: string[] = [] - syncImports.forEach((key) => { - const importPath = fileToImportMap[key] - const syncImportName = syncImportMap.get(key) - if (importPath && syncImportName) { - imports.push(`import * as ${syncImportName} from "${importPath}"`) - } - }) - - expect(imports).toEqual(['import * as SyncComponent0 from "./pages/critical.sync"']) - }) - - it("should generate correct imports for async files", () => { - const lazyImports = new Set(["./pages/normal.tsx"]) - const lazyImportMap = new Map([["./pages/normal.tsx", "lazy0"]]) - const fileToImportMap: Record = { - "./pages/normal.tsx": "./pages/normal", - } - - const imports: string[] = [] - lazyImports.forEach((key) => { - const importPath = fileToImportMap[key] - const lazyFuncName = lazyImportMap.get(key) - if (importPath && lazyFuncName) { - imports.push(`const ${lazyFuncName} = () => import("${importPath}")`) - } - }) - - expect(imports).toEqual(['const lazy0 = () => import("./pages/normal")']) - }) - - it("should handle route object transformation", () => { - const route = { - path: "test", - Component: "__SYNC_SyncComponent0.Component__", - loader: "__SYNC_SyncComponent0.loader__", - } - - const routesString = JSON.stringify([route], null, 2) - .replaceAll(/"__SYNC_([^.]+)\.Component__"/g, '"$1.Component"') - .replaceAll(/"__SYNC_([^.]+)\.loader__"/g, '"$1.loader"') - - expect(routesString).toContain('"Component": "SyncComponent0.Component"') - expect(routesString).toContain('"loader": "SyncComponent0.loader"') - }) - - it("should remove undefined loader properties", () => { - const routeString = `{ - "path": "test", - "Component": "SyncComponent0.Component", - "loader": undefined -}` - - const cleaned = routeString.replaceAll(/,?\s*"loader":\s*undefined/g, "") - - expect(cleaned).toContain('"Component": "SyncComponent0.Component"') - expect(cleaned).not.toContain('"loader"') - }) - - it("should generate complete route file content", () => { - const syncImports = ['import * as SyncComponent0 from "./pages/critical.sync"'] - const lazyImports = ['const lazy0 = () => import("./pages/normal")'] - const routes = [ - { - path: "critical", - Component: "SyncComponent0.Component", - loader: "SyncComponent0.loader", - }, - { - path: "normal", - lazy: "lazy0", - }, - ] - - const content = `// This file is auto-generated by vite-plugin-route-builder -// Do not edit manually -/* eslint-disable */ -// @ts-nocheck - -import type { RouteObject } from "react-router" - -// Imports for page components -${[...syncImports, ...lazyImports].join("\n")} - -// Generated route configuration -export const routes: RouteObject[] = ${JSON.stringify(routes, null, 2)} - -export default routes -` - - expect(content).toContain("import * as SyncComponent0") - expect(content).toContain("const lazy0 = () => import") - expect(content).toContain('"Component": "SyncComponent0.Component"') - expect(content).toContain('"loader": "SyncComponent0.loader"') - expect(content).toContain('"lazy": "lazy0"') - expect(content).toContain("export const routes: RouteObject[]") - }) - - it("should handle file extension replacement correctly", () => { - const testCases = [ - { - input: "./pages/test.sync.tsx", - expected: "./pages/test.sync", - }, - { - input: "./pages/test.tsx", - expected: "./pages/test", - }, - ] - - testCases.forEach(({ input, expected }) => { - let result: string - if (input.endsWith(".sync.tsx")) { - result = input.replace(/\.tsx$/, "") - } else { - result = input.replace(/\.tsx$/, "") - } - - expect(result).toBe(expected) - }) - }) -}) diff --git a/packages/vite-plugin-route-builder/src/__tests__/route-builder.test.ts b/packages/vite-plugin-route-builder/src/__tests__/route-builder.test.ts deleted file mode 100644 index 5f61c7026..000000000 --- a/packages/vite-plugin-route-builder/src/__tests__/route-builder.test.ts +++ /dev/null @@ -1,533 +0,0 @@ -import { describe, expect, test } from "vitest" - -import { buildGlobRoutes } from "../utils/route-builder" - -const fakePromise = () => Promise.resolve({ default: () => {} }) -describe("test route builder", () => { - test("match snapshot with default filesystem order", () => { - expect( - buildGlobRoutes({ - "./pages/(external)/layout.tsx": fakePromise, - "./pages/(external)/(with-layout)/index.tsx": fakePromise, - "./pages/(external)/(with-layout)/layout.tsx": fakePromise, - "./pages/(external)/(with-layout)/feed/[id]/index.tsx": fakePromise, - "./pages/(external)/(with-layout)/feed/[id]/layout.tsx": fakePromise, - - "./pages/(main)/layout.tsx": fakePromise, - "./pages/(main)/(context)/layout.tsx": fakePromise, - "./pages/(main)/(context)/discover/layout.tsx": fakePromise, - "./pages/(main)/(context)/discover/index.tsx": fakePromise, - - "./pages/preview.tsx": fakePromise, - "./pages/add/layout.tsx": fakePromise, - "./pages/add/index.tsx": fakePromise, - }), - ).toMatchInlineSnapshot(` - [ - { - "children": [ - { - "children": [ - { - "handle": { - "fs": "./pages/add/index/", - "fullPath": "/add/", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/add/layout", - "fullPath": "/add", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/add/add", - "fullPath": "/add", - }, - "path": "add", - }, - { - "handle": { - "fs": "./pages/preview/preview", - "fullPath": "/preview", - "isSync": false, - }, - "lazy": [Function], - "path": "preview", - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "handle": { - "fs": "./pages/(external)/(with-layout)/feed/[id]/index/", - "fullPath": "/feed/:id/", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/(external)/(with-layout)/feed/[id]/layout", - "fullPath": "/feed/:id", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/(external)/(with-layout)/feed/[id]/:id", - "fullPath": "/feed/:id", - }, - "path": ":id", - }, - ], - "handle": { - "fs": "./pages/(external)/(with-layout)/feed/feed", - "fullPath": "/feed", - }, - "path": "feed", - }, - { - "handle": { - "fs": "./pages/(external)/(with-layout)/index/", - "fullPath": "/", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/(external)/(with-layout)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/(external)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "handle": { - "fs": "./pages/(main)/(context)/discover/index/", - "fullPath": "/discover/", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/(main)/(context)/discover/layout", - "fullPath": "/discover", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/(main)/(context)/discover/discover", - "fullPath": "/discover", - }, - "path": "discover", - }, - ], - "handle": { - "fs": "./pages/(main)/(context)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/(main)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ] - `) - }) - - test("match snapshot with custom segment group order", () => { - expect( - buildGlobRoutes( - { - "./pages/(external)/layout.tsx": fakePromise, - "./pages/(external)/(with-layout)/index.tsx": fakePromise, - "./pages/(external)/(with-layout)/layout.tsx": fakePromise, - "./pages/(external)/(with-layout)/feed/[id]/index.tsx": fakePromise, - "./pages/(external)/(with-layout)/feed/[id]/layout.tsx": fakePromise, - - "./pages/(main)/layout.tsx": fakePromise, - "./pages/(main)/(context)/layout.tsx": fakePromise, - "./pages/(main)/(context)/discover/layout.tsx": fakePromise, - "./pages/(main)/(context)/discover/index.tsx": fakePromise, - - "./pages/preview.tsx": fakePromise, - "./pages/add/layout.tsx": fakePromise, - "./pages/add/index.tsx": fakePromise, - }, - { segmentGroupOrder: ["main", "external"] }, - ), - ).toMatchInlineSnapshot(` - [ - { - "children": [ - { - "children": [ - { - "handle": { - "fs": "./pages/add/index/", - "fullPath": "/add/", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/add/layout", - "fullPath": "/add", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/add/add", - "fullPath": "/add", - }, - "path": "add", - }, - { - "handle": { - "fs": "./pages/preview/preview", - "fullPath": "/preview", - "isSync": false, - }, - "lazy": [Function], - "path": "preview", - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "handle": { - "fs": "./pages/(main)/(context)/discover/index/", - "fullPath": "/discover/", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/(main)/(context)/discover/layout", - "fullPath": "/discover", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/(main)/(context)/discover/discover", - "fullPath": "/discover", - }, - "path": "discover", - }, - ], - "handle": { - "fs": "./pages/(main)/(context)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/(main)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "children": [ - { - "handle": { - "fs": "./pages/(external)/(with-layout)/feed/[id]/index/", - "fullPath": "/feed/:id/", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/(external)/(with-layout)/feed/[id]/layout", - "fullPath": "/feed/:id", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/(external)/(with-layout)/feed/[id]/:id", - "fullPath": "/feed/:id", - }, - "path": ":id", - }, - ], - "handle": { - "fs": "./pages/(external)/(with-layout)/feed/feed", - "fullPath": "/feed", - }, - "path": "feed", - }, - { - "handle": { - "fs": "./pages/(external)/(with-layout)/index/", - "fullPath": "/", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/(external)/(with-layout)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ], - "handle": { - "fs": "./pages/(external)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ] - `) - }) - - test("match snapshot with partial custom segment group order", () => { - expect( - buildGlobRoutes( - { - "./pages/(admin)/layout.tsx": fakePromise, - "./pages/(external)/layout.tsx": fakePromise, - "./pages/(main)/layout.tsx": fakePromise, - "./pages/(settings)/layout.tsx": fakePromise, - }, - { segmentGroupOrder: ["main"] }, - ), - ).toMatchInlineSnapshot(` - [ - { - "children": [], - "handle": { - "fs": "./pages/(main)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - { - "children": [], - "handle": { - "fs": "./pages/(admin)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - { - "children": [], - "handle": { - "fs": "./pages/(external)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - { - "children": [], - "handle": { - "fs": "./pages/(settings)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ] - `) - }) - - test("match snapshot with custom segment group order using parentheses format", () => { - expect( - buildGlobRoutes( - { - "./pages/(external)/layout.tsx": fakePromise, - "./pages/(main)/layout.tsx": fakePromise, - "./pages/(login)/layout.tsx": fakePromise, - }, - { segmentGroupOrder: ["(main)", "(login)"] }, - ), - ).toMatchInlineSnapshot(` - [ - { - "children": [], - "handle": { - "fs": "./pages/(main)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - { - "children": [], - "handle": { - "fs": "./pages/(login)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - { - "children": [], - "handle": { - "fs": "./pages/(external)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ] - `) - }) - - test("match snapshot with mixed format segment group order", () => { - expect( - buildGlobRoutes( - { - "./pages/(external)/layout.tsx": fakePromise, - "./pages/(main)/layout.tsx": fakePromise, - "./pages/(login)/layout.tsx": fakePromise, - "./pages/(settings)/layout.tsx": fakePromise, - }, - { segmentGroupOrder: ["(main)", "login", "external"] }, - ), - ).toMatchInlineSnapshot(` - [ - { - "children": [], - "handle": { - "fs": "./pages/(main)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - { - "children": [], - "handle": { - "fs": "./pages/(login)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - { - "children": [], - "handle": { - "fs": "./pages/(external)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - { - "children": [], - "handle": { - "fs": "./pages/(settings)", - "fullPath": "", - "isSync": false, - }, - "lazy": [Function], - "path": "", - }, - ] - `) - }) -}) diff --git a/packages/vite-plugin-route-builder/src/__tests__/vite-plugin.test.ts b/packages/vite-plugin-route-builder/src/__tests__/vite-plugin.test.ts deleted file mode 100644 index c745668c2..000000000 --- a/packages/vite-plugin-route-builder/src/__tests__/vite-plugin.test.ts +++ /dev/null @@ -1,455 +0,0 @@ -import * as fs from "node:fs" - -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" - -import { routeBuilderPluginV2 } from "../vite-plugin-route-builder" - -// Mock file system and glob -vi.mock("node:fs", () => ({ - writeFileSync: vi.fn(), - promises: { - access: vi.fn(), - readFile: vi.fn(), - writeFile: vi.fn(), - }, - resolve: vi.fn(), - dirname: vi.fn(), - relative: vi.fn(), -})) - -vi.mock("fast-glob", () => ({ - default: { - sync: vi.fn(), - }, -})) - -vi.mock("pathe", () => ({ - resolve: vi.fn((root: string, path: string) => `${root}/${path}`), - relative: vi.fn((from: string, to: string) => to.replace(from, "")), - dirname: vi.fn((path: string) => path.split("/").slice(0, -1).join("/")), -})) - -vi.mock("./utils/route-builder", () => ({ - buildGlobRoutes: vi.fn(() => [ - { - path: "test", - lazy: () => Promise.resolve({ default: () => null }), - handle: { - fs: "./pages/test", - fullPath: "/test", - isSync: false, - }, - }, - ]), -})) - -// Helper function to capture generated content -function captureGeneratedContent(): { content: string; filePath: string } | null { - const mockWriteFileSync = vi.mocked(fs.writeFileSync) - - if (!mockWriteFileSync.mock || mockWriteFileSync.mock.calls.length === 0) { - return null - } - - const lastCall = mockWriteFileSync.mock.calls.at(-1) - if (!lastCall) { - return null - } - - return { - filePath: lastCall[0] as string, - content: lastCall[1] as string, - } -} - -describe("routeBuilderPluginV2", () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - it("should create plugin with default options", () => { - const plugin = routeBuilderPluginV2() - - expect(plugin.name).toBe("vite-plugin-route-builder-v2") - expect(plugin).toHaveProperty("configResolved") - expect(plugin).toHaveProperty("buildStart") - expect(plugin).toHaveProperty("configureServer") - }) - - it("should use custom options", () => { - const customOptions = { - pagePattern: "./custom/**/*.tsx", - outputPath: "./custom/routes.ts", - enableInDev: false, - debug: true, - } - - const plugin = routeBuilderPluginV2(customOptions) - expect(plugin.name).toBe("vite-plugin-route-builder-v2") - }) - - it("should generate routes for sync and async files", async () => { - const mockFiles = [ - "/project/src/pages/index.tsx", - "/project/src/pages/about.sync.tsx", - "/project/src/pages/settings/layout.tsx", - "/project/src/pages/settings/profile.sync.tsx", - ] - - const glob = await import("fast-glob") - vi.mocked(glob.default.sync).mockReturnValue(mockFiles) - - const plugin = routeBuilderPluginV2({ - pagePattern: "./src/pages/**/*.{tsx,sync.tsx}", - outputPath: "./src/generated-routes.ts", - }) - - const mockConfig = { - command: "build" as const, - root: "/project", - logger: { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }, - } - - // Simulate plugin lifecycle - if (typeof plugin.configResolved === "function") { - plugin.configResolved(mockConfig as any) - } - if (typeof plugin.buildStart === "function") { - await plugin.buildStart.call({} as any, {} as any) - } - - expect(glob.default.sync).toHaveBeenCalledWith("./src/pages/**/*.{tsx,sync.tsx}", { - cwd: "/project", - absolute: true, - }) - }) - - it("should generate correct import statements for sync files", async () => { - const mockFiles = ["/project/src/pages/critical.sync.tsx", "/project/src/pages/normal.tsx"] - - const glob = await import("fast-glob") - vi.mocked(glob.default.sync).mockReturnValue(mockFiles) - - const plugin = routeBuilderPluginV2({ - outputPath: "./src/generated-routes.ts", - debug: true, - }) - - const mockConfig = { - command: "build" as const, - root: "/project", - logger: { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }, - } - - const { writeFileSync } = await import("node:fs") - const mockWriteFileSync = vi.mocked(writeFileSync) - - if (typeof plugin.configResolved === "function") { - plugin.configResolved(mockConfig as any) - } - if (typeof plugin.buildStart === "function") { - await plugin.buildStart.call({} as any, {} as any) - } - - expect(mockWriteFileSync).toHaveBeenCalled() - expect(mockConfig.logger.info).toHaveBeenCalledWith( - expect.stringContaining("Generated routes:"), - ) - - // Verify generated content with snapshot - const generatedContent = captureGeneratedContent() - expect(generatedContent).toBeTruthy() - expect(generatedContent!.content).toMatchSnapshot("sync-and-async-routes") - }) - - it("should handle transformPath option", async () => { - const mockFiles = ["/project/src/pages/test.tsx"] - - const glob = await import("fast-glob") - vi.mocked(glob.default.sync).mockReturnValue(mockFiles) - - const plugin = routeBuilderPluginV2({ - transformPath: (path) => path.replace("./pages/", "./custom/"), - debug: true, - }) - - const mockConfig = { - command: "build" as const, - root: "/project", - logger: { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }, - } - - if (typeof plugin.configResolved === "function") { - plugin.configResolved(mockConfig as any) - } - if (typeof plugin.buildStart === "function") { - await plugin.buildStart.call({} as any, {} as any) - } - - expect(mockConfig.logger.info).toHaveBeenCalledWith( - expect.stringContaining("./custom/test.tsx"), - ) - }) - - it("should watch files in dev mode", () => { - const plugin = routeBuilderPluginV2({ - enableInDev: true, - }) - - const mockWatcher = { - add: vi.fn(), - on: vi.fn(), - } - - const mockServer = { - watcher: mockWatcher, - ws: { - send: vi.fn(), - }, - } - - const mockConfig = { - command: "serve" as const, - root: "/project", - logger: { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }, - } - - if (typeof plugin.configResolved === "function") { - plugin.configResolved(mockConfig as any) - } - if (typeof plugin.configureServer === "function") { - plugin.configureServer(mockServer as any) - } - - expect(mockWatcher.add).toHaveBeenCalled() - expect(mockWatcher.on).toHaveBeenCalledWith("add", expect.any(Function)) - expect(mockWatcher.on).toHaveBeenCalledWith("unlink", expect.any(Function)) - }) - - it("should not generate in dev mode when disabled", async () => { - const plugin = routeBuilderPluginV2({ - enableInDev: false, - }) - - const mockConfig = { - command: "serve" as const, - root: "/project", - logger: { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }, - } - - const glob = await import("fast-glob") - - if (typeof plugin.configResolved === "function") { - plugin.configResolved(mockConfig as any) - } - if (typeof plugin.buildStart === "function") { - await plugin.buildStart.call({} as any, {} as any) - } - - expect(glob.default.sync).not.toHaveBeenCalled() - }) - - describe("Generated Routes Snapshots", () => { - it("should generate correct routes for mixed sync and async files", async () => { - const mockFiles = [ - "/project/src/pages/index.tsx", - "/project/src/pages/about.sync.tsx", - "/project/src/pages/settings/layout.tsx", - "/project/src/pages/settings/profile.sync.tsx", - "/project/src/pages/blog/[id].tsx", - "/project/src/pages/dashboard/admin.sync.tsx", - ] - - const glob = await import("fast-glob") - vi.mocked(glob.default.sync).mockReturnValue(mockFiles) - - const plugin = routeBuilderPluginV2({ - pagePattern: "./src/pages/**/*.{tsx,sync.tsx}", - outputPath: "./src/generated-routes.ts", - }) - - const mockConfig = { - command: "build" as const, - root: "/project", - logger: { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }, - } - - if (typeof plugin.configResolved === "function") { - plugin.configResolved(mockConfig as any) - } - if (typeof plugin.buildStart === "function") { - await plugin.buildStart.call({} as any, {} as any) - } - - const generatedContent = captureGeneratedContent() - expect(generatedContent).toBeTruthy() - expect(generatedContent!.content).toMatchSnapshot("mixed-routes-structure") - }) - - it("should generate correct routes for only sync files", async () => { - const mockFiles = [ - "/project/src/pages/critical.sync.tsx", - "/project/src/pages/important.sync.tsx", - "/project/src/pages/settings/config.sync.tsx", - ] - - const glob = await import("fast-glob") - vi.mocked(glob.default.sync).mockReturnValue(mockFiles) - - const plugin = routeBuilderPluginV2({ - outputPath: "./src/generated-routes.ts", - }) - - const mockConfig = { - command: "build" as const, - root: "/project", - logger: { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }, - } - - if (typeof plugin.configResolved === "function") { - plugin.configResolved(mockConfig as any) - } - if (typeof plugin.buildStart === "function") { - await plugin.buildStart.call({} as any, {} as any) - } - - const generatedContent = captureGeneratedContent() - expect(generatedContent).toBeTruthy() - expect(generatedContent!.content).toMatchSnapshot("sync-only-routes") - }) - - it("should generate correct routes for only async files", async () => { - const mockFiles = [ - "/project/src/pages/index.tsx", - "/project/src/pages/about.tsx", - "/project/src/pages/contact.tsx", - "/project/src/pages/blog/[slug].tsx", - ] - - const glob = await import("fast-glob") - vi.mocked(glob.default.sync).mockReturnValue(mockFiles) - - const plugin = routeBuilderPluginV2({ - outputPath: "./src/generated-routes.ts", - }) - - const mockConfig = { - command: "build" as const, - root: "/project", - logger: { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }, - } - - if (typeof plugin.configResolved === "function") { - plugin.configResolved(mockConfig as any) - } - if (typeof plugin.buildStart === "function") { - await plugin.buildStart.call({} as any, {} as any) - } - - const generatedContent = captureGeneratedContent() - expect(generatedContent).toBeTruthy() - expect(generatedContent!.content).toMatchSnapshot("async-only-routes") - }) - - it("should generate correct routes with custom transformPath", async () => { - const mockFiles = ["/project/src/pages/test.tsx", "/project/src/pages/demo.sync.tsx"] - - const glob = await import("fast-glob") - vi.mocked(glob.default.sync).mockReturnValue(mockFiles) - - const plugin = routeBuilderPluginV2({ - transformPath: (path) => path.replace("./pages/", "./custom/"), - outputPath: "./src/generated-routes.ts", - }) - - const mockConfig = { - command: "build" as const, - root: "/project", - logger: { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }, - } - - if (typeof plugin.configResolved === "function") { - plugin.configResolved(mockConfig as any) - } - if (typeof plugin.buildStart === "function") { - await plugin.buildStart.call({} as any, {} as any) - } - - const generatedContent = captureGeneratedContent() - expect(generatedContent).toBeTruthy() - expect(generatedContent!.content).toMatchSnapshot("custom-transform-routes") - }) - - it("should generate empty routes array when no files found", async () => { - const mockFiles: string[] = [] - - const glob = await import("fast-glob") - vi.mocked(glob.default.sync).mockReturnValue(mockFiles) - - const plugin = routeBuilderPluginV2({ - outputPath: "./src/generated-routes.ts", - }) - - const mockConfig = { - command: "build" as const, - root: "/project", - logger: { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }, - } - - if (typeof plugin.configResolved === "function") { - plugin.configResolved(mockConfig as any) - } - if (typeof plugin.buildStart === "function") { - await plugin.buildStart.call({} as any, {} as any) - } - - const generatedContent = captureGeneratedContent() - expect(generatedContent).toBeTruthy() - expect(generatedContent!.content).toMatchSnapshot("empty-routes") - }) - }) -}) diff --git a/packages/vite-plugin-route-builder/src/index.ts b/packages/vite-plugin-route-builder/src/index.ts deleted file mode 100644 index 8ad955d8c..000000000 --- a/packages/vite-plugin-route-builder/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./vite-plugin-route-builder" diff --git a/packages/vite-plugin-route-builder/src/utils/route-builder.ts b/packages/vite-plugin-route-builder/src/utils/route-builder.ts deleted file mode 100644 index 32d49d5fa..000000000 --- a/packages/vite-plugin-route-builder/src/utils/route-builder.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { get, omit } from "es-toolkit/compat" - -type NestedStructure = { [key: string]: NestedStructure } - -function nestPaths(paths: string[]): NestedStructure { - const result: NestedStructure = {} - - paths.forEach((path) => { - // Remove the './pages' prefix and the file extension - const prefix = "./pages/" - let suffix = ".tsx" - let trimmedPath: string - - // Check if it's a .sync.tsx file - if (path.endsWith(".sync.tsx")) { - suffix = ".sync.tsx" - trimmedPath = path.slice(prefix.length, -suffix.length) - } else { - trimmedPath = path.slice(prefix.length, -suffix.length) - } - - const parts = trimmedPath.split("/") - - let currentLevel = result - for (const part of parts) { - if (!currentLevel[part]) { - currentLevel[part] = {} - } - currentLevel = currentLevel[part] - } - }) - - return result -} - -// Extended RouteObject to include sync loading information -interface ExtendedRouteObject { - path?: string - index?: boolean - children?: ExtendedRouteObject[] - lazy?: any - handle?: { - fs: string - fullPath: string - isSync?: boolean - } -} - -export function buildGlobRoutes( - glob: Record Promise>, - options: { segmentGroupOrder?: string[] } = {}, -): ExtendedRouteObject[] { - const keys = Object.keys(glob) - const paths = nestPaths(keys) - const pathGetterSet = new Set() - const { segmentGroupOrder = [] } = options - - const routeObject: ExtendedRouteObject[] = [] - - function dfsRoutes( - parentKey: string, - children: ExtendedRouteObject[], - paths: NestedStructure, - parentPath = "", - ) { - const pathKeys = Object.keys(paths) - // sort `layout` to the start, and `index` to the end - pathKeys.sort((a, b) => { - if (a === "layout") { - return -1 - } - if (b === "layout") { - return 1 - } - if (a === "index") { - return 1 - } - if (b === "index") { - return -1 - } - return a.localeCompare(b) - }) - - // sort, if () group, then move to the end - pathKeys.sort((a, b) => { - if (a.startsWith("(") && a.endsWith(")")) { - return 1 - } - if (b.startsWith("(") && b.endsWith(")")) { - return -1 - } - return 0 - }) - - // Custom segment group ordering based on segmentGroupOrder option - if (segmentGroupOrder.length > 0) { - pathKeys.sort((a, b) => { - const isAGroup = a.startsWith("(") && a.endsWith(")") - const isBGroup = b.startsWith("(") && b.endsWith(")") - - // If both are groups, sort by the custom order - if (isAGroup && isBGroup) { - // Support both formats: "(main)" and "main" - const aIndex = segmentGroupOrder.findIndex( - (item) => item === a || item === a.slice(1, -1), - ) - const bIndex = segmentGroupOrder.findIndex( - (item) => item === b || item === b.slice(1, -1), - ) - - // If both are in the custom order, sort by their position - if (aIndex !== -1 && bIndex !== -1) { - return aIndex - bIndex - } - // If only a is in custom order, a comes first - if (aIndex !== -1 && bIndex === -1) { - return -1 - } - // If only b is in custom order, b comes first - if (aIndex === -1 && bIndex !== -1) { - return 1 - } - // If neither is in custom order, use filesystem order (localeCompare) - return a.localeCompare(b) - } - - return 0 // Non-groups maintain their existing sort order - }) - } - - for (const key of pathKeys) { - const isGroupedRoute = key.startsWith("(") && key.endsWith(")") - - const segmentPathKey = parentKey + key - - if (isGroupedRoute) { - // Check for both sync and async layout files - const syncLayoutPath = `${segmentPathKey}/layout.sync.tsx` - const asyncLayoutPath = `${segmentPathKey}/layout.tsx` - - let accessPath: string - let isSync = false - const syncGlobGetter = get(glob, syncLayoutPath) - let globGetter - - if (syncGlobGetter) { - accessPath = syncLayoutPath - isSync = true - globGetter = syncGlobGetter - } else { - accessPath = asyncLayoutPath - globGetter = get(glob, accessPath) || undefined - } - - if (pathGetterSet.has(accessPath)) { - console.error(`duplicate path: ${accessPath}`) - } - pathGetterSet.add(accessPath) - - const childrenChildren: ExtendedRouteObject[] = [] - dfsRoutes(`${segmentPathKey}/`, childrenChildren, paths[key]!, parentPath) - children.push({ - path: "", - lazy: globGetter, - children: childrenChildren, - handle: { - fs: segmentPathKey, - fullPath: parentPath, - isSync, - }, - }) - } else if (key === "layout") { - // if parent key is grouped routes, the layout is handled, so skip this logic - if (parentKey.endsWith(")/")) { - continue - } - - // Check for both sync and async layout files - const syncLayoutPath = `${segmentPathKey}.sync.tsx` - const asyncLayoutPath = `${segmentPathKey}.tsx` - - let isSync = false - const syncGlobGetter = get(glob, syncLayoutPath) - let globGetter - - if (syncGlobGetter) { - isSync = true - globGetter = syncGlobGetter - } else { - globGetter = get(glob, asyncLayoutPath) - } - - const childrenChildren: ExtendedRouteObject[] = [] - // should omit layout, because layout is already handled - dfsRoutes(parentKey, childrenChildren, omit(paths, "layout") as NestedStructure, parentPath) - children.push({ - path: "", - lazy: globGetter, - children: childrenChildren, - handle: { - fs: segmentPathKey, - fullPath: parentPath, - isSync, - }, - }) - break - } else { - const content = paths[key]! - const hasChild = Object.keys(content).length > 0 - - const normalizeKey = normalizePathKey(key) - - if (!hasChild) { - // Check for both sync and async files - const syncPath = `${segmentPathKey}.sync.tsx` - const asyncPath = `${segmentPathKey}.tsx` - - let accessPath: string - let isSync = false - const syncGlobGetter = get(glob, syncPath) - let globGetter - - if (syncGlobGetter) { - accessPath = syncPath - isSync = true - globGetter = syncGlobGetter - } else { - accessPath = asyncPath - globGetter = get(glob, asyncPath) - } - - if (pathGetterSet.has(accessPath)) { - console.error(`duplicate path: ${accessPath}`) - } - pathGetterSet.add(accessPath) - - children.push({ - path: normalizeKey, - lazy: globGetter, - handle: { - fs: `${segmentPathKey}/${normalizeKey}`, - fullPath: `${parentPath}/${normalizeKey}`, - isSync, - }, - }) - } else { - const childrenChildren: ExtendedRouteObject[] = [] - const fullPath = `${parentPath}/${normalizeKey}` - dfsRoutes(`${segmentPathKey}/`, childrenChildren, paths[key]!, fullPath) - children.push({ - path: normalizeKey, - children: childrenChildren, - handle: { - fs: `${segmentPathKey}/${normalizeKey}`, - fullPath, - }, - }) - } - } - } - } - - dfsRoutes("./pages/", routeObject, paths) - return routeObject -} - -const normalizePathKey = (key: string) => { - if (key === "index") { - return "" - } - - if (key.startsWith("[") && key.endsWith("]")) { - return `:${key.slice(1, -1)}` - } - return key -} diff --git a/packages/vite-plugin-route-builder/src/vite-plugin-route-builder.ts b/packages/vite-plugin-route-builder/src/vite-plugin-route-builder.ts deleted file mode 100644 index fa734c51b..000000000 --- a/packages/vite-plugin-route-builder/src/vite-plugin-route-builder.ts +++ /dev/null @@ -1,441 +0,0 @@ -import { writeFileSync } from "node:fs" -import { inspect } from "node:util" - -import glob from "fast-glob" -import { dirname, relative, resolve } from "pathe" -import type { Logger, Plugin } from "vite" - -import { buildGlobRoutes } from "./utils/route-builder" - -export interface RouteBuilderPluginOptions { - /** Page files glob pattern */ - pagePattern?: string - /** Output path for generated routes */ - outputPath?: string - /** Whether to enable in dev mode */ - enableInDev?: boolean - /** Custom file to route path transformation logic */ - transformPath?: (path: string) => string - /** Whether to disable logging */ - debug?: boolean - /** Custom order for segment groups in route tree. Array of group names (with or without parentheses). Default: filesystem order */ - segmentGroupOrder?: string[] -} - -export function routeBuilderPluginV2(options: RouteBuilderPluginOptions = {}): Plugin { - const { - pagePattern = "./pages/**/*.{tsx,sync.tsx}", - outputPath = "./src/generated-routes.ts", - enableInDev = true, - transformPath, - debug = false, - segmentGroupOrder = [], - } = options - - let isProduction = false - let root = "" - let logger: Logger - - function generateRouteFileContent( - routes: any[], - fileToImportMap: Record, - ): string { - // Collect all used lazy functions and sync imports - const usedLazyFunctions = new Set() - const usedSyncImports = new Set() - const lazyFunctionMap = new Map() - const syncImportMap = new Map() - let lazyCounter = 0 - let syncCounter = 0 - - // Recursively traverse route tree, collect all used functions - function collectUsedFunctions(routes: any[]) { - routes.forEach((route) => { - if (route.lazy && route.handle?.fs) { - const fsPath = route.handle.fs - const { isSync } = route.handle - - // Try to find the corresponding file - let matchedKey: string | undefined - - // Strategy 1: Direct match with sync extension - if (isSync && fileToImportMap[`${fsPath}.sync.tsx`]) { - matchedKey = `${fsPath}.sync.tsx` - } - // Strategy 2: Direct match with normal extension - else if (fileToImportMap[`${fsPath}.tsx`]) { - matchedKey = `${fsPath}.tsx` - } - // Strategy 3: layout file (for grouped routes) - sync - else if (isSync && fileToImportMap[`${fsPath}/layout.sync.tsx`]) { - matchedKey = `${fsPath}/layout.sync.tsx` - } - // Strategy 4: layout file (for grouped routes) - async - else if (fileToImportMap[`${fsPath}/layout.tsx`]) { - matchedKey = `${fsPath}/layout.tsx` - } - // Strategy 5: index file - sync - else if (isSync && fileToImportMap[`${fsPath}/index.sync.tsx`]) { - matchedKey = `${fsPath}/index.sync.tsx` - } - // Strategy 6: index file - async - else if (fileToImportMap[`${fsPath}/index.tsx`]) { - matchedKey = `${fsPath}/index.tsx` - } - // Strategy 7: For special path correction - else { - // If fsPath ends with /, it might be an index page - if (fsPath.endsWith("/")) { - const correctedPath = fsPath.slice(0, -1) // Remove trailing / - if (isSync && fileToImportMap[`${correctedPath}/index.sync.tsx`]) { - matchedKey = `${correctedPath}/index.sync.tsx` - } else if (fileToImportMap[`${correctedPath}/index.tsx`]) { - matchedKey = `${correctedPath}/index.tsx` - } else if (isSync && fileToImportMap[`${correctedPath}.sync.tsx`]) { - matchedKey = `${correctedPath}.sync.tsx` - } else if (fileToImportMap[`${correctedPath}.tsx`]) { - matchedKey = `${correctedPath}.tsx` - } - } - // For dynamic routes, remove /:param part, keep file path - else if (fsPath.includes("/:")) { - const correctedPath = fsPath.replace(/\/:[^/]+(?:\/.*)?$/, "") - if (isSync && fileToImportMap[`${correctedPath}.sync.tsx`]) { - matchedKey = `${correctedPath}.sync.tsx` - } else if (fileToImportMap[`${correctedPath}.tsx`]) { - matchedKey = `${correctedPath}.tsx` - } - } - // If fsPath ends with a repeated path segment, remove the last segment - else { - const pathParts = fsPath.split("/") - if (pathParts.length >= 2) { - const lastPart = pathParts.at(-1) - const secondLastPart = pathParts.at(-2) - - // If the last two path segments are the same, remove the last one - if (lastPart === secondLastPart) { - const correctedPath = pathParts.slice(0, -1).join("/") - if (isSync && fileToImportMap[`${correctedPath}.sync.tsx`]) { - matchedKey = `${correctedPath}.sync.tsx` - } else if (fileToImportMap[`${correctedPath}.tsx`]) { - matchedKey = `${correctedPath}.tsx` - } - } - } - } - } - - if (matchedKey && fileToImportMap[matchedKey]) { - if (isSync) { - const syncImportName = `SyncComponent${syncCounter++}` - usedSyncImports.add(matchedKey) - syncImportMap.set(matchedKey, syncImportName) - if (debug) { - logger.info( - `[route-builder-v2] Mapped sync import: ${fsPath} -> ${matchedKey} -> ${syncImportName}`, - ) - } - } else { - const lazyFuncName = `lazy${lazyCounter++}` - usedLazyFunctions.add(matchedKey) - lazyFunctionMap.set(matchedKey, lazyFuncName) - if (debug) { - logger.info( - `[route-builder-v2] Mapped lazy function: ${fsPath} -> ${matchedKey} -> ${lazyFuncName}`, - ) - } - } - } else { - logger.warn(`[route-builder-v2] Could not find file for fs path: ${fsPath}`) - logger.warn( - `[route-builder-v2] Available file keys: ${inspect(Object.keys(fileToImportMap), { - depth: null, - })}`, - ) - } - } - - if (route.children) { - collectUsedFunctions(route.children) - } - }) - } - - collectUsedFunctions(routes) - - // Generate import statements - const imports: string[] = [] - - // Generate sync imports - usedSyncImports.forEach((key) => { - const importPath = fileToImportMap[key] - const syncImportName = syncImportMap.get(key) - if (importPath && syncImportName) { - // Use import * as to avoid errors when loader doesn't exist - imports.push(`import * as ${syncImportName} from "${importPath}"`) - } - }) - - // Generate lazy imports - usedLazyFunctions.forEach((key) => { - const importPath = fileToImportMap[key] - const lazyFuncName = lazyFunctionMap.get(key) - if (importPath && lazyFuncName) { - imports.push(`const ${lazyFuncName} = () => import("${importPath}")`) - } - }) - - // Recursively process routes, replace lazy functions and remove handle - function processRoutes(routes: any[]): any { - return routes.map((route) => { - const newRoute: any = { ...route } - - // Process lazy functions and sync imports - if (route.lazy && route.handle?.fs) { - const fsPath = route.handle.fs - const { isSync } = route.handle - - // Find matching file - use the same matching logic - let matchedKey: string | undefined - - if (isSync && fileToImportMap[`${fsPath}.sync.tsx`]) { - matchedKey = `${fsPath}.sync.tsx` - } else if (fileToImportMap[`${fsPath}.tsx`]) { - matchedKey = `${fsPath}.tsx` - } else if (isSync && fileToImportMap[`${fsPath}/layout.sync.tsx`]) { - matchedKey = `${fsPath}/layout.sync.tsx` - } else if (fileToImportMap[`${fsPath}/layout.tsx`]) { - matchedKey = `${fsPath}/layout.tsx` - } else if (isSync && fileToImportMap[`${fsPath}/index.sync.tsx`]) { - matchedKey = `${fsPath}/index.sync.tsx` - } else if (fileToImportMap[`${fsPath}/index.tsx`]) { - matchedKey = `${fsPath}/index.tsx` - } else { - // For special path correction - if (fsPath.endsWith("/")) { - const correctedPath = fsPath.slice(0, -1) // Remove trailing / - if (isSync && fileToImportMap[`${correctedPath}/index.sync.tsx`]) { - matchedKey = `${correctedPath}/index.sync.tsx` - } else if (fileToImportMap[`${correctedPath}/index.tsx`]) { - matchedKey = `${correctedPath}/index.tsx` - } else if (isSync && fileToImportMap[`${correctedPath}.sync.tsx`]) { - matchedKey = `${correctedPath}.sync.tsx` - } else if (fileToImportMap[`${correctedPath}.tsx`]) { - matchedKey = `${correctedPath}.tsx` - } - } - // For dynamic routes, remove /:param part, keep file path - else if (fsPath.includes("/:")) { - const correctedPath = fsPath.replace(/\/:[^/]+(?:\/.*)?$/, "") - if (isSync && fileToImportMap[`${correctedPath}.sync.tsx`]) { - matchedKey = `${correctedPath}.sync.tsx` - } else if (fileToImportMap[`${correctedPath}.tsx`]) { - matchedKey = `${correctedPath}.tsx` - } - } - // For repeated path segments, try removing the last segment - else { - const pathParts = fsPath.split("/") - if (pathParts.length >= 2) { - const lastPart = pathParts.at(-1) - const secondLastPart = pathParts.at(-2) - - // If the last two path segments are the same, remove the last one - if (lastPart === secondLastPart) { - const correctedPath = pathParts.slice(0, -1).join("/") - if (isSync && fileToImportMap[`${correctedPath}.sync.tsx`]) { - matchedKey = `${correctedPath}.sync.tsx` - } else if (fileToImportMap[`${correctedPath}.tsx`]) { - matchedKey = `${correctedPath}.tsx` - } - } - } - } - } - - if (matchedKey) { - if (isSync && syncImportMap.has(matchedKey)) { - // For sync imports, use Component property instead of lazy - const syncComponentName = syncImportMap.get(matchedKey) - newRoute.Component = `__SYNC_${syncComponentName}.Component__` - // Conditionally add loader if it exists - newRoute.loader = `__SYNC_${syncComponentName}.loader__` - delete newRoute.lazy - } else if (lazyFunctionMap.has(matchedKey)) { - newRoute.lazy = `__LAZY_${lazyFunctionMap.get(matchedKey)}__` - } else { - // If no matching function is found, delete lazy property - delete newRoute.lazy - logger.warn(`[route-builder-v2] No function for route: ${fsPath}`) - } - } else { - delete newRoute.lazy - logger.warn(`[route-builder-v2] No matching file for route: ${fsPath}`) - } - } - - // Remove handle property - delete newRoute.handle - - // Recursively process children - if (route.children) { - newRoute.children = processRoutes(route.children) - } - - return newRoute - }) - } - - const processedRoutes = processRoutes(routes) - - // Convert routes object to string and replace function placeholders - const routesString = JSON.stringify(processedRoutes, null, 2) - .replaceAll(/"__LAZY_(\w+)__"/g, "$1") - .replaceAll(/"__SYNC_([^.]+)\.Component__"/g, "$1.Component") - .replaceAll(/"__SYNC_([^.]+)\.loader__"/g, "$1.loader") - // Remove loader property if it's undefined - .replaceAll(/,?\s*"loader":\s*undefined/g, "") - - return `// This file is auto-generated by vite-plugin-route-builder -// Do not edit manually -/* eslint-disable */ -// @ts-nocheck - -import type { RouteObject } from "react-router" - -// Imports for page components -${imports.join("\n")} - -// Generated route configuration -export const routes: RouteObject[] = ${routesString} - -export default routes -` - } - - function generateRoutes() { - try { - const pageFiles = glob.sync(pagePattern, { - cwd: root, - absolute: true, - }) - - logger.info(`[route-builder-v2] Found ${pageFiles.length} page files`) - - // Build glob object, key is the relative path to pages directory - const globObject: Record Promise> = {} - const fileToImportMap: Record = {} - - pageFiles.forEach((absolutePath) => { - // Get relative path to root - const relativePath = relative(root, absolutePath) - - // Convert to ./pages/ format for route-builder - let routeKey: string - if (relativePath.includes("/pages/")) { - routeKey = `./pages/${relativePath.split("/pages/")[1]}` - } else if (relativePath.includes("\\pages\\")) { - routeKey = `./pages/${relativePath.split("\\pages\\")[1]?.replaceAll("\\", "/")}` - } else { - // Assume file is in pages directory - routeKey = `./${relativePath.replaceAll("\\", "/")}` - } - - // Apply custom path transformation - if (transformPath) { - routeKey = transformPath(routeKey) - } - - // Generate relative path for import (relative to output file) - const outputDir = dirname(resolve(root, outputPath)) - let importPath = relative(outputDir, absolutePath) - - // Ensure correct path separator - importPath = importPath.replaceAll("\\", "/") - - // Ensure relative path starts with ./ or ../ - if (!importPath.startsWith(".")) { - importPath = `./${importPath}` - } - - // Store the import path with different handling for sync vs async - let finalImportPath: string - if (importPath.endsWith(".sync.tsx")) { - // For sync files, remove .tsx but keep .sync for the import path - finalImportPath = importPath.replace(/\.tsx$/, "") - } else { - // For async files, remove .tsx extension - finalImportPath = importPath.replace(/\.tsx$/, "") - } - - globObject[routeKey] = () => Promise.resolve({ default: () => null }) - fileToImportMap[routeKey] = finalImportPath - - if (debug) { - logger.info(`[route-builder-v2] Mapped: ${routeKey} -> ${finalImportPath}`) - } - }) - - // Use existing route building logic - const routes = buildGlobRoutes(globObject, { segmentGroupOrder }) - - // Generate route file content - const routeFileContent = generateRouteFileContent(routes, fileToImportMap) - - const outputFilePath = resolve(root, outputPath) - writeFileSync(outputFilePath, routeFileContent, "utf-8") - - logger.info(`[route-builder-v2] Generated routes: ${outputFilePath}`) - } catch (error: any) { - logger.error(`[route-builder-v2] Error generating routes:${error.message}`) - console.error(error) - throw error - } - } - - return { - name: "vite-plugin-route-builder-v2", - configResolved(config) { - isProduction = config.command === "build" - root = config.root - logger = config.logger - }, - - buildStart() { - if (isProduction || enableInDev) { - generateRoutes() - } - }, - - configureServer(server) { - if (!enableInDev) return - - const watchPattern = resolve(root, pagePattern.replace("./", "")) - server.watcher.add(watchPattern) - - server.watcher.on("add", handleFileChange) - server.watcher.on("unlink", handleFileChange) - - function handleFileChange(path: string) { - const relativePath = relative(root, path) - if ( - relativePath.includes("/pages/") && - (relativePath.endsWith(".tsx") || relativePath.endsWith(".sync.tsx")) - ) { - logger.info(`[route-builder-v2] Page file changed: ${relativePath}`) - generateRoutes() - - // Send custom HMR event - server.ws.send({ - type: "custom", - event: "routes-updated", - data: { timestamp: Date.now() }, - }) - } - } - }, - } -} - -export default routeBuilderPluginV2 diff --git a/packages/vite-plugin-route-builder/tsconfig.json b/packages/vite-plugin-route-builder/tsconfig.json deleted file mode 100644 index 3c714f787..000000000 --- a/packages/vite-plugin-route-builder/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../configs/tsconfig.extend.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/vite-plugin-route-builder/vitest.config.ts b/packages/vite-plugin-route-builder/vitest.config.ts deleted file mode 100644 index b8860f395..000000000 --- a/packages/vite-plugin-route-builder/vitest.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import tsconfigPath from "vite-tsconfig-paths" -import { defineConfig } from "vitest/config" - -export default defineConfig({ - test: { - globals: true, - environment: "node", - coverage: { - reporter: ["text", "json", "html"], - exclude: ["node_modules/", "src/__tests__/", "dist/", "*.config.*", "**/*.d.ts"], - }, - }, - plugins: [ - tsconfigPath({ - projects: ["./tsconfig.json"], - }), - ], -}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee01e78de..baddb411d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,9 @@ settings: catalogs: default: + '@follow-app/client-sdk': + specifier: 0.3.22 + version: 0.3.22 typescript: specifier: 5.8.3 version: 5.8.3 @@ -185,9 +188,6 @@ importers: '@electron-toolkit/tsconfig': specifier: 1.0.1 version: 1.0.1(@types/node@24.0.10) - '@follow-app/vite-plugin-route-builder': - specifier: workspace:* - version: link:../../packages/vite-plugin-route-builder '@follow/components': specifier: workspace:* version: link:../../packages/internal/components @@ -246,8 +246,8 @@ importers: specifier: 7.0.7 version: 7.0.7(postcss@8.5.6) drizzle-orm: - specifier: 0.44.2 - version: 0.44.2(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2) + specifier: 0.44.3 + version: 0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) electron: specifier: 37.2.0 version: 37.2.0 @@ -311,6 +311,9 @@ importers: vite-plugin-pwa: specifier: 1.0.1 version: 1.0.1(@vite-pwa/assets-generator@1.0.0)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.20.3)(yaml@2.8.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0) + vite-plugin-route-builder: + specifier: 0.3.0 + version: 0.3.0 vite-tsconfig-paths: specifier: 5.1.4 version: 5.1.4(typescript@5.8.3)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.20.3)(yaml@2.8.0)) @@ -427,11 +430,11 @@ importers: apps/desktop/layer/renderer: dependencies: '@ai-sdk/openai': - specifier: 2.0.0-beta.5 - version: 2.0.0-beta.5(zod@3.25.75) - '@ai-sdk/react': specifier: 2.0.0-beta.11 - version: 2.0.0-beta.11(react@19.0.0)(zod@3.25.75) + version: 2.0.0-beta.11(zod@3.25.75) + '@ai-sdk/react': + specifier: 2.0.0-beta.25 + version: 2.0.0-beta.25(react@19.0.0)(zod@3.25.75) '@dnd-kit/core': specifier: 6.3.1 version: 6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -441,6 +444,9 @@ importers: '@electron-toolkit/preload': specifier: 3.0.2 version: 3.0.2(electron@37.2.0) + '@follow-app/client-sdk': + specifier: 'catalog:' + version: 0.3.22(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) '@follow/database': specifier: workspace:* version: link:../../../../packages/internal/database @@ -456,6 +462,9 @@ importers: '@follow/tracker': specifier: workspace:* version: link:../../../../packages/internal/tracker + '@folo-services/constants': + specifier: 0.1.6 + version: 0.1.6 '@fontsource/sn-pro': specifier: 5.2.5 version: 5.2.5 @@ -535,8 +544,8 @@ importers: specifier: 0.10.1 version: 0.10.1 ai: - specifier: 5.0.0-beta.11 - version: 5.0.0-beta.11(zod@3.25.75) + specifier: 5.0.0-beta.25 + version: 5.0.0-beta.25(zod@3.25.75) camelcase-keys: specifier: 9.1.3 version: 9.1.3 @@ -727,6 +736,9 @@ importers: '@follow/utils': specifier: workspace:* version: link:../../../../packages/internal/utils + '@folo-services/ai-tools': + specifier: 0.2.14 + version: 0.2.14(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) '@types/node': specifier: 24.0.10 version: 24.0.10 @@ -1226,9 +1238,6 @@ importers: specifier: 3.25.75 version: 3.25.75 devDependencies: - '@follow-app/vite-plugin-route-builder': - specifier: workspace:* - version: link:../../packages/vite-plugin-route-builder '@follow/components': specifier: workspace:* version: link:../../packages/internal/components @@ -1310,6 +1319,9 @@ importers: vite: specifier: 7.0.2 version: 7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.20.3)(yaml@2.8.0) + vite-plugin-route-builder: + specifier: 0.3.0 + version: 0.3.0 packages/configs: dependencies: @@ -1582,6 +1594,9 @@ importers: packages/internal/constants: dependencies: + '@follow-app/client-sdk': + specifier: 'catalog:' + version: 0.3.22(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) '@follow/configs': specifier: workspace:* version: link:../../configs @@ -1591,6 +1606,9 @@ importers: packages/internal/database: dependencies: + '@follow-app/client-sdk': + specifier: 'catalog:' + version: 0.3.22(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) '@follow/constants': specifier: workspace:* version: link:../constants @@ -1602,16 +1620,16 @@ importers: version: link:../shared ai: specifier: 5.0.0-beta.7 - version: 5.0.0-beta.7(zod@3.25.75) + version: 5.0.0-beta.7(zod@3.25.76) drizzle-orm: - specifier: 0.44.2 - version: 0.44.2(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2) + specifier: 0.44.3 + version: 0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) expo-sqlite: specifier: 15.2.12 version: 15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0) sqlocal: specifier: npm:@hyoban/sqlocal@0.14.1-fork.4 - version: '@hyoban/sqlocal@0.14.1-fork.4(bufferutil@4.0.9)(drizzle-orm@0.44.2(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2))(kysely@0.28.2)' + version: '@hyoban/sqlocal@0.14.1-fork.4(bufferutil@4.0.9)(drizzle-orm@0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3))(kysely@0.28.2)' wa-sqlite: specifier: git+https://github.com/rhashimoto/wa-sqlite.git#v1.0.8 version: https://codeload.github.com/rhashimoto/wa-sqlite/tar.gz/03c00ed67cd934bd664ae310234bba317928f851 @@ -1666,6 +1684,9 @@ importers: packages/internal/models: dependencies: + '@follow-app/client-sdk': + specifier: 'catalog:' + version: 0.3.22(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) '@follow/constants': specifier: workspace:* version: link:../constants @@ -1710,8 +1731,8 @@ importers: specifier: 1.2.9 version: 1.2.9 drizzle-orm: - specifier: 0.44.2 - version: 0.44.2(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2) + specifier: 0.44.3 + version: 0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) hono: specifier: 4.8.1 version: 4.8.1(patch_hash=5c74c2d2afaa5880c13d75458dd26c84da568851691b1fb6de6d877c29936d05) @@ -1727,6 +1748,9 @@ importers: packages/internal/store: dependencies: + '@follow-app/client-sdk': + specifier: 'catalog:' + version: 0.3.22(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) '@follow/configs': specifier: workspace:* version: link:../../configs @@ -1859,39 +1883,6 @@ importers: specifier: 0.12.9 version: 0.12.9(typescript@5.8.3) - packages/vite-plugin-route-builder: - devDependencies: - '@types/node': - specifier: 24.0.10 - version: 24.0.10 - es-toolkit: - specifier: 1.39.6 - version: 1.39.6 - fast-glob: - specifier: 3.3.3 - version: 3.3.3 - happy-dom: - specifier: 18.0.1 - version: 18.0.1 - react-router: - specifier: 7.6.3 - version: 7.6.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - tsdown: - specifier: 0.12.9 - version: 0.12.9(typescript@5.8.3) - typescript: - specifier: 'catalog:' - version: 5.8.3 - vite: - specifier: 7.0.2 - version: 7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.20.3)(yaml@2.8.0) - vite-tsconfig-paths: - specifier: 5.1.4 - version: 5.1.4(typescript@5.8.3)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.20.3)(yaml@2.8.0)) - vitest: - specifier: 3.2.4 - version: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@24.0.10)(happy-dom@18.0.1)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.20.3)(yaml@2.8.0) - packages: 7zip-bin@5.2.0: @@ -1905,23 +1896,23 @@ packages: graphql: optional: true + '@ai-sdk/gateway@1.0.0-beta.11': + resolution: {integrity: sha512-dnRUPzSLvp3xvIx6M4FIz4ht8dfL8JkPKwH+akj10im4zbxUii3c3TQ3BJLRdx2Gq/SeljE9H0dX7PDtVyIrbQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + '@ai-sdk/gateway@1.0.0-beta.3': resolution: {integrity: sha512-g49gMSkXy94lYvl5LRh438OR/0JCG6ol0jV+iLot7cy5HLltZlGocEuauETBu4b10mDXOd7XIjTEZoQpYFMYLQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.49 - '@ai-sdk/gateway@1.0.0-beta.4': - resolution: {integrity: sha512-P5/dS7pb+cBRWnTP0Aezq3/3PIrF+p64fUTxBAyZIert8qTyHm2gd6atbAJZprZ304Ui3QjA9MFybAa849//2w==} + '@ai-sdk/openai@2.0.0-beta.11': + resolution: {integrity: sha512-HQXUMb1V6Xr8EBYvEDwNb8ISyRqyxg2zUst7lzPb6s1nGDKJRBTfSyytNWRL9dZ9vxjM2wK34cltCfZbjaHpAA==} engines: {node: '>=18'} peerDependencies: - zod: ^3.25.49 - - '@ai-sdk/openai@2.0.0-beta.5': - resolution: {integrity: sha512-jDd3NKFIbLHam25XSkTsR9U0SZ7tgdl9z+HO0pu7n6sde6f5+CW/3zIk6BJoCNJKU9382kEDKu8u+6PREYd9ZQ==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.49 + zod: ^3.25.76 || ^4 '@ai-sdk/provider-utils@3.0.0-beta.2': resolution: {integrity: sha512-H4K+4weOVgWqrDDeAbQWoA4U5mN4WrQPHQFdH7ynQYcnhj/pzctU9Q6mGlR5ESMWxaXxazxlOblSITlXo9bahA==} @@ -1929,16 +1920,22 @@ packages: peerDependencies: zod: ^3.25.49 + '@ai-sdk/provider-utils@3.0.0-beta.5': + resolution: {integrity: sha512-4Dv/wiGZrvO6fI7P0yMLa4XZru0XW8LPibTObbkHBdweLUVGIze7aCfxxQeY44Uqcbl/h6/yBTkx2XmPtwf/Ow==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + '@ai-sdk/provider@2.0.0-beta.1': resolution: {integrity: sha512-Z8SPncMtS3RsoXITmT7NVwrAq6M44dmw0DoUOYJqNNtCu8iMWuxB8Nxsoqpa0uEEy9R1V1ZThJAXTYgjTUxl3w==} engines: {node: '>=18'} - '@ai-sdk/react@2.0.0-beta.11': - resolution: {integrity: sha512-twritdquNQSBAS24Qme3+AMwMbQa41WF2zcgmnTbgkeltG+gl1be1rWtaOKAXlm5nuQOH9CriUvvRskHkx+mDw==} + '@ai-sdk/react@2.0.0-beta.25': + resolution: {integrity: sha512-3f3f/z3idsH+sctQJ7t0enFywzIbHXDM6a3tvdPMX1dI53mAsVfyhaHzz8WT9swW3Mmphj0i5fZCH4t78m0L4A==} engines: {node: '>=18'} peerDependencies: react: 19.0.0 - zod: ^3.25.49 + zod: ^3.25.76 || ^4 peerDependenciesMeta: zod: optional: true @@ -4024,6 +4021,27 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@follow-app/client-sdk@0.3.22': + resolution: {integrity: sha512-kFt3jQkgIQnCHwtiHa+08o4/X56VeDWpOA4Srus5llh5AEU0LvuZz2GWTsSmcOSGLDeFFXcMyxY7ZzScFR1igQ==} + + '@folo-services/ai-tools@0.2.14': + resolution: {integrity: sha512-3DG/FqGqNvpCUtq1WMwzMYW6XuhQQfRfIGyZbokJ+XgbyR4YO3Jtz9T5fluINy+Q9iQSRyZSLyhMxe4Evsj5Hg==} + + '@folo-services/constants@0.1.14': + resolution: {integrity: sha512-dR6KZhKnT6eKADoxhf+h4RNIdcqaO0TLV3eWhR7L4vuZY3uQJmscyvLfaXl0N+9Q3Dpt8IqzVCUz5PRt0eg9Lw==} + + '@folo-services/constants@0.1.6': + resolution: {integrity: sha512-+Wdd0BfCmjddduteksJnbHl1GVrTJlFHnjXeyVJxr7l6nkHdW9lacpqLbyFBmlXm39Mtvvz6vDdR0DjaCsJG3w==} + + '@folo-services/drizzle@0.1.9': + resolution: {integrity: sha512-X+YIuukSKaC5F7ZK7/zYDvtU75RVcJDC4lNvUdx/xCwaJgpRj0gjw8PMoTz7nc77Va99g6yw4a31cKxgfrPSSA==} + + '@folo-services/exceptions@0.1.7': + resolution: {integrity: sha512-e3BnGAfCEt19kAvdijGvgdFI0IJ8pBANeEj+CXUtoa95UdHCw88x6ePWpJp1m8SL34ImmstywNhnWHHQgT8ZKg==} + + '@folo-services/shared@0.0.4': + resolution: {integrity: sha512-wOxEbhMHeTMvlcHQmS9FjSNmvVIbLsxuhYm14ZT9yVL6ga5oaxcskZ0zlhvtY2Lkq/PobJ8WDqed3rH1rMIGiQ==} + '@fontsource/sn-pro@5.2.5': resolution: {integrity: sha512-rBdBv/0ygj6bkO7xDMMFpwobLdSrcQ2Jncb6DIwdeYGoAgeWkQRwVYhGDKasfLjEYCNYxrqr6wsXsM9+aU39RA==} @@ -6966,11 +6984,12 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} - ai@5.0.0-beta.11: - resolution: {integrity: sha512-go/+f+KKnIWLPcv5IIcCEoZ0J8IAnEIgGPJwFDti9g1zuafjlxoFlBza8gu5oGODWOfjS7aEKUMG5Ac7GPPXsQ==} + ai@5.0.0-beta.25: + resolution: {integrity: sha512-pbfFqtQvz7hiDw6TwUH75CK9FgrZFBsxqbW4yW0aqluHw3nRbhf0w1u2AMiYgvWMy8Xf8TkBbMtY4vyMc4neeA==} engines: {node: '>=18'} + hasBin: true peerDependencies: - zod: ^3.25.49 + zod: ^3.25.76 || ^4 ai@5.0.0-beta.6: resolution: {integrity: sha512-0VqcqbWiF2XwWBgNfrEr05QrB0bJqctwLyuk7xfy7g8OBSqyJ4OV95HGTzrBvN8/iU/4u+eNTlF5nAG5Qx3utw==} @@ -8691,8 +8710,8 @@ packages: resolution: {integrity: sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA==} hasBin: true - drizzle-orm@0.44.2: - resolution: {integrity: sha512-zGAqBzWWkVSFjZpwPOrmCrgO++1kZ5H/rZ4qTGeGOe18iXGVJWf3WPfHOVwFIbmi8kHjfJstC6rJomzGx8g/dQ==} + drizzle-orm@0.44.3: + resolution: {integrity: sha512-8nIiYQxOpgUicEL04YFojJmvC4DNO4KoyXsEIqN44+g6gNBr6hmVpWk3uyAt4CaTiRGDwoU+alfqNNeonLAFOQ==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' '@cloudflare/workers-types': '>=4' @@ -8783,6 +8802,12 @@ packages: sqlite3: optional: true + drizzle-zod@0.7.1: + resolution: {integrity: sha512-nZzALOdz44/AL2U005UlmMqaQ1qe5JfanvLujiTHiiT8+vZJTBFhj3pY4Vk+L6UWyKFfNmLhk602Hn4kCTynKQ==} + peerDependencies: + drizzle-orm: '>=0.36.0' + zod: '>=3.0.0' + ds-store@0.1.6: resolution: {integrity: sha512-kY21M6Lz+76OS3bnCzjdsJSF7LBpLYGCVfavW8TgQD2XkcqIZ86W0y9qUDZu6fp7SIZzqosMDW2zi7zVFfv4hw==} @@ -12651,17 +12676,43 @@ packages: perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + pg-cloudflare@1.2.7: + resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} + + pg-connection-string@2.9.1: + resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==} + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} + pg-pool@3.10.1: + resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==} + peerDependencies: + pg: '>=8.0' + pg-protocol@1.10.2: resolution: {integrity: sha512-Ci7jy8PbaWxfsck2dwZdERcDG2A0MG8JoQILs+uZNjABFuBuItAZCWUNz8sXRDMoui24rJw7WlXqgpMdBSN/vQ==} + pg-protocol@1.10.3: + resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} + pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} engines: {node: '>=4'} + pg@8.16.3: + resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + phin@2.9.3: resolution: {integrity: sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. @@ -15452,6 +15503,9 @@ packages: '@vite-pwa/assets-generator': optional: true + vite-plugin-route-builder@0.3.0: + resolution: {integrity: sha512-ukCx4NN9QbnsBNrPyVAbMHcyH9bg+qJI2zJcRDkXAbzahuZUxGXOEZPWY8/uEm52vdzdknG4W6iYFKvO/4lQtA==} + vite-tsconfig-paths@5.1.4: resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} peerDependencies: @@ -15930,6 +15984,9 @@ packages: zod@3.25.75: resolution: {integrity: sha512-OhpzAmVzabPOL6C3A3gpAifqr9MqihV/Msx3gor2b2kviCgcb+HM9SEOpMWwwNp9MRunWnhtAKUoo0AHhjyPPg==} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zustand@5.0.6: resolution: {integrity: sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==} engines: {node: '>=12.20.0'} @@ -15964,23 +16021,41 @@ snapshots: optionalDependencies: graphql: 16.8.1 + '@ai-sdk/gateway@1.0.0-beta.11(zod@3.25.75)': + dependencies: + '@ai-sdk/provider': 2.0.0-beta.1 + '@ai-sdk/provider-utils': 3.0.0-beta.5(zod@3.25.75) + zod: 3.25.75 + + '@ai-sdk/gateway@1.0.0-beta.11(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0-beta.1 + '@ai-sdk/provider-utils': 3.0.0-beta.5(zod@3.25.76) + zod: 3.25.76 + '@ai-sdk/gateway@1.0.0-beta.3(zod@3.25.75)': dependencies: '@ai-sdk/provider': 2.0.0-beta.1 '@ai-sdk/provider-utils': 3.0.0-beta.2(zod@3.25.75) zod: 3.25.75 - '@ai-sdk/gateway@1.0.0-beta.4(zod@3.25.75)': + '@ai-sdk/gateway@1.0.0-beta.3(zod@3.25.76)': dependencies: '@ai-sdk/provider': 2.0.0-beta.1 - '@ai-sdk/provider-utils': 3.0.0-beta.2(zod@3.25.75) + '@ai-sdk/provider-utils': 3.0.0-beta.2(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/openai@2.0.0-beta.11(zod@3.25.75)': + dependencies: + '@ai-sdk/provider': 2.0.0-beta.1 + '@ai-sdk/provider-utils': 3.0.0-beta.5(zod@3.25.75) zod: 3.25.75 - '@ai-sdk/openai@2.0.0-beta.5(zod@3.25.75)': + '@ai-sdk/openai@2.0.0-beta.11(zod@3.25.76)': dependencies: '@ai-sdk/provider': 2.0.0-beta.1 - '@ai-sdk/provider-utils': 3.0.0-beta.2(zod@3.25.75) - zod: 3.25.75 + '@ai-sdk/provider-utils': 3.0.0-beta.5(zod@3.25.76) + zod: 3.25.76 '@ai-sdk/provider-utils@3.0.0-beta.2(zod@3.25.75)': dependencies: @@ -15990,14 +16065,38 @@ snapshots: zod: 3.25.75 zod-to-json-schema: 3.24.5(zod@3.25.75) + '@ai-sdk/provider-utils@3.0.0-beta.2(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0-beta.1 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.3 + zod: 3.25.76 + zod-to-json-schema: 3.24.5(zod@3.25.76) + + '@ai-sdk/provider-utils@3.0.0-beta.5(zod@3.25.75)': + dependencies: + '@ai-sdk/provider': 2.0.0-beta.1 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.3 + zod: 3.25.75 + zod-to-json-schema: 3.24.5(zod@3.25.75) + + '@ai-sdk/provider-utils@3.0.0-beta.5(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0-beta.1 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.3 + zod: 3.25.76 + zod-to-json-schema: 3.24.5(zod@3.25.76) + '@ai-sdk/provider@2.0.0-beta.1': dependencies: json-schema: 0.4.0 - '@ai-sdk/react@2.0.0-beta.11(react@19.0.0)(zod@3.25.75)': + '@ai-sdk/react@2.0.0-beta.25(react@19.0.0)(zod@3.25.75)': dependencies: - '@ai-sdk/provider-utils': 3.0.0-beta.2(zod@3.25.75) - ai: 5.0.0-beta.11(zod@3.25.75) + '@ai-sdk/provider-utils': 3.0.0-beta.5(zod@3.25.75) + ai: 5.0.0-beta.25(zod@3.25.75) react: 19.0.0 swr: 2.3.3(react@19.0.0) throttleit: 2.1.0 @@ -19086,6 +19185,171 @@ snapshots: '@floating-ui/utils@0.2.9': {} + '@follow-app/client-sdk@0.3.22(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)': + dependencies: + '@folo-services/constants': 0.1.14 + '@folo-services/drizzle': 0.1.9(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2) + '@folo-services/exceptions': 0.1.7 + '@folo-services/shared': 0.0.4(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) + zod: 3.25.76 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bun-types + - expo-sqlite + - gel + - knex + - kysely + - mysql2 + - pg + - pg-native + - postgres + - prisma + - sql.js + - sqlite3 + + '@folo-services/ai-tools@0.2.14(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)': + dependencies: + '@ai-sdk/openai': 2.0.0-beta.11(zod@3.25.76) + '@folo-services/drizzle': 0.1.9(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2) + ai: 5.0.0-beta.25(zod@3.25.76) + drizzle-orm: 0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) + zod: 3.25.76 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bun-types + - expo-sqlite + - gel + - knex + - kysely + - mysql2 + - pg + - pg-native + - postgres + - prisma + - sql.js + - sqlite3 + + '@folo-services/constants@0.1.14': + dependencies: + zod: 3.25.76 + + '@folo-services/constants@0.1.6': + dependencies: + zod: 3.25.76 + + '@folo-services/drizzle@0.1.9(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)': + dependencies: + '@folo-services/exceptions': 0.1.7 + drizzle-orm: 0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) + drizzle-zod: 0.7.1(drizzle-orm@0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3))(zod@3.25.76) + nanoid: 5.1.5 + pg: 8.16.3 + zod: 3.25.76 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bun-types + - expo-sqlite + - gel + - knex + - kysely + - mysql2 + - pg-native + - postgres + - prisma + - sql.js + - sqlite3 + + '@folo-services/exceptions@0.1.7': {} + + '@folo-services/shared@0.0.4(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3)': + dependencies: + '@folo-services/drizzle': 0.1.9(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2) + drizzle-orm: 0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) + drizzle-zod: 0.7.1(drizzle-orm@0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3))(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@prisma/client' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/pg' + - '@types/sql.js' + - '@upstash/redis' + - '@vercel/postgres' + - '@xata.io/client' + - better-sqlite3 + - bun-types + - expo-sqlite + - gel + - knex + - kysely + - mysql2 + - pg + - pg-native + - postgres + - prisma + - sql.js + - sqlite3 + '@fontsource/sn-pro@5.2.5': {} '@freakycoder/react-native-bounceable@1.0.3': {} @@ -19169,12 +19433,12 @@ snapshots: tailwind-api-utils: 1.0.3(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@24.0.10)(typescript@5.8.3))) tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@24.0.10)(typescript@5.8.3)) - '@hyoban/sqlocal@0.14.1-fork.4(bufferutil@4.0.9)(drizzle-orm@0.44.2(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2))(kysely@0.28.2)': + '@hyoban/sqlocal@0.14.1-fork.4(bufferutil@4.0.9)(drizzle-orm@0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3))(kysely@0.28.2)': dependencies: '@sqlite.org/sqlite-wasm': 3.50.0-build1 coincident: 1.2.3(bufferutil@4.0.9) optionalDependencies: - drizzle-orm: 0.44.2(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2) + drizzle-orm: 0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) kysely: 0.28.2 transitivePeerDependencies: - bufferutil @@ -22549,14 +22813,22 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - ai@5.0.0-beta.11(zod@3.25.75): + ai@5.0.0-beta.25(zod@3.25.75): dependencies: - '@ai-sdk/gateway': 1.0.0-beta.4(zod@3.25.75) + '@ai-sdk/gateway': 1.0.0-beta.11(zod@3.25.75) '@ai-sdk/provider': 2.0.0-beta.1 - '@ai-sdk/provider-utils': 3.0.0-beta.2(zod@3.25.75) + '@ai-sdk/provider-utils': 3.0.0-beta.5(zod@3.25.75) '@opentelemetry/api': 1.9.0 zod: 3.25.75 + ai@5.0.0-beta.25(zod@3.25.76): + dependencies: + '@ai-sdk/gateway': 1.0.0-beta.11(zod@3.25.76) + '@ai-sdk/provider': 2.0.0-beta.1 + '@ai-sdk/provider-utils': 3.0.0-beta.5(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + zod: 3.25.76 + ai@5.0.0-beta.6(zod@3.25.75): dependencies: '@ai-sdk/gateway': 1.0.0-beta.3(zod@3.25.75) @@ -22565,13 +22837,13 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 3.25.75 - ai@5.0.0-beta.7(zod@3.25.75): + ai@5.0.0-beta.7(zod@3.25.76): dependencies: - '@ai-sdk/gateway': 1.0.0-beta.3(zod@3.25.75) + '@ai-sdk/gateway': 1.0.0-beta.3(zod@3.25.76) '@ai-sdk/provider': 2.0.0-beta.1 - '@ai-sdk/provider-utils': 3.0.0-beta.2(zod@3.25.75) + '@ai-sdk/provider-utils': 3.0.0-beta.2(zod@3.25.76) '@opentelemetry/api': 1.9.0 - zod: 3.25.75 + zod: 3.25.76 ajv-formats@2.1.1(ajv@8.11.0): optionalDependencies: @@ -24605,12 +24877,18 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.44.2(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2): + drizzle-orm@0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3): optionalDependencies: '@opentelemetry/api': 1.9.0 '@types/pg': 8.6.1 expo-sqlite: 15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0) kysely: 0.28.2 + pg: 8.16.3 + + drizzle-zod@0.7.1(drizzle-orm@0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3))(zod@3.25.76): + dependencies: + drizzle-orm: 0.44.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(expo-sqlite@15.2.12(expo@53.0.12(@babel/core@7.28.0)(@expo/metro-runtime@5.0.4(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0)))(bufferutil@4.0.9)(graphql@16.8.1)(react-native-webview@13.15.0(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(react-native@0.79.1(@babel/core@7.28.0)(@types/react@19.1.8)(bufferutil@4.0.9)(react@19.0.0))(react@19.0.0))(kysely@0.28.2)(pg@8.16.3) + zod: 3.25.76 ds-store@0.1.6: dependencies: @@ -29521,10 +29799,21 @@ snapshots: perfect-debounce@1.0.0: {} + pg-cloudflare@1.2.7: + optional: true + + pg-connection-string@2.9.1: {} + pg-int8@1.0.1: {} + pg-pool@3.10.1(pg@8.16.3): + dependencies: + pg: 8.16.3 + pg-protocol@1.10.2: {} + pg-protocol@1.10.3: {} + pg-types@2.2.0: dependencies: pg-int8: 1.0.1 @@ -29533,6 +29822,20 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 + pg@8.16.3: + dependencies: + pg-connection-string: 2.9.1 + pg-pool: 3.10.1(pg@8.16.3) + pg-protocol: 1.10.3 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.2.7 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + phin@2.9.3: {} phin@3.7.1: @@ -32550,6 +32853,8 @@ snapshots: transitivePeerDependencies: - supports-color + vite-plugin-route-builder@0.3.0: {} + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.20.3)(yaml@2.8.0)): dependencies: debug: 4.4.1(supports-color@8.1.1) @@ -33079,12 +33384,18 @@ snapshots: dependencies: zod: 3.25.75 + zod-to-json-schema@3.24.5(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod-validation-error@3.5.2(zod@3.25.75): dependencies: zod: 3.25.75 zod@3.25.75: {} + zod@3.25.76: {} + zustand@5.0.6(@types/react@19.1.8)(immer@10.1.1(patch_hash=594c60b929bc0a3b56576f1a1787da1aec2a1fba51e3c21ec09c0ed38280af6c))(react@19.0.0)(use-sync-external-store@1.5.0(react@19.0.0)): optionalDependencies: '@types/react': 19.1.8 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 17a57a945..64d294f4b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -53,3 +53,4 @@ overrides: catalog: typescript: "5.8.3" + "@follow-app/client-sdk": "0.3.22"