Skip to content

Commit 7de9c86

Browse files
committed
refactor: split feature logic
1 parent 057acb4 commit 7de9c86

File tree

12 files changed

+620
-602
lines changed

12 files changed

+620
-602
lines changed

src/features.ts

Lines changed: 10 additions & 351 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
1-
import { mkdir, writeFile } from 'node:fs/promises'
21
import type { Nuxt } from '@nuxt/schema'
3-
import { join } from 'pathe'
4-
import { logger, addImportsDir, addServerImportsDir, addServerScanDir, createResolver, addTypeTemplate } from '@nuxt/kit'
5-
import { defu } from 'defu'
6-
import { addDevToolsCustomTabs } from './utils/devtools'
7-
import { copyDatabaseMigrationsToHubDir, copyDatabaseQueriesToHubDir } from './runtime/database/server/utils/migrations/helpers'
8-
import type { ConnectorName } from 'db0'
9-
import type { NitroOptions } from 'nitropack'
10-
import { ensureDependencyInstalled } from 'nypm'
2+
import { logger } from '@nuxt/kit'
113

124
const log = logger.withTag('nuxt:hub')
13-
const { resolve } = createResolver(import.meta.url)
145

15-
function logWhenReady(nuxt: Nuxt, message: string, type: 'info' | 'warn' | 'error' = 'info') {
6+
export { setupAI } from './features/ai'
7+
export { setupBase } from './features/base'
8+
export { setupBlob } from './features/blob'
9+
export { setupCache } from './features/cache'
10+
export { setupDatabase } from './features/database'
11+
export { setupKV } from './features/kv'
12+
export { setupOpenAPI } from './features/openapi'
13+
14+
export function logWhenReady(nuxt: Nuxt, message: string, type: 'info' | 'warn' | 'error' = 'info') {
1615
nuxt.hooks.hookOnce('modules:done', () => {
1716
log[type](message)
1817
})
1918
}
2019

2120
export interface HubConfig {
22-
version?: string
23-
2421
ai?: 'vercel' | 'cloudflare'
2522
blob?: boolean
2623
cache?: boolean
@@ -32,341 +29,3 @@ export interface HubConfig {
3229
databaseQueriesPaths?: string[]
3330
applyDatabaseMigrationsDuringBuild?: boolean
3431
}
35-
36-
export async function setupBase(nuxt: Nuxt, hub: HubConfig) {
37-
// Create the hub.dir directory
38-
hub.dir = join(nuxt.options.rootDir, hub.dir!)
39-
try {
40-
await mkdir(hub.dir, { recursive: true })
41-
} catch (e: any) {
42-
if (e.errno === -17) {
43-
// File already exists
44-
} else {
45-
throw e
46-
}
47-
}
48-
49-
// Add custom tabs to Nuxt DevTools
50-
if (nuxt.options.dev) {
51-
addDevToolsCustomTabs(nuxt, hub)
52-
}
53-
54-
// Remove trailing slash for prerender routes
55-
nuxt.options.nitro.prerender ||= {}
56-
nuxt.options.nitro.prerender.autoSubfolderIndex ||= false
57-
}
58-
59-
export async function setupAI(nuxt: Nuxt, hub: HubConfig) {
60-
const providerName = hub.ai === 'vercel' ? 'Vercel AI Gateway' : 'Workers AI Provider'
61-
62-
if (hub.ai === 'vercel') {
63-
await Promise.all([
64-
ensureDependencyInstalled('@ai-sdk/gateway')
65-
])
66-
} else if (hub.ai === 'cloudflare') {
67-
await Promise.all([
68-
ensureDependencyInstalled('workers-ai-provider')
69-
])
70-
} else {
71-
return logWhenReady(nuxt, `\`${hub.ai}\` is not a supported AI provider. Set \`hub.ai\` to \`'vercel'\` or \`'cloudflare'\` in your \`nuxt.config.ts\`. Learn more at https://hub.nuxt.com/docs/features/ai.`, 'error')
72-
}
73-
74-
// Used for typing hubAI() with the correct provider
75-
addTypeTemplate({
76-
filename: 'types/nuxthub-ai.d.ts',
77-
getContents: () => `export type NuxtHubAIProvider = ${JSON.stringify(hub.ai)}
78-
`
79-
})
80-
81-
if (hub.ai === 'cloudflare') {
82-
const isCloudflareRuntime = nuxt.options.nitro.preset?.includes('cloudflare')
83-
const isAiBindingSet = !!(process.env.AI as { runtime: string } | undefined)?.runtime
84-
85-
if (isCloudflareRuntime && !isAiBindingSet) {
86-
return logWhenReady(nuxt, 'Ensure a `AI` binding is set in your Cloudflare Workers configuration', 'error')
87-
}
88-
89-
if (!process.env.CLOUDFLARE_ACCOUNT_ID || !process.env.CLOUDFLARE_API_KEY) {
90-
return logWhenReady(nuxt, `Set \`CLOUDFLARE_ACCOUNT_ID\` and \`CLOUDFLARE_API_KEY\` environment variables to enable \`hubAI()\` with ${providerName}`, 'error')
91-
}
92-
} else if (hub.ai === 'vercel') {
93-
const isMissingEnvVars = !process.env.AI_GATEWAY_API_KEY && !process.env.VERCEL_OIDC_TOKEN
94-
if (isMissingEnvVars && nuxt.options.dev) {
95-
return logWhenReady(nuxt, `Missing \`AI_GATEWAY_API_KEY\` environment variable to enable \`hubAI()\` with ${providerName}\nCreate an AI Gateway API key at \`${encodeURI('https://vercel.com/d?to=/[team]/~/ai/api-keys&title=Go+to+AI+Gateway')}\` or run \`npx vercel env pull .env\` to pull the environment variables.`, 'error')
96-
} else if (isMissingEnvVars) {
97-
return logWhenReady(nuxt, `Set \`AI_GATEWAY_API_KEY\` environment variable to enable \`hubAI()\` with ${providerName}\nCreate an AI Gateway API key at \`${encodeURI('https://vercel.com/d?to=/[team]/~/ai/api-keys&title=Go+to+AI+Gateway')}\``, 'error')
98-
}
99-
}
100-
101-
// Add Server scanning
102-
addServerScanDir(resolve('./runtime/ai/server'))
103-
addServerImportsDir(resolve('./runtime/ai/server/utils'))
104-
105-
logWhenReady(nuxt, `\`hubAI()\` configured with \`${providerName}\``)
106-
}
107-
108-
export function setupBlob(nuxt: Nuxt, hub: HubConfig) {
109-
// Configure dev storage
110-
nuxt.options.nitro.devStorage ||= {}
111-
nuxt.options.nitro.devStorage.blob = defu(nuxt.options.nitro.devStorage.blob, {
112-
driver: 'fs-lite',
113-
base: join(hub.dir!, 'blob')
114-
})
115-
116-
// Add Server scanning
117-
addServerScanDir(resolve('./runtime/blob/server'))
118-
addServerImportsDir(resolve('./runtime/blob/server/utils'))
119-
120-
// Add Composables
121-
addImportsDir(resolve('./runtime/blob/app/composables'))
122-
123-
if (nuxt.options.nitro.storage?.blob?.driver === 'vercel-blob') {
124-
nuxt.options.runtimeConfig.public.hub.blobProvider = 'vercel-blob'
125-
}
126-
127-
const driver = nuxt.options.dev ? nuxt.options.nitro.devStorage.blob.driver : nuxt.options.nitro.storage?.blob?.driver
128-
129-
logWhenReady(nuxt, `\`hubBlob()\` configured with \`${driver}\` driver`)
130-
}
131-
132-
export async function setupCache(nuxt: Nuxt, hub: HubConfig) {
133-
// Configure dev storage
134-
nuxt.options.nitro.devStorage ||= {}
135-
nuxt.options.nitro.devStorage.cache = defu(nuxt.options.nitro.devStorage.cache, {
136-
driver: 'fs-lite',
137-
base: join(hub.dir!, 'cache')
138-
})
139-
140-
// Add Server scanning
141-
addServerScanDir(resolve('./runtime/cache/server'))
142-
}
143-
144-
export async function setupDatabase(nuxt: Nuxt, hub: HubConfig) {
145-
// Configure dev storage
146-
if (typeof hub.database === 'string' && !['postgresql', 'sqlite', 'mysql'].includes(hub.database)) {
147-
return logWhenReady(nuxt, `Unknown database dialect set in hub.database: ${hub.database}`, 'error')
148-
}
149-
150-
let dialect: string
151-
const productionDriver = nuxt.options.nitro.database?.db?.connector as ConnectorName
152-
const isDialectConfigured = typeof hub.database === 'string' && (['postgresql', 'sqlite', 'mysql'].includes(hub.database))
153-
if (isDialectConfigured) {
154-
dialect = hub.database as string
155-
} else {
156-
// Infer dialect from production database driver
157-
// Map connectors to dialects based on the mappings:
158-
// "postgresql" -> "postgresql", pglite
159-
// "sqlite" -> "better-sqlite3", bun-sqlite, bun, node-sqlite, sqlite3
160-
// "mysql" -> mysql2
161-
// "libsql" -> libsql-core, libsql-http, libsql-node, libsql-web
162-
if (productionDriver === 'postgresql' || productionDriver === 'pglite') {
163-
dialect = 'postgresql'
164-
} else if (['better-sqlite3', 'bun-sqlite', 'bun', 'node-sqlite', 'sqlite3'].includes(productionDriver)) {
165-
dialect = 'sqlite'
166-
} else if (productionDriver === 'mysql2') {
167-
dialect = 'mysql'
168-
} else if (['libsql-core', 'libsql-http', 'libsql-node', 'libsql-web'].includes(productionDriver)) {
169-
// NOTE: libSQL implements additional functionality beyond sqlite, but users can manually configure a libsql database within Nitro if they require them
170-
dialect = 'sqlite' // libsql is SQLite-compatible
171-
} else {
172-
return logWhenReady(nuxt, 'Please specify a database dialect in `hub.database: \'<dialect>\'` or configure `nitro.database.db` within `nuxt.config.ts`. Learn more at https://hub.nuxt.com/docs/features/database.', 'error')
173-
}
174-
}
175-
176-
// Check if the configured dialect matches the production driver
177-
if (isDialectConfigured && productionDriver) {
178-
const dialectMatchesDriver = (
179-
(dialect === 'postgresql' && (productionDriver === 'postgresql' || productionDriver === 'pglite'))
180-
|| (dialect === 'sqlite' && ['better-sqlite3', 'bun-sqlite', 'bun', 'node-sqlite', 'sqlite3', 'libsql-core', 'libsql-http', 'libsql-node', 'libsql-web'].includes(productionDriver))
181-
|| (dialect === 'mysql' && productionDriver === 'mysql2')
182-
)
183-
184-
if (!dialectMatchesDriver) {
185-
// Infer the dialect from the production driver for the error message
186-
let inferredDialect: string
187-
if (productionDriver === 'postgresql' || productionDriver === 'pglite') {
188-
inferredDialect = 'postgresql'
189-
} else if (['better-sqlite3', 'bun-sqlite', 'bun', 'node-sqlite', 'sqlite3', 'libsql-core', 'libsql-http', 'libsql-node', 'libsql-web'].includes(productionDriver)) {
190-
inferredDialect = 'sqlite'
191-
} else if (productionDriver === 'mysql2') {
192-
inferredDialect = 'mysql'
193-
} else {
194-
inferredDialect = 'unknown'
195-
}
196-
197-
logWhenReady(nuxt, `Database dialect mismatch: \`hub.database\` is set to \`${dialect}\` but \`nitro.database.db\` is \`${inferredDialect}\` (\`${productionDriver}\`). Set \`hub.database\` to \`true\` or \`'${inferredDialect}'\` in your \`nuxt.config.ts\`.`, 'warn')
198-
}
199-
}
200-
201-
// Configure dev database based on dialect
202-
let devDatabaseConfig: NitroOptions['database']['default']
203-
204-
if (dialect === 'postgresql') {
205-
if (process.env.POSTGRES_URL || process.env.POSTGRESQL_URL || process.env.DATABASE_URL) {
206-
// Use postgresql if env variable is set
207-
const setEnvVarName = process.env.POSTGRES_URL ? 'POSTGRES_URL' : process.env.POSTGRESQL_URL ? 'POSTGRESQL_URL' : 'DATABASE_URL'
208-
logWhenReady(nuxt, `Using \`PostgreSQL\` during local development using provided \`${setEnvVarName}\``)
209-
devDatabaseConfig = {
210-
connector: 'postgresql',
211-
options: {
212-
url: process.env.POSTGRES_URL || process.env.POSTGRESQL_URL || process.env.DATABASE_URL
213-
}
214-
}
215-
} else {
216-
// Use pglite if env variable not provided
217-
logWhenReady(nuxt, 'Using `PGlite` during local development')
218-
devDatabaseConfig = {
219-
connector: 'pglite',
220-
options: {
221-
dataDir: join(hub.dir!, 'database/pglite')
222-
}
223-
}
224-
}
225-
} else if (dialect === 'sqlite') {
226-
logWhenReady(nuxt, 'Using `SQLite` during local development')
227-
devDatabaseConfig = {
228-
connector: 'better-sqlite3',
229-
options: {
230-
path: join(hub.dir!, 'database/sqlite/db.sqlite3')
231-
}
232-
}
233-
} else if (dialect === 'mysql') {
234-
if (!nuxt.options.nitro.devDatabase?.db?.connector) {
235-
logWhenReady(nuxt, 'Zero-config `MySQL` database setup during local development is not supported yet. Please manually configure your development database in `nitro.devDatabase.db` in `nuxt.config.ts`. Learn more at https://hub.nuxt.com/docs/features/database.', 'warn')
236-
}
237-
}
238-
239-
nuxt.options.nitro.devDatabase ||= {}
240-
nuxt.options.nitro.devDatabase.db = defu(nuxt.options.nitro.devDatabase.db, devDatabaseConfig!) as NitroOptions['database']['default']
241-
242-
// Verify development database dependencies are installed
243-
const developmentDriver = nuxt.options.nitro.devDatabase?.db?.connector as ConnectorName
244-
if (developmentDriver === 'postgresql') {
245-
await ensureDependencyInstalled('pg')
246-
} else if (developmentDriver === 'pglite') {
247-
await ensureDependencyInstalled('@electric-sql/pglite')
248-
} else if (developmentDriver === 'mysql2') {
249-
await ensureDependencyInstalled('mysql2')
250-
} else if (developmentDriver === 'better-sqlite3') {
251-
await ensureDependencyInstalled('better-sqlite3')
252-
}
253-
254-
// Enable Nitro database
255-
nuxt.options.nitro.experimental ||= {}
256-
nuxt.options.nitro.experimental.database = true
257-
258-
// Add Server scanning
259-
addServerScanDir(resolve('./runtime/database/server'))
260-
addServerImportsDir(resolve('./runtime/database/server/utils'))
261-
262-
// Handle migrations
263-
nuxt.hook('modules:done', async () => {
264-
// Call hub:database:migrations:dirs hook
265-
await nuxt.callHook('hub:database:migrations:dirs', hub.databaseMigrationsDirs!)
266-
// Copy all migrations files to the hub.dir directory
267-
await copyDatabaseMigrationsToHubDir(hub)
268-
// Call hub:database:migrations:queries hook
269-
await nuxt.callHook('hub:database:queries:paths', hub.databaseQueriesPaths!)
270-
await copyDatabaseQueriesToHubDir(hub)
271-
})
272-
273-
// Setup Drizzle ORM
274-
let isDrizzleOrmInstalled = false
275-
try {
276-
require.resolve('drizzle-orm', { paths: [nuxt.options.rootDir] })
277-
isDrizzleOrmInstalled = true
278-
} catch {
279-
// Ignore
280-
}
281-
282-
if (isDrizzleOrmInstalled) {
283-
const connector = nuxt.options.nitro.devDatabase.db.connector as ConnectorName
284-
const dbConfig = nuxt.options.nitro.devDatabase.db.options
285-
286-
// @ts-expect-error not all connectors are supported
287-
const db0ToDrizzle: Record<ConnectorName, string> = {
288-
postgresql: 'node-postgres',
289-
pglite: 'pglite',
290-
mysql2: 'mysql2',
291-
planetscale: 'planetscale-serverless',
292-
'better-sqlite3': 'better-sqlite3',
293-
'bun-sqlite': 'bun-sqlite',
294-
bun: 'bun-sqlite',
295-
sqlite3: 'better-sqlite3',
296-
libsql: 'libsql/node',
297-
'libsql-core': 'libsql',
298-
'libsql-http': 'libsql/http',
299-
'libsql-node': 'libsql/node',
300-
'libsql-web': 'libsql/web',
301-
'cloudflare-d1': 'd1'
302-
// unsupported: sqlite & node-sqlite
303-
}
304-
305-
// node-postgres requires connectionString instead of url
306-
let connectionConfig = dbConfig
307-
if (connector === 'postgresql' && dbConfig?.url) {
308-
connectionConfig = { connectionString: dbConfig.url, ...dbConfig.options }
309-
}
310-
311-
let drizzleOrmContent = `import { drizzle } from 'drizzle-orm/${db0ToDrizzle[connector]}'
312-
import type { DrizzleConfig } from 'drizzle-orm'
313-
314-
export function hubDrizzle<TSchema extends Record<string, unknown> = Record<string, never>>(options?: DrizzleConfig<TSchema>) {
315-
return drizzle({
316-
...options,
317-
connection: ${JSON.stringify(connectionConfig)}
318-
})
319-
}`
320-
321-
if (connector === 'pglite') {
322-
drizzleOrmContent = `import { drizzle } from 'drizzle-orm/pg-proxy'
323-
import type { DrizzleConfig } from 'drizzle-orm'
324-
325-
export function hubDrizzle<TSchema extends Record<string, unknown> = Record<string, never>>(options?: DrizzleConfig<TSchema>) {
326-
return drizzle(async (sql, params, method) => {
327-
try {
328-
const rows = await $fetch<any[]>('/api/_hub/database/query', { method: 'POST', body: { sql, params, method } })
329-
return { rows }
330-
} catch (e: any) {
331-
console.error(e.response)
332-
return { rows: [] }
333-
}
334-
}, {
335-
...options,
336-
})
337-
}`
338-
}
339-
340-
// create hub directory in .nuxt if it doesn't exist
341-
const hubBuildDir = join(nuxt.options.buildDir, 'hub')
342-
await mkdir(hubBuildDir, { recursive: true })
343-
344-
const drizzleOrmPath = join(hubBuildDir, 'drizzle-orm.ts')
345-
await writeFile(drizzleOrmPath, drizzleOrmContent, 'utf-8')
346-
347-
nuxt.options.alias['#hub/drizzle-orm'] = drizzleOrmPath
348-
}
349-
}
350-
351-
export function setupKV(nuxt: Nuxt, hub: HubConfig) {
352-
// Configure dev storage
353-
nuxt.options.nitro.devStorage ||= {}
354-
nuxt.options.nitro.devStorage.kv = defu(nuxt.options.nitro.devStorage.kv, {
355-
driver: 'fs-lite',
356-
base: join(hub.dir!, 'kv')
357-
})
358-
359-
// Add Server scanning
360-
addServerScanDir(resolve('./runtime/kv/server'))
361-
addServerImportsDir(resolve('./runtime/kv/server/utils'))
362-
363-
const driver = nuxt.options.dev ? nuxt.options.nitro.devStorage.kv.driver : nuxt.options.nitro.storage?.kv?.driver
364-
365-
logWhenReady(nuxt, `\`hubKV()\` configured with \`${driver}\` driver`)
366-
}
367-
368-
export function setupOpenAPI(nuxt: Nuxt, _hub: HubConfig) {
369-
// Enable Nitro database
370-
nuxt.options.nitro.experimental ||= {}
371-
nuxt.options.nitro.experimental.openAPI ??= true
372-
}

0 commit comments

Comments
 (0)