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
45 changes: 34 additions & 11 deletions src/blob/runtime/app/composables/useMultipartUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,32 @@ import { readonly, ref, type Ref } from 'vue'
import type { SerializeObject } from 'nitropack'
import type { BlobUploadedPart, BlobObject } from '@nuxthub/core/blob'
import { useRuntimeConfig } from '#imports'

type VercelBlobUpload = (pathname: string, body: File, options: {
access: 'public' | 'private'
multipart: boolean
handleUploadUrl: string
onUploadProgress: (uploadProgress: { percentage: number }) => void
}) => Promise<SerializeObject<BlobObject>>

type RuntimeImporter = (specifier: string) => Promise<{ upload?: VercelBlobUpload }>

const runtimeImport: RuntimeImporter = (specifier) => {
return new Function('modulePath', 'return import(modulePath)')(specifier) as Promise<{ upload?: VercelBlobUpload }>
}

export async function loadVercelBlobClient(importer: RuntimeImporter = runtimeImport): Promise<VercelBlobUpload> {
try {
const mod = await importer('@vercel/blob/client')
if (typeof mod.upload !== 'function') {
throw new TypeError('Missing upload export in @vercel/blob/client')
}
return mod.upload
} catch (error) {
const message = '@vercel/blob is required to use `useMultipartUpload` with Vercel Blob. Install it with `pnpm add @vercel/blob`.'
throw new Error(message, { cause: error as Error })
}
}
/**
* Create a multipart uploader.
*/
Expand Down Expand Up @@ -130,17 +156,14 @@ export function useMultipartUpload(
const start = async () => {
const hub = useRuntimeConfig().public.hub
if (hub.blobProvider === 'vercel-blob') {
// #809 - variable indirection to avoid Vite static analysis
const pkg = '@vercel/blob/client'
return import(/* @vite-ignore */ pkg).then(({ upload }) => {
return upload(file.name, file, {
access: 'public',
multipart: true,
handleUploadUrl: joinURL(baseURL, 'multipart', file.name || ''),
onUploadProgress: (uploadProgress) => {
progress.value = uploadProgress.percentage
}
})
const upload = await loadVercelBlobClient()
return upload(file.name, file, {
access: 'public',
multipart: true,
handleUploadUrl: joinURL(baseURL, 'multipart', file.name || ''),
onUploadProgress: (uploadProgress) => {
progress.value = uploadProgress.percentage
}
})
}

Expand Down
40 changes: 40 additions & 0 deletions test/useMultipartUpload.vercel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it, vi } from 'vitest'
import { loadVercelBlobClient } from '../src/blob/runtime/app/composables/useMultipartUpload'

vi.mock('#imports', () => ({
useRuntimeConfig: () => ({
public: {
hub: {
blobProvider: 'fs'
}
}
})
}))

describe('loadVercelBlobClient', () => {
it('returns upload function when module is available', async () => {
const upload = vi.fn()
const importer = vi.fn().mockResolvedValue({ upload })

const resolved = await loadVercelBlobClient(importer)

expect(importer).toHaveBeenCalledWith('@vercel/blob/client')
expect(resolved).toBe(upload)
})

it('throws actionable error when module is missing', async () => {
const importer = vi.fn().mockRejectedValue(new Error('Cannot find module'))

await expect(loadVercelBlobClient(importer))
.rejects
.toThrow('@vercel/blob is required to use `useMultipartUpload` with Vercel Blob. Install it with `pnpm add @vercel/blob`.')
})

it('throws actionable error when upload export is missing', async () => {
const importer = vi.fn().mockResolvedValue({})

await expect(loadVercelBlobClient(importer))
.rejects
.toThrow('@vercel/blob is required to use `useMultipartUpload` with Vercel Blob. Install it with `pnpm add @vercel/blob`.')
})
})
Loading