Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: improved $img interface #169

Merged
merged 2 commits into from
Jan 28, 2021
Merged
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
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 */
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/cc @danielroe if you know a better way to do this for mixed interface type

}

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