Skip to content

Commit

Permalink
feat: universal meta resolving (#55)
Browse files Browse the repository at this point in the history
Co-authored-by: Ahad Birang <[email protected]>
  • Loading branch information
pi0 and farnabaz authored Nov 2, 2020
1 parent 14b4026 commit 45bbbe9
Show file tree
Hide file tree
Showing 11 changed files with 297 additions and 57 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
"dependencies": {
"defu": "^3.1.0",
"hasha": "^5.2.2",
"image-meta": "^0.0.1",
"ipx": "0.4.0-rc.1",
"make-fetch-happen": "^8.0.10",
"node-fetch": "^2.6.1",
"upath": "^2.0.0"
},
Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ function imageModule (moduleOptions: ModuleOptions) {
nuxt.hook('generate:before', () => {
handleStaticGeneration(nuxt)
})

const LruCache = require('lru-cache')
const cache = new LruCache()
nuxt.hook('vue-renderer:context', (ssrContext) => {
ssrContext.cache = cache
})
}

function loadProvider (key: string, provider: any) {
Expand Down
13 changes: 1 addition & 12 deletions src/providers/fastly/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { RuntimeProvider, ImageModifiers } from 'types'
import { cleanDoubleSlashes, createOperationsGenerator } from '~image/utils'
import fetch from '~image/fetch'

const operationsGenerator = createOperationsGenerator({
valueMap: {
Expand All @@ -21,17 +20,7 @@ export default <RuntimeProvider> {
const operations = operationsGenerator(modifiers)
const url = cleanDoubleSlashes(options.baseURL + src + '?' + operations)
return {
url,
getInfo: async () => {
const infoString = await fetch(url).then(res => res.headers.get('fastly-io-info') || '')
const info = Object.fromEntries(infoString.split(' ').map(part => part.split('=')))
const [width, height] = (info.idim || '').split('x').map(p => parseInt(p, 10))
return {
width,
height,
bytes: info.ifsz
}
}
url
}
}
}
11 changes: 1 addition & 10 deletions src/providers/imgix/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { RuntimeProvider, ImageModifiers } from 'types'
import { cleanDoubleSlashes, createOperationsGenerator } from '~image/utils'
import fetch from '~image/fetch'

const operationsGenerator = createOperationsGenerator({
keyMap: {
Expand All @@ -26,15 +25,7 @@ export default <RuntimeProvider> {
const operations = operationsGenerator(modifiers)
const url = cleanDoubleSlashes(options.baseURL + src + '?' + operations)
return {
url,
getInfo: async () => {
const info = await fetch(url + '&fm=json').then(res => res.json())
return {
width: info.PixelWidth,
height: info.PixelHeight,
bytes: info['Content-Length']
}
}
url
}
}
}
19 changes: 2 additions & 17 deletions src/providers/local/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { RuntimeProvider, ImageModifiers } from 'types'
import { cleanDoubleSlashes } from '~image/utils'
import fetch from '~image/fetch'

function predictAdapter (src: string) {
return src.match(/^https?:\/\//) ? 'remote' : 'local'
}

export default <RuntimeProvider> {
getImage (src: string, modifiers: ImageModifiers, options: any) {
getImage (src: string, modifiers: ImageModifiers, _options: any) {
const operations = []

const fit = modifiers.fit ? `_${modifiers.fit}` : ''
Expand All @@ -23,24 +22,10 @@ export default <RuntimeProvider> {

const operationsString = operations.join(',') || '_'
const url = cleanDoubleSlashes(`/_image/local/${adapter}/${modifiers.format || '_'}/${operationsString}/${src}`)
const infoUrl = cleanDoubleSlashes(`/_image/local/${adapter}/${modifiers.format || 'jpg'}.json/${operationsString}_url/${src}`)

const baseURL = process.client ? options.baseURL : options.internalBaseURL

let _meta
const getMeta = () => _meta || fetch(baseURL + infoUrl).then(res => res.json())

return {
url,
isStatic: true,
async getInfo () {
const { width, height, size, data } = await getMeta()
return { width, height, bytes: size, placeholder: data }
},
async getPlaceHolder () {
const { data } = await getMeta()
return data
}
isStatic: true
}
}
}
30 changes: 24 additions & 6 deletions src/runtime/image.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CreateImageOptions, ImageModifiers, ImagePreset } from 'types'
import { getMeta } from './meta'

function processSource (src: string) {
if (!src.includes(':') || src.match('^https?://')) {
Expand All @@ -15,6 +16,22 @@ function processSource (src: string) {
}
}

function getCache (context) {
if (!context.cache) {
if (context.ssrContext && context.ssrContext.cache) {
context.cache = context.ssrContext.cache
} else {
const _cache = {}
context.cache = {
get: id => _cache[id],
set: (id, value) => { _cache[id] = value },
has: id => typeof _cache[id] !== 'undefined'
}
}
}
return context.cache
}

export function createImage (context, { providers, defaultProvider, presets, intersectOptions }: CreateImageOptions) {
const presetMap = presets.reduce((map, preset) => {
map[preset.name] = preset
Expand Down Expand Up @@ -101,14 +118,15 @@ export function createImage (context, { providers, defaultProvider, presets, int
const provider = getProvider(sourceProvider || options.provider || defaultProvider)

const sImage = provider.provider.getImage(src, { ...modifiers, width: 30 }, provider.defaults)
const meta = { placeholder: sImage.url }

if (typeof sImage.getInfo === 'function') {
Object.assign(meta, await sImage.getInfo())
}
const meta = await { placeholder: sImage.url }

if (typeof sImage.getPlaceholder === 'function') {
meta.placeholder = await sImage.getPlaceholder()
if (typeof sImage.getMeta === 'function') {
Object.assign(meta, await sImage.getMeta())
} else {
const baseUrl = 'http://localhost:3000'
const absoluteUrl = sImage.url[0] === '/' ? baseUrl + sImage.url : sImage.url
Object.assign(meta, await getMeta(absoluteUrl, getCache(context)))
}

return meta
Expand Down
42 changes: 42 additions & 0 deletions src/runtime/meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { RuntimeImageInfo } from 'types'

export async function getMeta (url, cache): RuntimeImageInfo {
const cacheKey = 'image:meta:' + url
if (cache.has(cacheKey)) {
return cache.get(cacheKey)
}

if (process.client) {
if (typeof Image === 'undefined') {
throw new TypeError('Image not supported')
}

return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => {
const meta = {
width: img.width,
height: img.height,
placeholder: url
}
cache.set(cacheKey, meta)
resolve(meta)
}
img.onerror = err => reject(err)
img.src = url
})
}

if (process.server) {
const imageMeta = require('image-meta').default
const data: Buffer = await fetch(url).then((res: any) => res.buffer())
const { width, height, mimeType } = await imageMeta(data)
const meta = {
width,
height,
placeholder: `data:${mimeType};base64,${data.toString('base64')}`
}
cache.set(cacheKey, meta)
return meta
}
}
4 changes: 2 additions & 2 deletions src/runtime/nuxt-image-mixins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default {
},
lazy: {
type: Boolean,
default: typeof IntersectionObserver !== 'undefined'
default: true
},
sets: {
type: [String, Array],
Expand Down Expand Up @@ -213,7 +213,7 @@ export default {
}
},
loadOriginalImage () {
this.lazyState = 'loading'
this.lazyState = LazyState.LOADING
},
renderImgAttributesToString (extraAttributes = {}) {
return renderAttributesToString({
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"types": [
"@nuxt/types",
"@types/node",
"image-meta",
"./types"
],
"paths": {
Expand Down
5 changes: 1 addition & 4 deletions types/runtime.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,14 @@ export interface RuntimeProvider {
export interface RuntimeImage {
url: string,
isStatic?: boolean,
getInfo?: () => Promise<RuntimeImageInfo>
getPlaceholder?: () => Promise<string>
getMeta?: () => Promise<RuntimeImageInfo>
}

export interface RuntimeImageInfo {
// width of image in pixels
width: number,
// height of image in pixels
height: number,
// size of image in bytes
bytes?: number,
// placeholder (base64 or url)
placeholder?: string,
}
Expand Down
Loading

0 comments on commit 45bbbe9

Please sign in to comment.