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
18 changes: 18 additions & 0 deletions docs/content/2.usage/1.nuxt-img.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<NuxtImg
provider="cloudinary"
:provider-options="{ baseURL: 'https://res.cloudinary.com/tenant-b/image/upload' }"
src="/remote/nuxt-org/blog/going-full-static/main.png"
width="300"
height="169"
/>
```

### `preset`

Presets are predefined sets of image modifiers that can be used create unified form of images in your projects.
Expand Down
12 changes: 12 additions & 0 deletions docs/content/2.usage/3.use-image.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`

Expand Down
35 changes: 22 additions & 13 deletions src/runtime/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>
sizes?: string | Record<string, any>
} & Record<string, any>

export function createImage(globalOptions: CreateImageOptions) {
const ctx: ImageCTX = {
options: globalOptions,
}

const getImage: $Img['getImage'] = (input: string, options = {}) => {
const getImage: $Img['getImage'] = <Provider extends keyof ConfiguredImageProviders = DefaultProvider>(input: string, options: ImageOptions<Provider> = {}) => {
const image = resolveImage(ctx, input, options)

// Prerender static images
Expand All @@ -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 = <Provider extends keyof ConfiguredImageProviders = DefaultProvider>(input: string, options?: ImageOptions<Provider>) => getMeta(ctx, input, options)
$img.getSizes = <Provider extends keyof ConfiguredImageProviders = DefaultProvider>(input: string, options: ImageOptions<Provider> = {}) => 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') {
Expand All @@ -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)})`)
}
Expand Down Expand Up @@ -90,8 +99,8 @@ function resolveImage(ctx: ImageCTX, input: string, options: ImageOptions): Reso
}
}

const _options = defu(options, preset, defaults as Partial<ImageOptions>)
const resolvedOptions = {
const _options: RuntimeImageOptions = defu(options, preset, defaults as Partial<ImageOptions>)
const resolvedOptions: RuntimeImageOptions & { modifiers: Record<string, any> } = {
..._options,
modifiers: {
..._options.modifiers,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions src/runtime/utils/props.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -19,6 +19,7 @@ export interface BaseImageProps<Provider extends keyof ConfiguredImageProviders>
// options
preset?: string
provider?: Provider
providerOptions?: ProviderOptions<Provider>

sizes?: string | Record<string, any>
densities?: string
Expand All @@ -37,7 +38,8 @@ export const useImageProps = <Provider extends keyof ConfiguredImageProviders>(p
const $img = useImage()

const providerOptions = computed(() => ({
provider: props.provider,
...props.providerOptions as ProviderOptions<keyof ConfiguredImageProviders>,
provider: props.provider as keyof ConfiguredImageProviders | undefined,
preset: props.preset,
}))

Expand Down
24 changes: 16 additions & 8 deletions src/types/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Provider extends keyof ConfiguredImageProviders = DefaultProvider> {
export type ProviderOptions<Provider extends keyof ConfiguredImageProviders> = Partial<Omit<ConfiguredImageProviders[Provider], 'modifiers'>>

export type ImageOptions<Provider extends keyof ConfiguredImageProviders = DefaultProvider> = {
provider?: Provider
preset?: string
densities?: string
modifiers?: Partial<Omit<ImageModifiers, 'format' | 'quality' | 'background' | 'fit'>>
& ('modifiers' extends keyof ConfiguredImageProviders[Provider] ? ConfiguredImageProviders[Provider]['modifiers'] : Record<string, unknown>)
sizes?: string | Record<string, any>
}
} & ProviderOptions<Provider>

export interface ImageSizesOptions extends ImageOptions {
sizes: Record<string, string | number> | string
}

export type ProviderGetImage<T = Record<string, unknown>> = (src: string, options: Omit<ImageOptions, 'modifiers'> & { modifiers: Partial<ResolvedImageModifiers> } & T, ctx: ImageCTX) => ResolvedImage
export type ProviderGetImage<T = Record<string, unknown>> = (src: string, options: {
provider?: string
preset?: string
densities?: string
sizes?: string | Record<string, any>
modifiers: Partial<ResolvedImageModifiers>
} & T, ctx: ImageCTX) => ResolvedImage

interface ImageModifierOptions {
modifiers?: Record<string, unknown>
Expand Down Expand Up @@ -84,11 +92,11 @@ export interface ImageSizes {
}

export interface Img {
(source: string, modifiers?: ImageOptions['modifiers'], options?: ImageOptions): ResolvedImage['url']
<Provider extends keyof ConfiguredImageProviders = DefaultProvider>(source: string, modifiers?: ImageOptions<Provider>['modifiers'], options?: ImageOptions<Provider>): ResolvedImage['url']
options: CreateImageOptions
getImage: (source: string, options?: ImageOptions) => ResolvedImage
getSizes: (source: string, options?: ImageOptions, sizes?: string[]) => ImageSizes
getMeta: (source: string, options?: ImageOptions) => Promise<ImageInfo>
getImage: <Provider extends keyof ConfiguredImageProviders = DefaultProvider>(source: string, options?: ImageOptions<Provider>) => ResolvedImage
getSizes: <Provider extends keyof ConfiguredImageProviders = DefaultProvider>(source: string, options?: ImageOptions<Provider>, sizes?: string[]) => ImageSizes
getMeta: <Provider extends keyof ConfiguredImageProviders = DefaultProvider>(source: string, options?: ImageOptions<Provider>) => Promise<ImageInfo>
}

export type $Img = Img & {
Expand Down
12 changes: 12 additions & 0 deletions test/nuxt/use-image.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Loading