Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/content/docs/1.getting-started/3.deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ The following table shows how NuxtHub configuration options map to Wrangler conf
| `hub.cache.binding` | `kv_namespaces[].binding` | `CACHE` (default) |
| `hub.blob.bucketName` | `r2_buckets[].bucket_name` | From config |
| `hub.blob.binding` | `r2_buckets[].binding` | `BLOB` (default) |
| `hub.blob.jurisdiction` | `r2_buckets[].jurisdiction` | From config |

#### Environment Resolution

Expand Down
4 changes: 3 additions & 1 deletion docs/content/docs/3.blob/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ By default, if NuxtHub cannot detect a driver, files are stored locally in the `
hub: {
blob: {
driver: 'cloudflare-r2',
bucketName: '<bucket-name>'
bucketName: '<bucket-name>',
// Optional: for jurisdictional R2 buckets (e.g. EU)
// jurisdiction: 'eu'
}
}
})
Expand Down
8 changes: 6 additions & 2 deletions src/blob/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,17 @@ export async function setupBlob(nuxt: Nuxt, hub: HubConfig, deps: Record<string,
const blobConfig = hub.blob as ResolvedBlobConfig

if (blobConfig.driver === 'cloudflare-r2' && blobConfig.bucketName) {
addWranglerBinding(nuxt, 'r2_buckets', { binding: blobConfig.binding || 'BLOB', bucket_name: blobConfig.bucketName })
addWranglerBinding(nuxt, 'r2_buckets', {
binding: blobConfig.binding || 'BLOB',
bucket_name: blobConfig.bucketName,
...(blobConfig.jurisdiction ? { jurisdiction: blobConfig.jurisdiction } : {})
})
}

// Add Composables
addImportsDir(resolve('blob/runtime/app/composables'))

const { driver, bucketName: _bucketName, ...driverOptions } = blobConfig as CloudflareR2BlobConfig
const { driver, bucketName: _bucketName, jurisdiction: _jurisdiction, ...driverOptions } = blobConfig as CloudflareR2BlobConfig

if (!supportedDrivers.includes(driver as any)) {
log.error(`Unsupported blob driver: ${driver}. Supported drivers: ${supportedDrivers.join(', ')}`)
Expand Down
13 changes: 12 additions & 1 deletion src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,18 @@ export interface ModuleOptions {
export type FSBlobConfig = { driver: 'fs' } & FSDriverOptions
export type S3BlobConfig = { driver: 's3' } & S3DriverOptions
export type VercelBlobConfig = { driver: 'vercel-blob' } & VercelDriverOptions
export type CloudflareR2BlobConfig = { driver: 'cloudflare-r2', bucketName?: string } & CloudflareDriverOptions
export type CloudflareR2BlobConfig = {
driver: 'cloudflare-r2'
/**
* R2 bucket name used to auto-generate wrangler bindings
*/
bucketName?: string
/**
* R2 jurisdiction for the bucket binding (e.g. `'eu'`)
* @see https://developers.cloudflare.com/r2/reference/data-location/#jurisdictional-restrictions
*/
jurisdiction?: string
} & CloudflareDriverOptions

export type BlobConfig = boolean | FSBlobConfig | S3BlobConfig | VercelBlobConfig | CloudflareR2BlobConfig
export type ResolvedBlobConfig = FSBlobConfig | S3BlobConfig | VercelBlobConfig | CloudflareR2BlobConfig
Expand Down
9 changes: 9 additions & 0 deletions test/fixtures/wrangler-jurisdiction/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineNuxtConfig } from 'nuxt/config'

export default defineNuxtConfig({
extends: ['../basic'],
modules: ['../../../src/module'],
hub: {
blob: { driver: 'cloudflare-r2', bucketName: 'test-bucket', binding: 'BLOB', jurisdiction: 'eu' }
}
})
1 change: 1 addition & 0 deletions test/fixtures/wrangler-jurisdiction/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "private": true, "name": "hub-wrangler-jurisdiction-test", "type": "module" }
18 changes: 18 additions & 0 deletions test/wrangler-jurisdiction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { setup, useTestContext } from '@nuxt/test-utils'

describe('wrangler r2 jurisdiction e2e', async () => {
await setup({ rootDir: fileURLToPath(new URL('./fixtures/wrangler-jurisdiction', import.meta.url)), dev: true })

it('should include r2 jurisdiction when configured', () => {
const { nuxt } = useTestContext()
const wrangler = nuxt?.options.nitro.cloudflare?.wrangler

expect(wrangler?.r2_buckets).toContainEqual({
binding: 'BLOB',
bucket_name: 'test-bucket',
jurisdiction: 'eu'
})
})
})
9 changes: 9 additions & 0 deletions test/wrangler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,13 @@ describe('wrangler bindings e2e', async () => {
expect(wrangler?.kv_namespaces).toContainEqual({ binding: 'CACHE', id: 'test-cache-id' })
expect(wrangler?.d1_databases).toContainEqual({ binding: 'DB', database_id: 'test-db-id' })
})

it('should omit r2 jurisdiction when not configured', () => {
const { nuxt } = useTestContext()
const wrangler = nuxt?.options.nitro.cloudflare?.wrangler
const blobBinding = wrangler?.r2_buckets?.find((b: { binding: string }) => b.binding === 'BLOB')

expect(blobBinding).toEqual({ binding: 'BLOB', bucket_name: 'test-bucket' })
expect(blobBinding).not.toHaveProperty('jurisdiction')
})
})