Skip to content

Commit 25b0549

Browse files
committed
feat: cloudflare hub shorthands
1 parent cca1648 commit 25b0549

File tree

10 files changed

+236
-51
lines changed

10 files changed

+236
-51
lines changed

docs/content/docs/1.getting-started/3.deploy.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,34 @@ You can deploy your project on your own Cloudflare account, you need to create t
2424
You only need to create these resources if you have explicitly enabled them in the `hub` config.
2525
::
2626

27+
### Recommended: generate Wrangler bindings from `hub` config
28+
29+
If you deploy using Wrangler (CLI or CI), you can configure Cloudflare resource IDs directly in `nuxt.config.ts` and let Nitro generate the Wrangler config during build:
30+
31+
```ts [nuxt.config.ts]
32+
export default defineNuxtConfig({
33+
modules: ['@nuxthub/core'],
34+
hub: {
35+
db: 'd1:<database_id>',
36+
blob: 'r2:<bucket_name>',
37+
kv: 'kv:<namespace_id>',
38+
cache: 'kv:<namespace_id>'
39+
}
40+
})
41+
```
42+
43+
Then deploy the Nuxt output entrypoint (the Wrangler config is generated in `.output/`):
44+
45+
```bash
46+
npx wrangler deploy .output/server/index.mjs
47+
```
48+
49+
::note
50+
If you are using Cloudflare Workers Builds, set your deploy command to use the built output entrypoint (not `npx wrangler deploy` from the repo root).
51+
::
52+
53+
### Manual (Dashboard bindings / custom Wrangler config)
54+
2755
Then, create a [Cloudflare Workers project](https://dash.cloudflare.com/?to=/:account/workers-and-pages/create) and link your GitHub or GitLab repository.
2856

2957
Once your project is created, open the `Settings` tab and set:

playground/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22

33
.wrangler
44
.env*.local
5+
wrangler.toml
6+
wrangler.json

playground/nuxt.config.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,16 @@ export default defineNuxtConfig({
2020
compatibilityDate: '2025-12-11',
2121

2222
nitro: {
23-
// preset: 'cloudflare-module',
2423
experimental: {
2524
websocket: true
2625
}
2726
},
2827

2928
hub: {
30-
db: 'sqlite',
31-
blob: true,
32-
kv: true,
33-
cache: true
29+
db: 'd1:adc66aba-ce5f-40bc-bffc-bc80eb17e0b7',
30+
blob: 'r2:playground',
31+
kv: 'kv:919a41f11e5649deace923de93d8d9ec',
32+
cache: 'kv:c59a5cea9d0f44ad92b92e328c7e8b67'
3433
},
3534
hooks: {
3635
'hub:db:migrations:dirs': (dirs) => {

playground/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"scripts": {
66
"postinstall": "pnpm -C .. run dev:prepare",
77
"dev": "nuxi dev",
8-
"build": "nuxi build",
8+
"build": "NITRO_PRESET=cloudflare_module nuxi build && node ./scripts/sync-wrangler-config.mjs",
99
"preview": "nuxi preview"
1010
},
1111
"dependencies": {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import fs from 'node:fs'
2+
3+
const inputPath = new URL('../.output/server/wrangler.json', import.meta.url)
4+
const outputPath = new URL('../wrangler.json', import.meta.url)
5+
6+
if (!fs.existsSync(inputPath)) {
7+
process.exit(0)
8+
}
9+
10+
const config = JSON.parse(fs.readFileSync(inputPath, 'utf8'))
11+
12+
config.main = '.output/server/index.mjs'
13+
14+
if (config.assets && typeof config.assets === 'object') {
15+
config.assets.directory = '.output/public'
16+
}
17+
18+
fs.writeFileSync(outputPath, JSON.stringify(config, null, 2) + '\n', 'utf8')

playground/wrangler.jsonc

Lines changed: 0 additions & 37 deletions
This file was deleted.

src/cloudflare/wrangler.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { HubCloudflareConfig, HubConfig } from '../types/config'
2+
3+
type WranglerD1Database = { binding: string, database_id: string }
4+
type WranglerR2Bucket = { binding: string, bucket_name: string }
5+
type WranglerKVNamespace = { binding: string, id: string }
6+
7+
export type WranglerConfig = {
8+
d1_databases?: WranglerD1Database[]
9+
r2_buckets?: WranglerR2Bucket[]
10+
kv_namespaces?: WranglerKVNamespace[]
11+
[key: string]: unknown
12+
}
13+
14+
function parsePrefixed(value: unknown, prefix: string): string | undefined {
15+
if (typeof value !== 'string') return
16+
if (!value.startsWith(prefix)) return
17+
const rest = value.slice(prefix.length).trim()
18+
if (!rest) return
19+
return rest
20+
}
21+
22+
export function normalizeCloudflareShorthands(hub: HubConfig) {
23+
const cloudflare: HubCloudflareConfig = hub.cloudflare || {}
24+
25+
const d1DatabaseId = parsePrefixed(hub.db, 'd1:')
26+
if (d1DatabaseId) {
27+
cloudflare.d1DatabaseId = d1DatabaseId
28+
hub.db = 'sqlite'
29+
}
30+
31+
const r2BucketName = parsePrefixed(hub.blob, 'r2:')
32+
if (r2BucketName) {
33+
cloudflare.r2BucketName = r2BucketName
34+
hub.blob = true
35+
}
36+
37+
const kvNamespaceId = parsePrefixed(hub.kv, 'kv:')
38+
if (kvNamespaceId) {
39+
cloudflare.kvNamespaceId = kvNamespaceId
40+
hub.kv = true
41+
}
42+
43+
const cacheNamespaceId = parsePrefixed(hub.cache, 'kv:')
44+
if (cacheNamespaceId) {
45+
cloudflare.cacheNamespaceId = cacheNamespaceId
46+
hub.cache = true
47+
}
48+
49+
if (Object.keys(cloudflare).length > 0) {
50+
hub.cloudflare = cloudflare
51+
}
52+
}
53+
54+
function upsertByBinding<T extends { binding: string }>(existing: T[] | undefined, additions: T[]): T[] {
55+
const next = [...(existing || [])]
56+
for (const item of additions) {
57+
const index = next.findIndex(existingItem => existingItem.binding === item.binding)
58+
if (index === -1) next.push(item)
59+
else next[index] = item
60+
}
61+
return next
62+
}
63+
64+
export function applyWranglerBindingsFromHubCloudflare(wrangler: WranglerConfig, cloudflare: HubCloudflareConfig | undefined) {
65+
if (!cloudflare) return
66+
67+
if (cloudflare.d1DatabaseId) {
68+
wrangler.d1_databases = upsertByBinding(wrangler.d1_databases, [
69+
{ binding: 'DB', database_id: cloudflare.d1DatabaseId }
70+
])
71+
}
72+
if (cloudflare.r2BucketName) {
73+
wrangler.r2_buckets = upsertByBinding(wrangler.r2_buckets, [
74+
{ binding: 'BLOB', bucket_name: cloudflare.r2BucketName }
75+
])
76+
}
77+
78+
const kvAdditions: WranglerKVNamespace[] = []
79+
if (cloudflare.kvNamespaceId) kvAdditions.push({ binding: 'KV', id: cloudflare.kvNamespaceId })
80+
if (cloudflare.cacheNamespaceId) kvAdditions.push({ binding: 'CACHE', id: cloudflare.cacheNamespaceId })
81+
if (kvAdditions.length) {
82+
wrangler.kv_namespaces = upsertByBinding(wrangler.kv_namespaces, kvAdditions)
83+
}
84+
}

src/module.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { setupBlob } from './blob/setup'
1212
import type { ModuleOptions, HubConfig, ResolvedHubConfig } from '@nuxthub/core'
1313
import { addDevToolsCustomTabs } from './devtools'
1414
import type { NuxtModule } from '@nuxt/schema'
15+
import { applyWranglerBindingsFromHubCloudflare, normalizeCloudflareShorthands } from './cloudflare/wrangler'
1516

1617
const log = logger.withTag('nuxt:hub')
1718

@@ -45,6 +46,9 @@ export default defineNuxtModule<ModuleOptions>({
4546
db: false,
4647
kv: false
4748
}) as HubConfig
49+
50+
normalizeCloudflareShorthands(hub)
51+
4852
// resolve the hub directory
4953
hub.dir = await resolveFs(nuxt.options.rootDir, hub.dir)
5054

@@ -100,6 +104,10 @@ export default defineNuxtModule<ModuleOptions>({
100104
compatibility_flags: ['nodejs_compat']
101105
})
102106
}
107+
108+
// Auto-generate wrangler bindings from hub config (d1:/r2:/kv: shorthands)
109+
nuxt.options.nitro.cloudflare.wrangler ||= {}
110+
applyWranglerBindingsFromHubCloudflare(nuxt.options.nitro.cloudflare.wrangler, hub.cloudflare)
103111
}
104112

105113
// Add .data to .gitignore

src/types/config.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,41 @@ import type { S3DriverOptions } from '@nuxthub/core/blob/drivers/s3'
44
import type { VercelDriverOptions } from '@nuxthub/core/blob/drivers/vercel-blob'
55
import type { CloudflareDriverOptions } from '@nuxthub/core/blob/drivers/cloudflare-r2'
66

7+
export type CloudflareD1DatabaseId = `d1:${string}`
8+
export type CloudflareR2BucketName = `r2:${string}`
9+
export type CloudflareKVNamespaceId = `kv:${string}`
10+
11+
export interface HubCloudflareConfig {
12+
/**
13+
* Cloudflare D1 database_id used to generate wrangler bindings.
14+
* Filled automatically when using `hub.db: "d1:<database_id>"`.
15+
*/
16+
d1DatabaseId?: string
17+
/**
18+
* Cloudflare R2 bucket_name used to generate wrangler bindings.
19+
* Filled automatically when using `hub.blob: "r2:<bucket_name>"`.
20+
*/
21+
r2BucketName?: string
22+
/**
23+
* Cloudflare KV namespace id used to generate wrangler bindings for `hub.kv`.
24+
* Filled automatically when using `hub.kv: "kv:<namespace_id>"`.
25+
*/
26+
kvNamespaceId?: string
27+
/**
28+
* Cloudflare KV namespace id used to generate wrangler bindings for `hub.cache`.
29+
* Filled automatically when using `hub.cache: "kv:<namespace_id>"`.
30+
*/
31+
cacheNamespaceId?: string
32+
}
33+
734
export interface HubConfig {
8-
blob: boolean | BlobConfig
9-
cache: boolean | CacheConfig
10-
db: false | 'postgresql' | 'sqlite' | 'mysql' | DatabaseConfig
11-
kv: boolean | KVConfig
35+
blob: boolean | BlobConfig | CloudflareR2BucketName
36+
cache: boolean | CacheConfig | CloudflareKVNamespaceId
37+
db: false | 'postgresql' | 'sqlite' | 'mysql' | DatabaseConfig | CloudflareD1DatabaseId
38+
kv: boolean | KVConfig | CloudflareKVNamespaceId
1239
dir: string
1340
hosting: string
41+
cloudflare?: HubCloudflareConfig
1442
}
1543

1644
export interface ResolvedHubConfig extends HubConfig {
@@ -29,35 +57,39 @@ export interface ModuleOptions {
2957
/**
3058
* Set `true` to enable blob storage with auto-configuration.
3159
* Or provide a BlobConfig object with driver and connection details.
60+
* Or provide a Cloudflare shorthand string like `"r2:<bucket_name>"` to auto-generate wrangler bindings.
3261
*
3362
* @default false
3463
* @see https://hub.nuxt.com/docs/features/blob
3564
*/
36-
blob?: boolean | BlobConfig
65+
blob?: boolean | BlobConfig | CloudflareR2BucketName
3766
/**
3867
* Set `true` to enable caching for the project with auto-configuration.
3968
* Or provide a CacheConfig object with driver and connection details.
69+
* Or provide a Cloudflare shorthand string like `"kv:<namespace_id>"` to auto-generate wrangler bindings.
4070
*
4171
* @default false
4272
* @see https://hub.nuxt.com/docs/features/cache
4373
*/
44-
cache?: boolean | CacheConfig
74+
cache?: boolean | CacheConfig | CloudflareKVNamespaceId
4575
/**
4676
* Set to `'postgresql'`, `'sqlite'`, or `'mysql'` to use a specific database dialect with a zero-config development database.
4777
* Or provide a DatabaseConfig object with dialect and connection details.
78+
* Or provide a Cloudflare shorthand string like `"d1:<database_id>"` to use SQLite locally and auto-generate wrangler bindings.
4879
*
4980
* @default false
5081
* @see https://hub.nuxt.com/docs/features/database
5182
*/
52-
db?: 'postgresql' | 'sqlite' | 'mysql' | DatabaseConfig | false
83+
db?: 'postgresql' | 'sqlite' | 'mysql' | DatabaseConfig | CloudflareD1DatabaseId | false
5384
/**
5485
* Set `true` to enable the key-value storage with auto-configuration.
5586
* Or provide a KVConfig object with driver and connection details.
87+
* Or provide a Cloudflare shorthand string like `"kv:<namespace_id>"` to auto-generate wrangler bindings.
5688
*
5789
* @default false
5890
* @see https://hub.nuxt.com/docs/features/kv
5991
*/
60-
kv?: boolean | KVConfig
92+
kv?: boolean | KVConfig | CloudflareKVNamespaceId
6193
/**
6294
* The directory used for storage (database, kv, etc.) during local development.
6395
* @default '.data'
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { describe, it, expect } from 'vitest'
2+
import type { HubConfig } from '../src/types'
3+
import { applyWranglerBindingsFromHubCloudflare, normalizeCloudflareShorthands } from '../src/cloudflare/wrangler'
4+
5+
describe('cloudflare wrangler bindings', () => {
6+
it('normalizes hub shorthands into hub.cloudflare + updates hub flags', () => {
7+
const hub: HubConfig = {
8+
hosting: 'cloudflare_module',
9+
dir: '.data',
10+
db: 'd1:db-id',
11+
blob: 'r2:bucket-name',
12+
kv: 'kv:kv-id',
13+
cache: 'kv:cache-id'
14+
// ignored by this test
15+
} as any
16+
17+
normalizeCloudflareShorthands(hub)
18+
19+
expect(hub.db).toBe('sqlite')
20+
expect(hub.blob).toBe(true)
21+
expect(hub.kv).toBe(true)
22+
expect(hub.cache).toBe(true)
23+
expect(hub.cloudflare).toEqual({
24+
d1DatabaseId: 'db-id',
25+
r2BucketName: 'bucket-name',
26+
kvNamespaceId: 'kv-id',
27+
cacheNamespaceId: 'cache-id'
28+
})
29+
})
30+
31+
it('applies wrangler bindings without clobbering other bindings', () => {
32+
const wrangler = {
33+
kv_namespaces: [{ binding: 'OTHER', id: 'x' }]
34+
} as any
35+
36+
applyWranglerBindingsFromHubCloudflare(wrangler, {
37+
d1DatabaseId: 'db-id',
38+
r2BucketName: 'bucket-name',
39+
kvNamespaceId: 'kv-id',
40+
cacheNamespaceId: 'cache-id'
41+
})
42+
43+
expect(wrangler.d1_databases).toEqual([{ binding: 'DB', database_id: 'db-id' }])
44+
expect(wrangler.r2_buckets).toEqual([{ binding: 'BLOB', bucket_name: 'bucket-name' }])
45+
expect(wrangler.kv_namespaces).toEqual([
46+
{ binding: 'OTHER', id: 'x' },
47+
{ binding: 'KV', id: 'kv-id' },
48+
{ binding: 'CACHE', id: 'cache-id' }
49+
])
50+
})
51+
})

0 commit comments

Comments
 (0)