fix: extract useConversations hook to dedicated context file (#82)
* fix: extract useConversations hook to dedicated context file Move the conversations context and hook from the layout file to src/contexts/conversations-context.tsx. Next.js layouts cannot export hooks or types - only the default component export is allowed. * fix(build): prevent esbuild from bundling memory-provider The memory-provider module imports better-sqlite3, a native Node.js module that cannot run in Cloudflare Workers. This causes OpenNext build failures when esbuild tries to resolve the module. Changes: - Use dynamically constructed import path to prevent static analysis - Remove memory provider from getServerDb (never used on Cloudflare) - Add memory-provider to serverExternalPackages in next.config.ts * fix(lint): rename module variable to loadedModule The variable name 'module' is reserved in Next.js and causes ESLint error @next/next/no-assign-module-variable. --------- Co-authored-by: Nicholai <nicholaivogelfilms@gmail.com>
This commit is contained in:
parent
40fdf48cbf
commit
d5a28ee709
@ -17,7 +17,8 @@ const nextConfig: NextConfig = {
|
||||
],
|
||||
},
|
||||
// Node.js native modules that should not be bundled for edge/browser
|
||||
serverExternalPackages: ["better-sqlite3"],
|
||||
// memory-provider imports better-sqlite3 which cannot run in Cloudflare Workers
|
||||
serverExternalPackages: ["better-sqlite3", "./src/db/provider/memory-provider"],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@ -1,86 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
type ThreadMessage = {
|
||||
readonly id: string
|
||||
readonly channelId: string
|
||||
readonly threadId: string | null
|
||||
readonly content: string
|
||||
readonly contentHtml: string | null
|
||||
readonly editedAt: string | null
|
||||
readonly deletedAt: string | null
|
||||
readonly isPinned: boolean
|
||||
readonly replyCount: number
|
||||
readonly lastReplyAt: string | null
|
||||
readonly createdAt: string
|
||||
readonly user: {
|
||||
readonly id: string
|
||||
readonly displayName: string | null
|
||||
readonly email: string
|
||||
readonly avatarUrl: string | null
|
||||
} | null
|
||||
}
|
||||
|
||||
type ConversationsContextType = {
|
||||
readonly threadOpen: boolean
|
||||
readonly threadMessageId: string | null
|
||||
readonly threadParentMessage: ThreadMessage | null
|
||||
readonly openThread: (messageId: string, parentMessage: ThreadMessage) => void
|
||||
readonly closeThread: () => void
|
||||
}
|
||||
|
||||
const ConversationsContext = React.createContext<ConversationsContextType | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
export function useConversations() {
|
||||
const context = React.useContext(ConversationsContext)
|
||||
if (!context) {
|
||||
throw new Error("useConversations must be used within ConversationsProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export type { ThreadMessage }
|
||||
import { ConversationsProvider } from "@/contexts/conversations-context"
|
||||
|
||||
export default function ConversationsLayout({
|
||||
children,
|
||||
}: {
|
||||
readonly children: React.ReactNode
|
||||
}) {
|
||||
const [threadOpen, setThreadOpen] = React.useState(false)
|
||||
const [threadMessageId, setThreadMessageId] = React.useState<string | null>(null)
|
||||
const [threadParentMessage, setThreadParentMessage] = React.useState<ThreadMessage | null>(null)
|
||||
|
||||
const openThread = React.useCallback((messageId: string, parentMessage: ThreadMessage) => {
|
||||
setThreadMessageId(messageId)
|
||||
setThreadParentMessage(parentMessage)
|
||||
setThreadOpen(true)
|
||||
}, [])
|
||||
|
||||
const closeThread = React.useCallback(() => {
|
||||
setThreadOpen(false)
|
||||
setThreadMessageId(null)
|
||||
setThreadParentMessage(null)
|
||||
}, [])
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
threadOpen,
|
||||
threadMessageId,
|
||||
threadParentMessage,
|
||||
openThread,
|
||||
closeThread,
|
||||
}),
|
||||
[threadOpen, threadMessageId, threadParentMessage, openThread, closeThread]
|
||||
)
|
||||
|
||||
return (
|
||||
<ConversationsContext.Provider value={value}>
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
</ConversationsContext.Provider>
|
||||
)
|
||||
return <ConversationsProvider>{children}</ConversationsProvider>
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ import { Button } from "@/components/ui/button"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { MarkdownRenderer } from "@/components/ui/markdown-renderer"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useConversations } from "@/app/dashboard/conversations/layout"
|
||||
import { useConversations } from "@/contexts/conversations-context"
|
||||
import { editMessage, deleteMessage, addReaction } from "@/app/actions/chat-messages"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
|
||||
@ -6,8 +6,7 @@ import { Button } from "@/components/ui/button"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useConversations } from "@/app/dashboard/conversations/layout"
|
||||
import type { ThreadMessage } from "@/app/dashboard/conversations/layout"
|
||||
import { useConversations, type ThreadMessage } from "@/contexts/conversations-context"
|
||||
import { getThreadMessages } from "@/app/actions/chat-messages"
|
||||
import { MessageItem } from "./message-item"
|
||||
import { MessageComposer } from "./message-composer"
|
||||
|
||||
84
src/contexts/conversations-context.tsx
Normal file
84
src/contexts/conversations-context.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
export type ThreadMessage = {
|
||||
readonly id: string
|
||||
readonly channelId: string
|
||||
readonly threadId: string | null
|
||||
readonly content: string
|
||||
readonly contentHtml: string | null
|
||||
readonly editedAt: string | null
|
||||
readonly deletedAt: string | null
|
||||
readonly isPinned: boolean
|
||||
readonly replyCount: number
|
||||
readonly lastReplyAt: string | null
|
||||
readonly createdAt: string
|
||||
readonly user: {
|
||||
readonly id: string
|
||||
readonly displayName: string | null
|
||||
readonly email: string
|
||||
readonly avatarUrl: string | null
|
||||
} | null
|
||||
}
|
||||
|
||||
type ConversationsContextType = {
|
||||
readonly threadOpen: boolean
|
||||
readonly threadMessageId: string | null
|
||||
readonly threadParentMessage: ThreadMessage | null
|
||||
readonly openThread: (messageId: string, parentMessage: ThreadMessage) => void
|
||||
readonly closeThread: () => void
|
||||
}
|
||||
|
||||
const ConversationsContext = React.createContext<ConversationsContextType | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
export function useConversations() {
|
||||
const context = React.useContext(ConversationsContext)
|
||||
if (!context) {
|
||||
throw new Error("useConversations must be used within ConversationsProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export function ConversationsProvider({
|
||||
children,
|
||||
}: {
|
||||
readonly children: React.ReactNode
|
||||
}) {
|
||||
const [threadOpen, setThreadOpen] = React.useState(false)
|
||||
const [threadMessageId, setThreadMessageId] = React.useState<string | null>(null)
|
||||
const [threadParentMessage, setThreadParentMessage] = React.useState<ThreadMessage | null>(null)
|
||||
|
||||
const openThread = React.useCallback((messageId: string, parentMessage: ThreadMessage) => {
|
||||
setThreadMessageId(messageId)
|
||||
setThreadParentMessage(parentMessage)
|
||||
setThreadOpen(true)
|
||||
}, [])
|
||||
|
||||
const closeThread = React.useCallback(() => {
|
||||
setThreadOpen(false)
|
||||
setThreadMessageId(null)
|
||||
setThreadParentMessage(null)
|
||||
}, [])
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
threadOpen,
|
||||
threadMessageId,
|
||||
threadParentMessage,
|
||||
openThread,
|
||||
closeThread,
|
||||
}),
|
||||
[threadOpen, threadMessageId, threadParentMessage, openThread, closeThread]
|
||||
)
|
||||
|
||||
return (
|
||||
<ConversationsContext.Provider value={value}>
|
||||
<div className="flex min-h-0 flex-1 overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
</ConversationsContext.Provider>
|
||||
)
|
||||
}
|
||||
@ -55,6 +55,23 @@ export interface DatabaseProviderProps {
|
||||
}
|
||||
}
|
||||
|
||||
// Lazy-loaded memory provider factory - only loaded when actually needed
|
||||
// This avoids bundling better-sqlite3 (a native Node.js module) in
|
||||
// environments like Cloudflare Workers where it can't run
|
||||
let createMemoryProviderFn: typeof import("./memory-provider")["createMemoryProvider"] | null = null
|
||||
|
||||
async function getMemoryProvider(config?: MemoryProviderConfig): Promise<DatabaseProvider> {
|
||||
if (!createMemoryProviderFn) {
|
||||
// Construct the path dynamically to prevent static analysis by bundlers
|
||||
// The path segments are concatenated at runtime
|
||||
const providerDir = "."
|
||||
const providerFile = "memory-provider"
|
||||
const loadedModule = await import(/* webpackIgnore: true */ `${providerDir}/${providerFile}`)
|
||||
createMemoryProviderFn = loadedModule.createMemoryProvider
|
||||
}
|
||||
return createMemoryProviderFn!(config)
|
||||
}
|
||||
|
||||
export function DatabaseProvider({
|
||||
children,
|
||||
forcePlatform,
|
||||
@ -91,11 +108,7 @@ export function DatabaseProvider({
|
||||
|
||||
case "memory":
|
||||
default: {
|
||||
// Dynamic import to avoid bundling better-sqlite3 in browser
|
||||
const { createMemoryProvider } = await import(
|
||||
/* webpackIgnore: true */ "./memory-provider"
|
||||
)
|
||||
newProvider = createMemoryProvider(config?.memory)
|
||||
newProvider = await getMemoryProvider(config?.memory)
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -164,12 +177,15 @@ export async function getServerDb(): Promise<DrizzleDB> {
|
||||
|
||||
case "memory":
|
||||
default: {
|
||||
// Dynamic import to avoid bundling better-sqlite3 in browser
|
||||
const { createMemoryProvider } = await import(
|
||||
/* webpackIgnore: true */ "./memory-provider"
|
||||
// Memory provider is only for local development/testing.
|
||||
// On Cloudflare Workers, detectPlatform() returns "d1", so this
|
||||
// code path is never reached at runtime. However, bundlers like
|
||||
// esbuild still try to resolve the import. We throw an error here
|
||||
// since this code should never execute in production environments.
|
||||
throw new Error(
|
||||
"Memory provider not available in this environment. " +
|
||||
"Ensure you have a local SQLite setup or use the D1 provider."
|
||||
)
|
||||
const provider = createMemoryProvider()
|
||||
return provider.getDb()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user