Skip to content

Commit

Permalink
feat!: improved $img interface (#169)
Browse files Browse the repository at this point in the history
* feat: improved $img interface

* chore: add sizes to img fallback

Co-authored-by: Sébastien Chopin <seb@nuxtjs.com>
  • Loading branch information
pi0 and Atinux committed Jan 28, 2021
1 parent 930e192 commit 40ab562
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 84 deletions.
2 changes: 1 addition & 1 deletion playground/pages/playground.vue
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export default {
return this.$img(this.src, {
width: 30,
format: 'jpg'
}).url
})
},
bgStyle () {
return {
Expand Down
2 changes: 1 addition & 1 deletion playground/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"types": [
"@types/node",
"@nuxt/types",
"../src/types"
"../src/types/global"
]
},
"exclude": [
Expand Down
24 changes: 14 additions & 10 deletions src/runtime/components/nuxt-img.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand All @@ -76,6 +72,12 @@ export default {
background: this.background,
fit: this.fit
}
},
nOptions () {
return {
provider: this.provider,
preset: this.preset
}
}
},
created () {
Expand All @@ -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`)
Expand Down
27 changes: 16 additions & 11 deletions src/runtime/components/nuxt-picture.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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]
},
Expand All @@ -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%'
Expand Down Expand Up @@ -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}`,
Expand Down
71 changes: 37 additions & 34 deletions src/runtime/image.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -11,18 +11,28 @@ 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)
}
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
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
11 changes: 6 additions & 5 deletions src/types/global.d.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<S> {
$img: $Image
$img: $Img
}
}
42 changes: 23 additions & 19 deletions src/types/image.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ImageInfo>
}

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<ImageInfo>
[preset: string]: $Img['options'] | $Img['getImage'] | $Img['getSizes'] | $Img['getMeta'] | $Img /* preset */
}

export interface ImageCTX {
options: CreateImageOptions,
accept: Matcher<any>
Expand All @@ -47,7 +69,7 @@ export interface ImageCTX {
isStatic: boolean
nuxtState?: any
}
$img?: Function
$img?: $Img
}

export interface ImageSize {
Expand All @@ -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<ImageInfo>
}

export interface $Image {
(source: string, options: ImageOptions): ResolvedImage
[preset: string]: (source: string) => any
}

export interface RuntimePlaceholder extends ImageInfo {
url: string;
}
Expand Down
4 changes: 2 additions & 2 deletions test/unit/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down

0 comments on commit 40ab562

Please sign in to comment.