diff --git a/playground/pages/playground.vue b/playground/pages/playground.vue index 6c8273d49..6bb1589b5 100644 --- a/playground/pages/playground.vue +++ b/playground/pages/playground.vue @@ -153,7 +153,7 @@ export default { return this.$img(this.src, { width: 30, format: 'jpg' - }).url + }) }, bgStyle () { return { diff --git a/playground/tsconfig.json b/playground/tsconfig.json index 985e9b42d..8d7aba6a2 100644 --- a/playground/tsconfig.json +++ b/playground/tsconfig.json @@ -25,7 +25,7 @@ "types": [ "@types/node", "@nuxt/types", - "../src/types" + "../src/types/global" ] }, "exclude": [ diff --git a/src/runtime/components/nuxt-img.vue b/src/runtime/components/nuxt-img.vue index e4cef65e2..e51e83ced 100644 --- a/src/runtime/components/nuxt-img.vue +++ b/src/runtime/components/nuxt-img.vue @@ -51,11 +51,7 @@ export default { if (this.usePlaceholder) { return EMPTY_GIF } - return this.$img(this.src, { - provider: this.provider, - preset: this.preset, - modifiers: this.nModifiers - }).url + return this.$img(this.src, this.nModifiers, this.nOptions) }, nAttrs () { const attrs: any = {} @@ -76,6 +72,12 @@ export default { background: this.background, fit: this.fit } + }, + nOptions () { + return { + provider: this.provider, + preset: this.preset + } } }, created () { @@ -95,11 +97,13 @@ export default { methods: { getResponsive () { const sizes = this.$img.getSizes(this.src, { - sizes: this.sizes, - width: parseSize(this.width), - height: parseSize(this.height), - modifiers: this.modifiers - }) + ...this.nOptions, + modifiers: { + ...this.nModifiers, + width: parseSize(this.width), + height: parseSize(this.height) + } + }, this.sizes) return { sizes: sizes.map(({ width }) => `(max-width: ${width}px) ${width}px`), srcSet: sizes.map(({ width, src }) => `${src} ${width}w`) diff --git a/src/runtime/components/nuxt-picture.vue b/src/runtime/components/nuxt-picture.vue index 0dc89d95b..7565b0fa6 100644 --- a/src/runtime/components/nuxt-picture.vue +++ b/src/runtime/components/nuxt-picture.vue @@ -22,7 +22,8 @@ :crossorigin="crossorigin" :src="defaultSrc" :srcset="sources[0].srcset" - :style="{ opacity: isLoaded ? 1 : 0 }" + :style="{ opacity: isLoaded ? 1 : 0.01 }" + :sizes="sources[0].sizes" :loading="isLazy ? 'lazy' : 'eager'" @load="onImageLoaded" @onbeforeprint="onPrint" @@ -132,6 +133,12 @@ export default { fit: this.fit } }, + nOptions () { + return { + provider: this.provider, + preset: this.preset + } + }, defaultSrc () { return this.sources[0].srcset[0].split(' ')[0] }, @@ -154,12 +161,10 @@ export default { } const width = 30 return this.$img(this.src, { - modifiers: { - ...this.modifiers, - width, - height: this.ratio ? Math.round(width * this.ratio) : undefined - } - }).url + ...this.nModifiers, + width, + height: this.ratio ? Math.round(width * this.ratio) : undefined + }, this.nOptions) }, sizerHeight () { return this.ratio ? `${this.ratio * 100}%` : '100%' @@ -193,14 +198,14 @@ export default { const sources = formats.map((format) => { const sizes = this.$img.getSizes(this.src, { - sizes: this.sizes, - width: this.nWidth, - height: this.nHeight, + ...this.nOptions, modifiers: { ...this.nModifiers, + width: this.nWidth, + height: this.nHeight, format } - }) + }, this.sizes) return { type: `image/${format}`, diff --git a/src/runtime/image.ts b/src/runtime/image.ts index 9624fdcba..933b0a22a 100644 --- a/src/runtime/image.ts +++ b/src/runtime/image.ts @@ -1,7 +1,7 @@ import { allowList } from 'allowlist' import defu from 'defu' import { hasProtocol, joinURL } from 'ufo' -import type { ImageOptions, CreateImageOptions, ResolvedImage, MapToStatic, ImageCTX } from '../types/image' +import type { ImageOptions, CreateImageOptions, ResolvedImage, MapToStatic, ImageCTX, $Img } from '../types/image' import { imageMeta } from './utils/meta' import { parseSize } from './utils' import { useStaticImageMap } from './utils/static-map' @@ -11,11 +11,11 @@ export function createImage (globalOptions: CreateImageOptions, nuxtContext) { const ctx: ImageCTX = { options: globalOptions, - allow: allowList(globalOptions.allow), + accept: allowList(globalOptions.accept), nuxtContext } - function $img (input: string, options: ImageOptions = {}) { + const getImage: $Img['getImage'] = function (input: string, options: ImageOptions = {}) { const image = resolveImage(ctx, input, options) if (image.isStatic) { handleStaticImage(image, input) @@ -23,6 +23,16 @@ export function createImage (globalOptions: CreateImageOptions, nuxtContext) { return image } + function $img (input: string, modifiers: ImageOptions['modifiers'] = {}, options: ImageOptions = {}) { + return getImage(input, { + ...options, + modifiers: { + ...options.modifiers, + ...modifiers + } + }).url + } + function handleStaticImage (image: ResolvedImage, input: string) { if (process.static) { const staticImagesBase = '/_nuxt/image' // TODO @@ -51,19 +61,17 @@ export function createImage (globalOptions: CreateImageOptions, nuxtContext) { } } - $img.options = globalOptions - ctx.$img = $img - for (const presetName in globalOptions.presets) { - $img[presetName] = (source: string, _options: ImageOptions = {}) => $img(source, { - ...globalOptions.presets[presetName], - ..._options - }) + $img[presetName] = ((source, modifiers, options) => + $img(source, modifiers, { ...globalOptions.presets[presetName], ...options })) as $Img[string] } - $img.getMeta = (input: string, options: ImageOptions) => getMeta(ctx, input, options) - // eslint-disable-next-line no-use-before-define - $img.getSizes = (input: string, options: GetSizesOptions) => getSizes(ctx, input, options) + $img.options = globalOptions + $img.getImage = getImage + $img.getMeta = ((input: string, options?: ImageOptions) => getMeta(ctx, input, options)) as $Img['getMeta'] + $img.getSizes = ((input: string, options?: ImageOptions, sizes?: string[]) => getSizes(ctx, input, options, sizes)) as $Img['getSizes'] + + ctx.$img = $img as $Img return $img } @@ -87,7 +95,7 @@ function resolveImage (ctx: ImageCTX, input: string, options: ImageOptions): Res throw new TypeError(`input must be a string (received ${typeof input}: ${JSON.stringify(input)})`) } - if (input.startsWith('data:') || (hasProtocol(input) && !ctx.allow(input))) { + if (input.startsWith('data:') || (hasProtocol(input) && !ctx.accept(input))) { return { url: input } @@ -132,36 +140,31 @@ function getPreset (ctx: ImageCTX, name?: string): ImageOptions { return ctx.options.presets[name] } -interface GetSizesOptions { - sizes?: string[] - modifiers?: any, - width?: number, - height?: number, -} - -function getSizes (ctx: ImageCTX, input: string, opts: GetSizesOptions) { - let widths = [].concat(opts.sizes || ctx.options.sizes) - if (opts.width) { - widths.push(opts.width) - widths = widths.filter(w => w <= opts.width) - widths.push(opts.width * 2) +function getSizes (ctx: ImageCTX, input: string, opts: ImageOptions = {}, sizes?: string[]) { + let widths = [].concat(sizes || ctx.options.sizes) + if (opts.modifiers.width) { + widths.push(opts.modifiers.width) + widths = widths.filter(w => w <= opts.modifiers.width) + widths.push(opts.modifiers.width * 2) } widths = Array.from(new Set(widths)) .sort((s1, s2) => s1 - s2) // unique & lowest to highest - const sizes = [] - const ratio = opts.height / opts.width + const sources = [] + const ratio = opts.modifiers.height / opts.modifiers.width for (const width of widths) { - const height = ratio ? Math.round(width * ratio) : opts.height - sizes.push({ + const height = ratio ? Math.round(width * ratio) : opts.modifiers.height + sources.push({ width, height, src: ctx.$img(input, { - modifiers: { ...opts.modifiers, width, height } - }).url + ...opts.modifiers, + width, + height + }, opts) }) } - return sizes + return sources } diff --git a/src/types/global.d.ts b/src/types/global.d.ts index f3bea020b..ce7bcd971 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -1,13 +1,14 @@ -import type { $Image } from './image' +import { } from '@nuxt/types' +import type { $Img } from './image' import { ModuleOptions } from './module' declare module '@nuxt/types' { interface Context { - $img: $Image + $img: $Img } interface NuxtAppOptions { - $img: $Image + $img: $Img } interface Configuration { @@ -17,13 +18,13 @@ declare module '@nuxt/types' { declare module 'vue/types/vue' { interface Vue { - $img: $Image + $img: $Img } } declare module 'vuex/types/index' { // eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars interface Store { - $img: $Image + $img: $Img } } diff --git a/src/types/image.d.ts b/src/types/image.d.ts index 2a994f629..534c1b5b3 100644 --- a/src/types/image.d.ts +++ b/src/types/image.d.ts @@ -37,6 +37,28 @@ export interface CreateImageOptions { accept: AllowlistOptions } +export interface ImageInfo { + width: number, + height: number, + placeholder?: string, +} + +export interface ResolvedImage { + url: string, + format?: string + isStatic?: boolean + getMeta?: () => Promise +} + +export interface $Img { + (source: string, modifiers?: ImageOptions['modifiers'], options?: ImageOptions): ResolvedImage['url'] + options: CreateImageOptions + getImage: (source: string, options?: ImageOptions) => ResolvedImage + getSizes: (source: string, options?: ImageOptions, sizes?: string[]) => { width: string, height: string, src: string }[] + getMeta: (source: string, options?: ImageOptions) => Promise + [preset: string]: $Img['options'] | $Img['getImage'] | $Img['getSizes'] | $Img['getMeta'] | $Img /* preset */ +} + export interface ImageCTX { options: CreateImageOptions, accept: Matcher @@ -47,7 +69,7 @@ export interface ImageCTX { isStatic: boolean nuxtState?: any } - $img?: Function + $img?: $Img } export interface ImageSize { @@ -58,24 +80,6 @@ export interface ImageSize { url: string; } -export interface ImageInfo { - width: number, - height: number, - placeholder?: string, -} - -export interface ResolvedImage { - url: string, - format?: string - isStatic?: boolean - getMeta?: () => Promise -} - -export interface $Image { - (source: string, options: ImageOptions): ResolvedImage - [preset: string]: (source: string) => any -} - export interface RuntimePlaceholder extends ImageInfo { url: string; } diff --git a/test/unit/plugin.test.ts b/test/unit/plugin.test.ts index 9249631dc..027324426 100644 --- a/test/unit/plugin.test.ts +++ b/test/unit/plugin.test.ts @@ -45,12 +45,12 @@ describe('Plugin', () => { test.skip('Generate Random Image', () => { const image = nuxtContext.$img('/test.png', { provider: 'random' }) - expect(image.url).toEqual('https://source.unsplash.com/random/600x400') + expect(image).toEqual('https://source.unsplash.com/random/600x400') }) test.skip('Generate Circle Image with Cloudinary', () => { const image = nuxtContext.$img('/test.png', { provider: 'cloudinary', preset: 'circle' }) - expect(image.url).toEqual('https://res.cloudinary.com/nuxt/image/upload/f_auto,q_auto,r_100/test') + expect(image).toEqual('https://res.cloudinary.com/nuxt/image/upload/f_auto,q_auto,r_100/test') }) test('Deny undefined provider', () => { diff --git a/tsconfig.json b/tsconfig.json index 1ded4c2fd..5d02ac16a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "moduleResolution": "Node", "esModuleInterop": true, "resolveJsonModule": true, - "types": ["node", "jest"], + "types": ["node", "jest", "./src/types/global"], "paths": { "~image/*": ["src/runtime/*"], "~image": ["src/runtime"]