diff --git a/docs/content/2.usage/1.nuxt-img.md b/docs/content/2.usage/1.nuxt-img.md index d1df456ed..e9440d8ef 100644 --- a/docs/content/2.usage/1.nuxt-img.md +++ b/docs/content/2.usage/1.nuxt-img.md @@ -259,6 +259,24 @@ export default defineNuxtConfig({ ``` :: +### `providerOptions` + +Override the selected provider's configuration for this image only. This is useful when you have a provider with multiple tenants. + +The prop is typed after the selected `provider`: only options valid for that provider are accepted. Options passed here take precedence over the provider configuration in `nuxt.config`. + +**Example:** + +```vue + +``` + ### `preset` Presets are predefined sets of image modifiers that can be used create unified form of images in your projects. diff --git a/docs/content/2.usage/3.use-image.md b/docs/content/2.usage/3.use-image.md index 7675da043..767790ff7 100644 --- a/docs/content/2.usage/3.use-image.md +++ b/docs/content/2.usage/3.use-image.md @@ -31,6 +31,17 @@ const backgroundStyles = computed(() => { }) ``` +The `options` object also accepts the selected provider's own options, which override the provider configuration from `nuxt.config` for this call only. This is useful for multi-tenant setups: + +```js +const img = useImage() + +const url = img('/logo.png', { width: 100 }, { + provider: 'cloudinary', + baseURL: 'https://res.cloudinary.com/tenant-b/image/upload' +}) +``` + ### `img.getSizes` ```js @@ -56,6 +67,7 @@ Unstable: `getSizes` API might change or be removed. - `options`: (object) - `provider`: (string) Provider name other than default (see [providers](/get-started/configuration#providers)) - `preset`: Use a [preset](/get-started/configuration#presets) + - (any option of the selected provider, e.g. `baseURL`, overriding its configuration for this call) **Example:** Responsive srcset with Vuetify `v-img` diff --git a/src/runtime/image.ts b/src/runtime/image.ts index 764d32268..786f0dc90 100644 --- a/src/runtime/image.ts +++ b/src/runtime/image.ts @@ -3,14 +3,22 @@ import { hasProtocol, parseURL, joinURL, withLeadingSlash } from 'ufo' import { imageMeta } from './utils/meta' import { checkDensities, parseDensities, parseSize, parseSizes } from './utils' import { prerenderStaticImages } from './utils/prerender' -import type { ImageOptions, ImageSizesOptions, CreateImageOptions, ResolvedImage, ImageCTX, $Img, ImageSizes, ImageSizesVariant, ConfiguredImageProviders } from '@nuxt/image' +import type { ImageOptions, CreateImageOptions, ResolvedImage, ImageCTX, $Img, ImageSizes, ImageSizesVariant, ConfiguredImageProviders, DefaultProvider } from '@nuxt/image' + +type RuntimeImageOptions = { + provider?: keyof ConfiguredImageProviders + preset?: string + densities?: string + modifiers?: Record + sizes?: string | Record +} & Record export function createImage(globalOptions: CreateImageOptions) { const ctx: ImageCTX = { options: globalOptions, } - const getImage: $Img['getImage'] = (input: string, options = {}) => { + const getImage: $Img['getImage'] = (input: string, options: ImageOptions = {}) => { const image = resolveImage(ctx, input, options) // Prerender static images @@ -21,24 +29,25 @@ export function createImage(globalOptions: CreateImageOptions) { return image } - const $img = ((input, modifiers, options) => getImage(input, defu({ modifiers }, options)).url) as $Img + const $img = ((input: string, modifiers?: RuntimeImageOptions['modifiers'], options?: RuntimeImageOptions) => + getImage(input, defu({ modifiers }, options)).url) as $Img for (const presetName in globalOptions.presets) { - $img[presetName] = ((source, modifiers, options) => + $img[presetName] = ((source: string, modifiers?: RuntimeImageOptions['modifiers'], options?: RuntimeImageOptions) => $img(source, modifiers, { ...globalOptions.presets[presetName], ...options })) as $Img[string] } $img.options = globalOptions $img.getImage = getImage - $img.getMeta = ((input: string, options?: ImageOptions) => getMeta(ctx, input, options)) as $Img['getMeta'] - $img.getSizes = ((input: string, options: ImageSizesOptions) => getSizes(ctx, input, options)) as $Img['getSizes'] + $img.getMeta = (input: string, options?: ImageOptions) => getMeta(ctx, input, options) + $img.getSizes = (input: string, options: ImageOptions = {}) => getSizes(ctx, input, options) - ctx.$img = $img as $Img + ctx.$img = $img return $img } -async function getMeta(ctx: ImageCTX, input: string, options?: ImageOptions) { +async function getMeta(ctx: ImageCTX, input: string, options?: RuntimeImageOptions) { const image = resolveImage(ctx, input, { ...options }) if (typeof image.getMeta === 'function') { @@ -49,7 +58,7 @@ async function getMeta(ctx: ImageCTX, input: string, options?: ImageOptions) { } } -function resolveImage(ctx: ImageCTX, input: string, options: ImageOptions): ResolvedImage { +function resolveImage(ctx: ImageCTX, input: string, options: RuntimeImageOptions): ResolvedImage { if (input && typeof input !== 'string') { throw new TypeError(`input must be a string (received ${typeof input}: ${JSON.stringify(input)})`) } @@ -90,8 +99,8 @@ function resolveImage(ctx: ImageCTX, input: string, options: ImageOptions): Reso } } - const _options = defu(options, preset, defaults as Partial) - const resolvedOptions = { + const _options: RuntimeImageOptions = defu(options, preset, defaults as Partial) + const resolvedOptions: RuntimeImageOptions & { modifiers: Record } = { ..._options, modifiers: { ..._options.modifiers, @@ -124,7 +133,7 @@ function getPreset(ctx: ImageCTX, name?: string): ImageOptions { return ctx.options.presets[name] } -function getSizes(ctx: ImageCTX, input: string, opts: ImageSizesOptions): ImageSizes { +function getSizes(ctx: ImageCTX, input: string, opts: RuntimeImageOptions): ImageSizes { // Merge preset options so preset-provided sizes/densities are respected const preset = getPreset(ctx, opts.preset) const merged = defu(opts, preset) @@ -232,7 +241,7 @@ function getSizesVariant(key: string, size: string, height: number | undefined, } } -function getVariantSrc(ctx: ImageCTX, input: string, opts: ImageSizesOptions, variant: ImageSizesVariant, density: number) { +function getVariantSrc(ctx: ImageCTX, input: string, opts: RuntimeImageOptions, variant: ImageSizesVariant, density: number) { return ctx.$img!(input, { ...opts.modifiers, diff --git a/src/runtime/utils/props.ts b/src/runtime/utils/props.ts index dee657590..1c3360b55 100644 --- a/src/runtime/utils/props.ts +++ b/src/runtime/utils/props.ts @@ -1,6 +1,6 @@ import { computed } from 'vue' -import type { ConfiguredImageProviders, ImageModifiers } from '@nuxt/image' +import type { ConfiguredImageProviders, ImageModifiers, ProviderOptions } from '@nuxt/image' import { parseSize } from '.' import { useImage } from '#imports' @@ -19,6 +19,7 @@ export interface BaseImageProps // options preset?: string provider?: Provider + providerOptions?: ProviderOptions sizes?: string | Record densities?: string @@ -37,7 +38,8 @@ export const useImageProps = (p const $img = useImage() const providerOptions = computed(() => ({ - provider: props.provider, + ...props.providerOptions as ProviderOptions, + provider: props.provider as keyof ConfiguredImageProviders | undefined, preset: props.preset, })) diff --git a/src/types/image.ts b/src/types/image.ts index 0ff89fb67..934802219 100644 --- a/src/types/image.ts +++ b/src/types/image.ts @@ -17,22 +17,30 @@ export interface ResolvedImageModifiers extends ImageModifiers { height: number } -type DefaultProvider = ProviderDefaults extends Record<'provider', unknown> ? ProviderDefaults['provider'] : never +export type DefaultProvider = ProviderDefaults extends Record<'provider', unknown> ? ProviderDefaults['provider'] : never -export interface ImageOptions { +export type ProviderOptions = Partial> + +export type ImageOptions = { provider?: Provider preset?: string densities?: string modifiers?: Partial> & ('modifiers' extends keyof ConfiguredImageProviders[Provider] ? ConfiguredImageProviders[Provider]['modifiers'] : Record) sizes?: string | Record -} +} & ProviderOptions export interface ImageSizesOptions extends ImageOptions { sizes: Record | string } -export type ProviderGetImage> = (src: string, options: Omit & { modifiers: Partial } & T, ctx: ImageCTX) => ResolvedImage +export type ProviderGetImage> = (src: string, options: { + provider?: string + preset?: string + densities?: string + sizes?: string | Record + modifiers: Partial +} & T, ctx: ImageCTX) => ResolvedImage interface ImageModifierOptions { modifiers?: Record @@ -84,11 +92,11 @@ export interface ImageSizes { } export interface Img { - (source: string, modifiers?: ImageOptions['modifiers'], options?: ImageOptions): ResolvedImage['url'] + (source: string, modifiers?: ImageOptions['modifiers'], options?: ImageOptions): ResolvedImage['url'] options: CreateImageOptions - getImage: (source: string, options?: ImageOptions) => ResolvedImage - getSizes: (source: string, options?: ImageOptions, sizes?: string[]) => ImageSizes - getMeta: (source: string, options?: ImageOptions) => Promise + getImage: (source: string, options?: ImageOptions) => ResolvedImage + getSizes: (source: string, options?: ImageOptions, sizes?: string[]) => ImageSizes + getMeta: (source: string, options?: ImageOptions) => Promise } export type $Img = Img & { diff --git a/test/nuxt/use-image.test.ts b/test/nuxt/use-image.test.ts index e0547ef1f..aea502c6f 100644 --- a/test/nuxt/use-image.test.ts +++ b/test/nuxt/use-image.test.ts @@ -24,7 +24,19 @@ describe('image helper', () => { expect(() => useImage().getImage('/test.png', { preset: 'invalid' })).toThrow(Error) }) + it('applies per-call provider options', () => { + const { url } = useImage().getImage('/test.png', { provider: 'ipx', baseURL: '/custom' }) + expect(url).toMatchInlineSnapshot('"/custom/_/test.png"') + }) + it('is correctly typed for provider options', () => { + useImage().getImage('/test.png', { + provider: 'ipx', + baseURL: '/other', + // @ts-expect-error this is not a valid option for ipx + yolo: true, + }) + useImage().getImage('/test.png', { provider: 'ipx', modifiers: {