Skip to content

Commit ff2fabe

Browse files
onmaxatinux
andauthored
feat: auto-generate wrangler bindings from hub config (#716)
Co-authored-by: Sébastien Chopin <[email protected]>
1 parent cec8c89 commit ff2fabe

File tree

10 files changed

+111
-14
lines changed

10 files changed

+111
-14
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,5 @@ test/fixtures/basic/.data
6060
test/fixtures/kv/.data
6161
test/fixtures/blob/.data
6262
test/fixtures/openapi/.data
63-
test/fixtures/cache/.data
63+
test/fixtures/cache/.data
64+
test/fixtures/wrangler/.data

src/blob/setup.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { defu } from 'defu'
33
import { addTypeTemplate, addServerImports, addImportsDir, logger, addTemplate } from '@nuxt/kit'
44

55
import type { Nuxt } from '@nuxt/schema'
6-
import type { HubConfig, ResolvedBlobConfig } from '@nuxthub/core'
7-
import { resolve, logWhenReady } from '../utils'
6+
import type { HubConfig, ResolvedBlobConfig, CloudflareR2BlobConfig } from '@nuxthub/core'
7+
import { resolve, logWhenReady, addWranglerBinding } from '../utils'
88

99
const log = logger.withTag('nuxt:hub')
1010

@@ -70,10 +70,14 @@ export function setupBlob(nuxt: Nuxt, hub: HubConfig, deps: Record<string, strin
7070

7171
const blobConfig = hub.blob as ResolvedBlobConfig
7272

73+
if (blobConfig.driver === 'cloudflare-r2' && blobConfig.bucketName) {
74+
addWranglerBinding(nuxt, 'r2_buckets', { binding: blobConfig.binding || 'BLOB', bucket_name: blobConfig.bucketName })
75+
}
76+
7377
// Add Composables
7478
addImportsDir(resolve('blob/runtime/app/composables'))
7579

76-
const { driver, ...driverOptions } = blobConfig
80+
const { driver, bucketName: _bucketName, ...driverOptions } = blobConfig as CloudflareR2BlobConfig
7781

7882
if (!supportedDrivers.includes(driver as any)) {
7983
log.error(`Unsupported blob driver: ${driver}. Supported drivers: ${supportedDrivers.join(', ')}`)

src/cache/setup.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { join } from 'pathe'
22
import { defu } from 'defu'
3-
import { logWhenReady } from '../utils'
3+
import { logWhenReady, addWranglerBinding } from '../utils'
44

55
import type { Nuxt } from '@nuxt/schema'
66
import type { HubConfig, CacheConfig, ResolvedCacheConfig } from '@nuxthub/core'
@@ -47,14 +47,19 @@ export async function setupCache(nuxt: Nuxt, hub: HubConfig, _deps: Record<strin
4747

4848
const cacheConfig = hub.cache as ResolvedCacheConfig
4949

50+
if (cacheConfig.driver === 'cloudflare-kv-binding' && cacheConfig.namespaceId) {
51+
addWranglerBinding(nuxt, 'kv_namespaces', { binding: cacheConfig.binding || 'CACHE', id: cacheConfig.namespaceId })
52+
}
53+
5054
// Configure storage
55+
const { namespaceId: _namespaceId, ...cacheStorageConfig } = cacheConfig
5156
nuxt.options.nitro.storage ||= {}
52-
nuxt.options.nitro.storage.cache = defu(nuxt.options.nitro.storage.cache, cacheConfig)
57+
nuxt.options.nitro.storage.cache = defu(nuxt.options.nitro.storage.cache, cacheStorageConfig)
5358

5459
// Also set devStorage for development mode (fs-lite driver)
5560
if (nuxt.options.dev) {
5661
nuxt.options.nitro.devStorage ||= {}
57-
nuxt.options.nitro.devStorage.cache = defu(nuxt.options.nitro.devStorage.cache, cacheConfig)
62+
nuxt.options.nitro.devStorage.cache = defu(nuxt.options.nitro.devStorage.cache, cacheStorageConfig)
5863
}
5964

6065
logWhenReady(nuxt, `\`hub:cache\` using \`${cacheConfig.driver.split('/').pop()}\` driver`)

src/db/setup.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { glob } from 'tinyglobby'
44
import { join, resolve as resolveFs, relative } from 'pathe'
55
import { defu } from 'defu'
66
import { addServerImports, addTemplate, addServerPlugin, addTypeTemplate, getLayerDirectories, updateTemplates, logger, addServerHandler } from '@nuxt/kit'
7-
import { resolve, resolvePath, logWhenReady } from '../utils'
7+
import { resolve, resolvePath, logWhenReady, addWranglerBinding } from '../utils'
88
import { copyDatabaseMigrationsToHubDir, copyDatabaseQueriesToHubDir, copyDatabaseAssets, applyBuildTimeMigrations, getDatabaseSchemaPathMetadata, buildDatabaseSchema } from './lib'
99
import { cloudflareHooks } from '../hosting/cloudflare'
1010

@@ -61,6 +61,11 @@ export async function resolveDatabaseConfig(nuxt: Nuxt, hub: HubConfig): Promise
6161
break
6262
}
6363
case 'postgresql': {
64+
// Cloudflare Hyperdrive with explicit hyperdriveId
65+
if (hub.hosting.includes('cloudflare') && config.connection?.hyperdriveId && !config.driver) {
66+
config.driver = 'postgres-js'
67+
break
68+
}
6469
config.connection = defu(config.connection, { url: process.env.POSTGRES_URL || process.env.POSTGRESQL_URL || process.env.DATABASE_URL || '' })
6570
if (config.driver && ['neon-http', 'postgres-js'].includes(config.driver) && !config.connection.url) {
6671
throw new Error(`\`${config.driver}\` driver requires \`DATABASE_URL\`, \`POSTGRES_URL\`, or \`POSTGRESQL_URL\` environment variable`)
@@ -76,6 +81,11 @@ export async function resolveDatabaseConfig(nuxt: Nuxt, hub: HubConfig): Promise
7681
break
7782
}
7883
case 'mysql': {
84+
// Cloudflare Hyperdrive with explicit hyperdriveId
85+
if (hub.hosting.includes('cloudflare') && config.connection?.hyperdriveId && !config.driver) {
86+
config.driver = 'mysql2'
87+
break
88+
}
7989
config.driver ||= 'mysql2'
8090
config.connection = defu(config.connection, { uri: process.env.MYSQL_URL || process.env.DATABASE_URL || '' })
8191
if (!config.connection.uri) {
@@ -97,10 +107,18 @@ export async function setupDatabase(nuxt: Nuxt, hub: HubConfig, deps: Record<str
97107
hub.db = await resolveDatabaseConfig(nuxt, hub)
98108
if (!hub.db) return
99109

100-
const { dialect, driver, migrationsDirs, queriesPaths } = hub.db as ResolvedDatabaseConfig
110+
const { dialect, driver, connection, migrationsDirs, queriesPaths } = hub.db as ResolvedDatabaseConfig
101111

102112
logWhenReady(nuxt, `\`hub:db\` using \`${dialect}\` database with \`${driver}\` driver`, 'info')
103113

114+
if (driver === 'd1' && connection?.databaseId) {
115+
addWranglerBinding(nuxt, 'd1_databases', { binding: 'DB', database_id: connection.databaseId })
116+
}
117+
if (['postgres-js', 'mysql2'].includes(driver) && connection?.hyperdriveId) {
118+
const binding = driver === 'postgres-js' ? 'POSTGRES' : 'MYSQL'
119+
addWranglerBinding(nuxt, 'hyperdrive', { binding, id: connection.hyperdriveId })
120+
}
121+
104122
// Verify development database dependencies are installed
105123
if (!deps['drizzle-orm'] || !deps['drizzle-kit']) {
106124
logWhenReady(nuxt, 'Please run `npx nypm i drizzle-orm drizzle-kit` to properly setup Drizzle ORM with NuxtHub.', 'error')

src/kv/setup.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { defu } from 'defu'
22
import { addTypeTemplate, addServerImports, addTemplate } from '@nuxt/kit'
3-
import { resolve, logWhenReady } from '../utils'
3+
import { resolve, logWhenReady, addWranglerBinding } from '../utils'
44

55
import type { Nuxt } from '@nuxt/schema'
66
import type { HubConfig, ResolvedKVConfig } from '@nuxthub/core'
@@ -71,6 +71,10 @@ export function setupKV(nuxt: Nuxt, hub: HubConfig, deps: Record<string, string>
7171

7272
const kvConfig = hub.kv as ResolvedKVConfig
7373

74+
if (kvConfig.driver === 'cloudflare-kv-binding' && kvConfig.namespaceId) {
75+
addWranglerBinding(nuxt, 'kv_namespaces', { binding: kvConfig.binding || 'KV', id: kvConfig.namespaceId })
76+
}
77+
7478
// Verify dependencies
7579
if (kvConfig.driver === 'upstash' && !deps['@upstash/redis']) {
7680
logWhenReady(nuxt, 'Please run `npx nypm i @upstash/redis` to use Upstash Redis KV storage', 'error')
@@ -83,10 +87,11 @@ export function setupKV(nuxt: Nuxt, hub: HubConfig, deps: Record<string, string>
8387
}
8488

8589
// Configure production storage
90+
const { namespaceId: _namespaceId, ...kvStorageConfig } = kvConfig
8691
nuxt.options.nitro.storage ||= {}
87-
nuxt.options.nitro.storage.kv = defu(nuxt.options.nitro.storage.kv, kvConfig)
92+
nuxt.options.nitro.storage.kv = defu(nuxt.options.nitro.storage.kv, kvStorageConfig)
8893

89-
const { driver, ...driverOptions } = kvConfig
94+
const { driver, ...driverOptions } = kvStorageConfig
9095
const template = addTemplate({
9196
filename: 'hub/kv.mjs',
9297
getContents: () => `import { createStorage } from "unstorage"

src/types/config.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,17 @@ export interface ModuleOptions {
7474
export type FSBlobConfig = { driver: 'fs' } & FSDriverOptions
7575
export type S3BlobConfig = { driver: 's3' } & S3DriverOptions
7676
export type VercelBlobConfig = { driver: 'vercel-blob' } & VercelDriverOptions
77-
export type CloudflareR2BlobConfig = { driver: 'cloudflare-r2' } & CloudflareDriverOptions
77+
export type CloudflareR2BlobConfig = { driver: 'cloudflare-r2', bucketName?: string } & CloudflareDriverOptions
7878

7979
export type BlobConfig = boolean | FSBlobConfig | S3BlobConfig | VercelBlobConfig | CloudflareR2BlobConfig
8080
export type ResolvedBlobConfig = FSBlobConfig | S3BlobConfig | VercelBlobConfig | CloudflareR2BlobConfig
8181

8282
export type CacheConfig = {
8383
driver?: BuiltinDriverName
84+
/**
85+
* Cloudflare KV namespace ID for auto-generating wrangler bindings
86+
*/
87+
namespaceId?: string
8488
[key: string]: any
8589
}
8690
export type ResolvedCacheConfig = CacheConfig & {
@@ -89,6 +93,10 @@ export type ResolvedCacheConfig = CacheConfig & {
8993

9094
export type KVConfig = {
9195
driver?: BuiltinDriverName
96+
/**
97+
* Cloudflare KV namespace ID for auto-generating wrangler bindings
98+
*/
99+
namespaceId?: string
92100
[key: string]: any
93101
}
94102

@@ -138,9 +146,13 @@ type DatabaseConnection = {
138146
*/
139147
apiToken?: string
140148
/**
141-
* Cloudflare D1 Database ID (for D1 HTTP driver)
149+
* Cloudflare D1 Database ID (for D1 driver and D1 HTTP driver)
142150
*/
143151
databaseId?: string
152+
/**
153+
* Cloudflare Hyperdrive ID for auto-generating wrangler bindings (PostgreSQL/MySQL)
154+
*/
155+
hyperdriveId?: string
144156
/**
145157
* Additional connection options
146158
*/

src/utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,15 @@ export function logWhenReady(nuxt: Nuxt, message: string, type: 'info' | 'warn'
1717
}
1818

1919
export const { resolve, resolvePath } = createResolver(import.meta.url)
20+
21+
type WranglerBindingType = 'd1_databases' | 'r2_buckets' | 'kv_namespaces' | 'hyperdrive'
22+
23+
export function addWranglerBinding(nuxt: Nuxt, type: WranglerBindingType, binding: { binding: string, [key: string]: any }) {
24+
nuxt.options.nitro.cloudflare ||= {}
25+
nuxt.options.nitro.cloudflare.wrangler ||= {}
26+
nuxt.options.nitro.cloudflare.wrangler[type] ||= []
27+
const existing = nuxt.options.nitro.cloudflare.wrangler[type] as Array<{ binding: string }>
28+
if (!existing.some(b => b.binding === binding.binding)) {
29+
(existing as any[]).push(binding)
30+
}
31+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineNuxtConfig } from 'nuxt/config'
2+
3+
export default defineNuxtConfig({
4+
extends: ['../basic'],
5+
modules: ['../../../src/module'],
6+
hub: {
7+
blob: { driver: 'cloudflare-r2', bucketName: 'test-bucket', binding: 'BLOB' },
8+
kv: { driver: 'cloudflare-kv-binding', namespaceId: 'test-kv-id', binding: 'KV' },
9+
cache: { driver: 'cloudflare-kv-binding', namespaceId: 'test-cache-id', binding: 'CACHE' },
10+
db: { dialect: 'sqlite', driver: 'd1', connection: { databaseId: 'test-db-id' } }
11+
}
12+
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "private": true, "name": "hub-wrangler-test", "type": "module" }

test/wrangler.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { fileURLToPath } from 'node:url'
2+
import { describe, expect, it } from 'vitest'
3+
import { setup, useTestContext } from '@nuxt/test-utils'
4+
import { addWranglerBinding } from '../src/utils'
5+
6+
describe('addWranglerBinding', () => {
7+
it('should not add duplicate bindings', () => {
8+
const nuxt = { options: { nitro: {} } } as any
9+
addWranglerBinding(nuxt, 'kv_namespaces', { binding: 'KV', id: 'first' })
10+
addWranglerBinding(nuxt, 'kv_namespaces', { binding: 'KV', id: 'second' })
11+
expect(nuxt.options.nitro.cloudflare.wrangler.kv_namespaces).toHaveLength(1)
12+
})
13+
})
14+
15+
describe('wrangler bindings e2e', async () => {
16+
await setup({ rootDir: fileURLToPath(new URL('./fixtures/wrangler', import.meta.url)), dev: true })
17+
18+
it('should auto-generate all wrangler bindings from hub config', () => {
19+
const { nuxt } = useTestContext()
20+
const wrangler = nuxt?.options.nitro.cloudflare?.wrangler
21+
22+
expect(wrangler?.r2_buckets).toContainEqual({ binding: 'BLOB', bucket_name: 'test-bucket' })
23+
expect(wrangler?.kv_namespaces).toContainEqual({ binding: 'KV', id: 'test-kv-id' })
24+
expect(wrangler?.kv_namespaces).toContainEqual({ binding: 'CACHE', id: 'test-cache-id' })
25+
expect(wrangler?.d1_databases).toContainEqual({ binding: 'DB', database_id: 'test-db-id' })
26+
})
27+
})

0 commit comments

Comments
 (0)