diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 000000000..2838a3076 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,6 @@ +import { $Img } from './src/types' + +declare global { + // Convenience declaration to avoid importing types into runtime templates + const $Img: $Img +} diff --git a/package.json b/package.json index f9d59c9bf..866da3b9c 100755 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "vetur" ], "scripts": { - "build": "siroc build && mkdist --src src/runtime --dist dist/runtime", + "build": "siroc build && mkdist --src src/runtime --dist dist/runtime -d", "dev": "yarn nuxt playground", "docs:build": "cd docs && nuxt generate", "docs:dev": "yarn nuxt dev docs", @@ -48,14 +48,17 @@ "@nuxt/typescript-build": "latest", "@nuxt/typescript-runtime": "latest", "@nuxtjs/eslint-config-typescript": "latest", + "@types/fs-extra": "^9.0.11", "@types/jest": "latest", + "@types/lru-cache": "^5.1.0", + "@types/node-fetch": "^2.5.10", "@vue/test-utils": "latest", "babel-eslint": "latest", "eslint": "latest", "jest": "latest", "jsdom": "latest", "jsdom-global": "latest", - "mkdist": "latest", + "mkdist": "^0.2.0", "nuxt": "latest", "playwright": "latest", "siroc": "latest", diff --git a/src/generate.ts b/src/generate.ts index 208a3cc61..0f7e18523 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -13,9 +13,9 @@ import { hash, logger } from './utils' const pipeline = promisify(stream.pipeline) export function setupStaticGeneration (nuxt: any, options: ModuleOptions) { - const staticImages = {} // url ~> hashed file name + const staticImages: Record = {} // url ~> hashed file name - nuxt.hook('vue-renderer:ssr:prepareContext', (renderContext) => { + nuxt.hook('vue-renderer:ssr:prepareContext', (renderContext: any) => { renderContext.image = renderContext.image || {} renderContext.image.mapToStatic = function ({ url, format }: ResolvedImage) { if (!staticImages[url]) { @@ -43,7 +43,7 @@ export function setupStaticGeneration (nuxt: any, options: ModuleOptions) { }) } -async function downloadImage ({ url, name, outDir }) { +async function downloadImage ({ url, name, outDir }: { url: string, name: string, outDir: string }) { try { const response = await fetch(url) if (!response.ok) { throw new Error(`Unexpected response ${response.statusText}`) } diff --git a/src/ipx.ts b/src/ipx.ts index ff3a25286..f7a643637 100644 --- a/src/ipx.ts +++ b/src/ipx.ts @@ -1,5 +1,7 @@ -export function createIPXMiddleware (ipxOptions) { - const { createIPX, createIPXMiddleware } = require('ipx') +import type { IPXOptions } from 'ipx' + +export function createIPXMiddleware (ipxOptions: IPXOptions) { + const { createIPX, createIPXMiddleware } = require('ipx') as typeof import('ipx') const ipx = createIPX(ipxOptions) return createIPXMiddleware(ipx) } diff --git a/src/module.ts b/src/module.ts index b6c9bc6bc..cdc548198 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,12 +1,15 @@ -import { resolve } from 'path' +import { resolve } from 'upath' + +import type { Module } from '@nuxt/types' import defu from 'defu' -import type { ModuleOptions, CreateImageOptions } from './types' -import { pick, pkg } from './utils' + import { setupStaticGeneration } from './generate' -import { resolveProviders, detectProvider } from './provider' import { createIPXMiddleware } from './ipx' +import { resolveProviders, detectProvider } from './provider' +import type { ModuleOptions, CreateImageOptions } from './types' +import { pick, pkg } from './utils' -async function imageModule (moduleOptions: ModuleOptions) { +const imageModule: Module = async function imageModule (moduleOptions) { const { nuxt, addPlugin, addServerMiddleware } = this const defaults: ModuleOptions = { @@ -44,9 +47,10 @@ async function imageModule (moduleOptions: ModuleOptions) { 'intersectOptions' ]) + options.static = options.static || {} options.static.domains = options.domains - const providers = await resolveProviders(nuxt, options) + const providers = resolveProviders(nuxt, options) // Run setup for (const p of providers) { @@ -87,17 +91,17 @@ async function imageModule (moduleOptions: ModuleOptions) { setupStaticGeneration(nuxt, options) }) - const LruCache = require('lru-cache') + const LruCache = await import('lru-cache').then(r => r.default || r) const cache = new LruCache() - nuxt.hook('vue-renderer:context', (ssrContext) => { + nuxt.hook('vue-renderer:context', (ssrContext: any) => { ssrContext.cache = cache }) - nuxt.hook('listen', (_, listener) => { + nuxt.hook('listen', (_: any, listener: any) => { options.internalUrl = `http://localhost:${listener.port}` }) } -(imageModule as any).meta = pkg + ; (imageModule as any).meta = pkg export default imageModule diff --git a/src/provider.ts b/src/provider.ts index 5db2437ee..cd5d56a9c 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -22,12 +22,12 @@ export const providerSetup: Record = { await mkdirp(dirname(imagesConfig)) await writeJson(imagesConfig, { domains: moduleOptions.domains, - sizes: Object.values(moduleOptions.screens) + sizes: Object.values(moduleOptions.screens || {}) }) } } -export function resolveProviders (nuxt, options: ModuleOptions): ImageModuleProvider[] { +export function resolveProviders (nuxt: any, options: ModuleOptions): ImageModuleProvider[] { const providers: ImageModuleProvider[] = [] for (const key in options) { @@ -65,8 +65,8 @@ export function resolveProvider (nuxt: any, key: string, input: InputProvider): return { ...input, setup, - runtime: normalize(input.provider), - importName: `${key}Runtime$${hash(input.provider, 4)}`, + runtime: normalize(input.provider!), + importName: `${key}Runtime$${hash(input.provider!, 4)}`, runtimeOptions: input.options } } diff --git a/src/runtime/components/image.mixin.ts b/src/runtime/components/image.mixin.ts index 53fa9d2bb..386df7d6a 100644 --- a/src/runtime/components/image.mixin.ts +++ b/src/runtime/components/image.mixin.ts @@ -1,5 +1,10 @@ +import { parseSize } from '../utils' +import type { DefineMixin } from '../../types/vue' + +const defineMixin: DefineMixin = (opts: any) => opts + // @vue/component -export const imageMixin = { +export const imageMixin = defineMixin({ props: { // input source src: { type: String, required: true }, @@ -9,13 +14,13 @@ export const imageMixin = { quality: { type: [Number, String], default: undefined }, background: { type: String, default: undefined }, fit: { type: String, default: undefined }, - modifiers: { type: Object, default: undefined }, + modifiers: { type: Object as () => Record, default: undefined }, // options preset: { type: String, default: undefined }, provider: { type: String, default: undefined }, - sizes: { type: [Object, String], default: undefined }, + sizes: { type: [Object, String] as unknown as () => string | Record, default: undefined }, // attributes width: { type: [String, Number], default: undefined }, @@ -25,41 +30,52 @@ export const imageMixin = { usemap: { type: String, default: undefined }, longdesc: { type: String, default: undefined }, ismap: { type: Boolean, default: undefined }, - crossorigin: { type: Boolean, default: undefined }, + crossorigin: { type: [Boolean, String] as unknown as () => boolean | '' | 'anonymous' | 'use-credentials', default: undefined, validator: val => ['anonymous', 'use-credentials', '', true, false].includes(val) }, loading: { type: String, default: undefined }, - decoding: { type: String, default: undefined } + decoding: { type: String as () => 'async' | 'auto' | 'sync', default: undefined, validator: val => ['async', 'auto', 'sync'].includes(val) } }, computed: { - nImgAttrs () { + nImgAttrs (): { + width?: number + height?: number + alt?: string + referrerpolicy?: string + usemap?: string + longdesc?: string + ismap?: boolean + crossorigin?: '' | 'anonymous' | 'use-credentials' + loading?: string + decoding?: 'async' | 'auto' | 'sync' + } { return { - width: this.width, - height: this.height, + width: parseSize(this.width), + height: parseSize(this.height), alt: this.alt, referrerpolicy: this.referrerpolicy, usemap: this.usemap, longdesc: this.longdesc, ismap: this.ismap, - crossorigin: this.crossorigin, + crossorigin: this.crossorigin === true ? 'anonymous' : this.crossorigin || undefined, loading: this.loading, decoding: this.decoding } }, - nModifiers () { + nModifiers (): { width?: number, height?: number, format?: string, quality?: string | number, background?: string, fit?: string } & Record { return { ...this.modifiers, - width: this.width, - height: this.height, + width: parseSize(this.width), + height: parseSize(this.height), format: this.format, quality: this.quality, background: this.background, fit: this.fit } }, - nOptions () { + nOptions (): { provider?: string, preset?: string } { return { provider: this.provider, preset: this.preset } } } -} +}) diff --git a/src/runtime/components/lazy.mixin.ts b/src/runtime/components/lazy.mixin.ts index 97159781c..e045c623e 100644 --- a/src/runtime/components/lazy.mixin.ts +++ b/src/runtime/components/lazy.mixin.ts @@ -1,12 +1,21 @@ +import type { DefineMixin } from '../../types/vue' +import type { imageMixin } from './image.mixin' + import { useObserver } from '~image' export const EMPTY_GIF = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' +const defineMixin: DefineMixin = (opts: any) => opts + +interface PrivateThis { + _removeObserver?: ReturnType +} + // @vue/component -export const lazyMixin = { +export const lazyMixin = defineMixin({ data () { return { - lazyLoad: this.loading === 'lazy' + lazyLoad: (this as any as typeof imageMixin).loading === 'lazy' } }, mounted () { @@ -19,15 +28,16 @@ export const lazyMixin = { }, methods: { observe () { - this._removeObserver = useObserver(this.$el, () => { + ;(this as PrivateThis)._removeObserver = useObserver(this.$el, () => { this.lazyLoad = false }) }, unobserve () { - if (this._removeObserver) { - this._removeObserver() - delete this._removeObserver + const { _removeObserver } = this as PrivateThis + if (_removeObserver) { + _removeObserver() + delete (this as PrivateThis)._removeObserver } } } -} +}) diff --git a/src/runtime/components/nuxt-img.vue b/src/runtime/components/nuxt-img.vue index e8015a3df..58caf8d74 100644 --- a/src/runtime/components/nuxt-img.vue +++ b/src/runtime/components/nuxt-img.vue @@ -7,17 +7,26 @@ diff --git a/src/runtime/components/nuxt-picture.vue b/src/runtime/components/nuxt-picture.vue index e4d3db4e7..bb68fa093 100644 --- a/src/runtime/components/nuxt-picture.vue +++ b/src/runtime/components/nuxt-picture.vue @@ -16,41 +16,46 @@ diff --git a/src/runtime/image.ts b/src/runtime/image.ts index 81408286a..ecf30c2a8 100644 --- a/src/runtime/image.ts +++ b/src/runtime/image.ts @@ -5,15 +5,15 @@ import { imageMeta } from './utils/meta' import { parseSize } from './utils' import { useStaticImageMap } from './utils/static-map' -export function createImage (globalOptions: CreateImageOptions, nuxtContext) { - const staticImageManifest = (process.client && process.static) ? useStaticImageMap(nuxtContext) : {} +export function createImage (globalOptions: CreateImageOptions, nuxtContext: any) { + const staticImageManifest: Record = (process.client && process.static) ? useStaticImageMap(nuxtContext) : {} const ctx: ImageCTX = { options: globalOptions, nuxtContext } - const getImage: $Img['getImage'] = function (input: string, options: ImageOptions = {}) { + const getImage: $Img['getImage'] = function (input: string, options = {}) { const image = resolveImage(ctx, input, options) if (image.isStatic) { handleStaticImage(image, input) @@ -21,7 +21,7 @@ export function createImage (globalOptions: CreateImageOptions, nuxtContext) { return image } - function $img (input: string, modifiers: ImageOptions['modifiers'] = {}, options: ImageOptions = {}) { + const $img = function $img (input, modifiers = {}, options = {}) { return getImage(input, { ...options, modifiers: { @@ -29,7 +29,7 @@ export function createImage (globalOptions: CreateImageOptions, nuxtContext) { ...modifiers } }).url - } + } as $Img function handleStaticImage (image: ResolvedImage, input: string) { if (process.static) { @@ -74,18 +74,14 @@ export function createImage (globalOptions: CreateImageOptions, nuxtContext) { return $img } -async function getMeta (ctx: ImageCTX, input: string, options: ImageOptions) { +async function getMeta (ctx: ImageCTX, input: string, options?: ImageOptions) { const image = resolveImage(ctx, input, { ...options }) - const meta = {} - if (typeof image.getMeta === 'function') { - Object.assign(meta, await image.getMeta()) + return await image.getMeta() } else { - Object.assign(meta, await imageMeta(ctx, image.url)) + return await imageMeta(ctx, image.url) } - - return meta } function resolveImage (ctx: ImageCTX, input: string, options: ImageOptions): ResolvedImage { @@ -139,7 +135,9 @@ function getPreset (ctx: ImageCTX, name?: string): ImageOptions { } function getSizes (ctx: ImageCTX, input: string, opts: ImageSizesOptions) { - const ratio = parseSize(opts.modifiers.height) / parseSize(opts.modifiers.width) + const width = parseSize(opts.modifiers?.width) + const height = parseSize(opts.modifiers?.height) + const ratio = (width && height) ? width / height : 0 const variants = [] const sizes: Record = {} @@ -156,7 +154,7 @@ function getSizes (ctx: ImageCTX, input: string, opts: ImageSizesOptions) { } for (const key in sizes) { - const screenMaxWidth = ctx.options.screens[key] || parseInt(key) + const screenMaxWidth = (ctx.options.screens && ctx.options.screens[key]) || parseInt(key) let size = String(sizes[key]) const isFluid = size.endsWith('vw') if (!isFluid && /^\d+$/.test(size)) { @@ -165,20 +163,20 @@ function getSizes (ctx: ImageCTX, input: string, opts: ImageSizesOptions) { if (!isFluid && !size.endsWith('px')) { continue } - let width = parseInt(size) - if (!screenMaxWidth || !width) { + let _cWidth = parseInt(size) + if (!screenMaxWidth || !_cWidth) { continue } if (isFluid) { - width = Math.round((width / 100) * screenMaxWidth) + _cWidth = Math.round((_cWidth / 100) * screenMaxWidth) } - const height = ratio ? Math.round(width * ratio) : parseSize(opts.modifiers.height) + const _cHeight = ratio ? Math.round(_cWidth * ratio) : height variants.push({ - width, + width: _cWidth, size, screenMaxWidth, media: `(max-width: ${screenMaxWidth}px)`, - src: ctx.$img(input, { ...opts.modifiers, width, height }, opts) + src: ctx.$img!(input, { ...opts.modifiers, width: _cWidth, _cHeight }, opts) }) } diff --git a/src/runtime/providers/cloudinary.ts b/src/runtime/providers/cloudinary.ts index 5774eebe1..e128d2a03 100644 --- a/src/runtime/providers/cloudinary.ts +++ b/src/runtime/providers/cloudinary.ts @@ -80,7 +80,7 @@ export const getImage: ProviderGetImage = (src, { modifiers = {}, baseURL = '/' const mergeModifiers = { ...defaultModifiers, ...modifiers } const operations = operationsGenerator(mergeModifiers as any) - let remoteFolderMapping = baseURL.match(/\/image\/upload\/(.*)/) + const remoteFolderMapping = baseURL.match(/\/image\/upload\/(.*)/) // Handle delivery remote media file URLs // see: https://cloudinary.com/documentation/fetch_remote_images // Note: Non-remote images will pass into this function if the baseURL is not using a sub directory diff --git a/src/runtime/providers/ipx.ts b/src/runtime/providers/ipx.ts index e3cce08eb..ea5d97fdf 100644 --- a/src/runtime/providers/ipx.ts +++ b/src/runtime/providers/ipx.ts @@ -26,7 +26,7 @@ export const getImage: ProviderGetImage = (src, { modifiers = {}, baseURL = '/_i const params = operationsGenerator(modifiers) if (hasProtocol(src)) { - if (!domains.find(d => src.startsWith(d))) { + if (!domains.find((d: string) => src.startsWith(d))) { return { url: src } diff --git a/src/runtime/providers/storyblok.ts b/src/runtime/providers/storyblok.ts index 281451366..f5bd46d43 100644 --- a/src/runtime/providers/storyblok.ts +++ b/src/runtime/providers/storyblok.ts @@ -28,10 +28,10 @@ export const getImage: ProviderGetImage = (src, { modifiers = {}, baseURL = stor const _filters = Object.entries(filters || {}).map(e => `${e[0]}(${e[1]})`).join(':') const options = joinURL( - fit && `fit-${fit}`, - doResize ? `${width}x${height}` : undefined, - smart && 'smart', - _filters && ('filters:' + _filters) + fit ? `fit-${fit}` : '', + doResize ? `${width}x${height}` : '', + smart ? 'smart' : '', + _filters ? ('filters:' + _filters) : '' ) // TODO: check if hostname is https://a.storyblok.com ? diff --git a/src/runtime/providers/vercel.ts b/src/runtime/providers/vercel.ts index 73bc0f30d..7b46aaf11 100644 --- a/src/runtime/providers/vercel.ts +++ b/src/runtime/providers/vercel.ts @@ -7,8 +7,8 @@ export const getImage: ProviderGetImage = (src, { modifiers, baseURL = '/_vercel return { url: baseURL + '?' + stringifyQuery({ url: src, - w: String(modifiers.width), - q: String(modifiers.quality || '100') + w: modifiers?.width ? String(modifiers.width) : undefined, + q: String(modifiers?.quality || '100') }) } } diff --git a/src/runtime/utils/index.ts b/src/runtime/utils/index.ts index edc7f939f..e5e5cb0ef 100644 --- a/src/runtime/utils/index.ts +++ b/src/runtime/utils/index.ts @@ -4,7 +4,7 @@ export default function imageFetch (url: string) { return fetch(cleanDoubleSlashes(url)) } -export function getInt (x): number | undefined { +export function getInt (x: unknown): number | undefined { if (typeof x === 'number') { return x } @@ -15,7 +15,7 @@ export function getInt (x): number | undefined { } export function getFileExtension (url: string = '') { - const extension = url.split(/[?#]/).shift().split('/').pop().split('.').pop() + const extension = url.split(/[?#]/).shift()!.split('/').pop()!.split('.').pop()! return extension } @@ -24,8 +24,8 @@ export function cleanDoubleSlashes (path: string = '') { } export function createMapper (map: any) { - return (key: string) => { - return map[key] || key || map.missingValue + return (key?: string) => { + return key ? map[key] || key : map.missingValue } } @@ -36,10 +36,10 @@ export function createOperationsGenerator ({ formatter, keyMap, joinWith = '/', if (keyMap && typeof keyMap !== 'function') { keyMap = createMapper(keyMap) } - valueMap = valueMap || {} - Object.keys(valueMap).forEach((valueKey) => { - if (typeof valueMap[valueKey] !== 'function') { - valueMap[valueKey] = createMapper(valueMap[valueKey]) + const map = valueMap || {} + Object.keys(map).forEach((valueKey) => { + if (typeof map[valueKey] !== 'function') { + map[valueKey] = createMapper(map[valueKey]) } }) @@ -47,14 +47,14 @@ export function createOperationsGenerator ({ formatter, keyMap, joinWith = '/', const operations = Object.entries(modifiers) .filter(([_, value]) => typeof value !== 'undefined') .map(([key, value]) => { - const mapper = valueMap[key] + const mapper = map[key] if (typeof mapper === 'function') { value = mapper(modifiers[key]) } key = typeof keyMap === 'function' ? keyMap(key) : key - return formatter(key, value) + return formatter!(key, value) }) return operations.join(joinWith) @@ -78,10 +78,10 @@ export function renderTag (tag: string, attrs: Attrs, contents?: string) { } export function generateAlt (src: string = '') { - return src.split(/[?#]/).shift().split('/').pop().split('.').shift() + return src.split(/[?#]/).shift()!.split('/').pop()!.split('.').shift() } -export function parseSize (input: string | number = '') { +export function parseSize (input: string | number | undefined = '') { if (typeof input === 'number') { return input } diff --git a/src/runtime/utils/meta.ts b/src/runtime/utils/meta.ts index 595cf9b95..63b101d86 100644 --- a/src/runtime/utils/meta.ts +++ b/src/runtime/utils/meta.ts @@ -1,7 +1,7 @@ import type { ImageInfo, ImageCTX } from '../../types/image' -export async function imageMeta (ctx: ImageCTX, url): Promise { - const cache = getCache(ctx) +export async function imageMeta (ctx: ImageCTX, url: string): Promise { + const cache = getCache(ctx) const cacheKey = 'image:meta:' + url if (cache.has(cacheKey)) { @@ -22,51 +22,58 @@ export async function imageMeta (ctx: ImageCTX, url): Promise { return meta } -async function _imageMeta (url): Promise { - 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, - ratio: img.width / img.height - } - resolve(meta) - } - img.onerror = err => reject(err) - img.src = url - }) - } - +async function _imageMeta (url: string): Promise { if (process.server) { - const imageMeta = require('image-meta').default + const imageMeta = await import('image-meta').then(r => r.default || r) const data: Buffer = await fetch(url).then((res: any) => res.buffer()) - const { width, height } = await imageMeta(data) + const metadata = imageMeta(data) + if (!metadata) { + throw new Error(`No metadata could be extracted from the image \`${url}\`.`) + } + const { width, height } = metadata const meta = { - width, - height, - ratio: width / height + width: width!, + height: height!, + ratio: width && height ? width / height : undefined } return meta } + 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, + ratio: img.width / img.height + } + resolve(meta) + } + img.onerror = err => reject(err) + img.src = url + }) +} + +interface Cache { + get: (id: string) => T, + set: (id: string, value: T) => void, + has: (id: string) => boolean } -function getCache (ctx: ImageCTX) { +function getCache (ctx: ImageCTX): Cache { if (!ctx.nuxtContext.cache) { if (ctx.nuxtContext.ssrContext && ctx.nuxtContext.ssrContext.cache) { ctx.nuxtContext.cache = ctx.nuxtContext.ssrContext.cache } else { - const _cache = {} + const _cache: Record = {} ctx.nuxtContext.cache = { - get: id => _cache[id], - set: (id, value) => { _cache[id] = value }, - has: id => typeof _cache[id] !== 'undefined' + get: (id: string) => _cache[id], + set: (id: string, value: any) => { _cache[id] = value }, + has: (id: string) => typeof _cache[id] !== 'undefined' } } } diff --git a/src/runtime/utils/observer.ts b/src/runtime/utils/observer.ts index 26eb693f0..2dfb7e6b6 100644 --- a/src/runtime/utils/observer.ts +++ b/src/runtime/utils/observer.ts @@ -1,7 +1,9 @@ +/* eslint-disable no-use-before-define */ +type ObserverCallback = (event?: 'unsupported' | 'intersect') => any interface Observer { - add: (el, cb) => void - remove: (el) => void + add: (el: ObservedElement, cb: ObserverCallback) => void + remove: (el: ObservedElement) => void supported: boolean } @@ -9,6 +11,8 @@ let observerInstance: Observer let observerIdCtr = 1 const OBSERVER_ID_KEY = '__observer_id__' +type ObservedElement = Element & { [OBSERVER_ID_KEY]?: number | string } + export function getObserver (): Observer | false { if (typeof IntersectionObserver === 'undefined') { return false @@ -18,18 +22,19 @@ export function getObserver (): Observer | false { return observerInstance } - const elementCallbackMap = {} + const elementCallbackMap: Record = {} - const remove = (el) => { - if (!el[OBSERVER_ID_KEY]) { return } - delete elementCallbackMap[el[OBSERVER_ID_KEY]] + const remove = (el: ObservedElement) => { + const key = el[OBSERVER_ID_KEY] + if (!key) { return } + delete elementCallbackMap[key] delete el[OBSERVER_ID_KEY] intersectObserver.unobserve(el) } - const add = (el, fn) => { + const add = (el: ObservedElement, fn: () => any) => { el[OBSERVER_ID_KEY] = el[OBSERVER_ID_KEY] || ++observerIdCtr - elementCallbackMap[el[OBSERVER_ID_KEY]] = fn + elementCallbackMap[el[OBSERVER_ID_KEY]!] = fn intersectObserver.observe(el) } @@ -37,10 +42,10 @@ export function getObserver (): Observer | false { Object.values(elementCallbackMap).forEach((fn: any) => fn('print')) }) - const onMatch = (entries) => { + const onMatch = (entries: IntersectionObserverEntry[]) => { for (const entry of entries) { if (entry.isIntersecting) { - const fn = elementCallbackMap[entry.target[OBSERVER_ID_KEY]] + const fn = elementCallbackMap[(entry.target as ObservedElement)[OBSERVER_ID_KEY]!] if (typeof fn === 'function') { fn('intersect') } remove(entry.target) } @@ -52,7 +57,7 @@ export function getObserver (): Observer | false { return observerInstance } -export function useObserver (el, fn): Function { +export function useObserver (el: ObservedElement, fn: ObserverCallback): Function { const observer = getObserver() if (!observer) { fn('unsupported') @@ -62,7 +67,7 @@ export function useObserver (el, fn): Function { return () => observer.remove(el) } -function onPrint (fn) { +function onPrint (fn: ObserverCallback) { if (typeof window === 'undefined' || typeof window.matchMedia === 'undefined') { return } diff --git a/src/runtime/utils/static-map.ts b/src/runtime/utils/static-map.ts index 4bac085fa..7ed589497 100644 --- a/src/runtime/utils/static-map.ts +++ b/src/runtime/utils/static-map.ts @@ -1,3 +1,5 @@ +import type { Context } from '@nuxt/types' + const staticImageMap = {} function updateImageMap () { @@ -7,13 +9,13 @@ function updateImageMap () { } } -export function useStaticImageMap (nuxtContext?) { +export function useStaticImageMap (nuxtContext?: Context) { // Update on initialization updateImageMap() // Merge new mappings on route change if (nuxtContext) { - nuxtContext.app.router.afterEach(updateImageMap) + nuxtContext.app.router?.afterEach(updateImageMap) } // Make sure manifest is initialized diff --git a/src/types/global.d.ts b/src/types/global.ts similarity index 100% rename from src/types/global.d.ts rename to src/types/global.ts diff --git a/src/types/image.d.ts b/src/types/image.ts similarity index 92% rename from src/types/image.d.ts rename to src/types/image.ts index 324cf0973..f3be513e1 100644 --- a/src/types/image.d.ts +++ b/src/types/image.ts @@ -51,13 +51,22 @@ export interface ResolvedImage { getMeta?: () => Promise } -export interface $Img { +export interface ImageSizes { + srcset: string + sizes: string + src: string +} + +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[]) => { srcset: string, sizes: string } + getSizes: (source: string, options?: ImageOptions, sizes?: string[]) => ImageSizes getMeta: (source: string, options?: ImageOptions) => Promise - [preset: string]: $Img['options'] | $Img['getImage'] | $Img['getSizes'] | $Img['getMeta'] | $Img /* preset */ +} + +export type $Img = Img & { + [preset: string]: $Img } export interface ImageCTX { diff --git a/src/types/index.d.ts b/src/types/index.ts similarity index 100% rename from src/types/index.d.ts rename to src/types/index.ts diff --git a/src/types/module.d.ts b/src/types/module.ts similarity index 91% rename from src/types/module.d.ts rename to src/types/module.ts index 3557f2f3f..87dfb5c92 100644 --- a/src/types/module.d.ts +++ b/src/types/module.ts @@ -2,10 +2,10 @@ import type { IPXOptions } from 'ipx' import type { ImageOptions, CreateImageOptions } from './image' // eslint-disable-next-line no-use-before-define -export type ProviderSetup = (providerOptions: ImageModuleProvider, moduleOptions: ModuleOptions, nuxt) +export type ProviderSetup = (providerOptions: ImageModuleProvider, moduleOptions: ModuleOptions, nuxt: any) => void | Promise -export interface InputProvider { +export interface InputProvider { name?: string provider?: string options?: T @@ -34,6 +34,7 @@ export interface ModuleOptions extends ImageProviders { internalUrl: string intersectOptions: CreateImageOptions['intersectOptions'] providers: { [name: string]: InputProvider | any } & ImageProviders + [key: string]: any } export interface ImageModuleProvider { diff --git a/src/types/vue.ts b/src/types/vue.ts new file mode 100644 index 000000000..a0d06f53b --- /dev/null +++ b/src/types/vue.ts @@ -0,0 +1,13 @@ +import type Vue from 'vue' +import type { ThisTypedComponentOptionsWithRecordProps } from 'vue/types/options' +import type { ExtendedVue, VueConstructor } from 'vue/types/vue' + +export interface DefineMixin { + (options?: ThisTypedComponentOptionsWithRecordProps): Data & Methods & Computed & Props & VueConstructor +} + +export interface DefineComponentWithMixin { + // this is currently a hack - ideally we wouldn't need to duplicate this for multiple mixins + , Mixin2 extends Record>(options?: ThisTypedComponentOptionsWithRecordProps>, Methods, Computed, Props> & { mixins: [Mixin1, Mixin2] }): ExtendedVue; + >(options?: ThisTypedComponentOptionsWithRecordProps>, Methods, Computed, Props> & { mixins: Mixin[] }): ExtendedVue; +} diff --git a/tsconfig.json b/tsconfig.json index 5d02ac16a..e7a65e392 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,15 @@ { "compilerOptions": { + "strict": true, "baseUrl": ".", "moduleResolution": "Node", "esModuleInterop": true, "resolveJsonModule": true, - "types": ["node", "jest", "./src/types/global"], + "types": ["node", "jest"], "paths": { "~image/*": ["src/runtime/*"], "~image": ["src/runtime"] } - } + }, + "exclude": ["dist"] } diff --git a/yarn.lock b/yarn.lock index 2fae348a1..be5cd2208 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1813,6 +1813,13 @@ dependencies: "@types/webpack" "^4" +"@types/fs-extra@^9.0.11": + version "9.0.11" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.11.tgz#8cc99e103499eab9f347dbc6ca4e99fb8d2c2b87" + integrity sha512-mZsifGG4QeQ7hlkhO56u7zt/ycBgGxSVsFI/6lGTU34VtwkiqrrSDgw0+ygs8kFGWcXnFQWMrzF2h7TtDFNixA== + dependencies: + "@types/node" "*" + "@types/glob@^7.1.1": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" @@ -1896,6 +1903,11 @@ resolved "https://registry.yarnpkg.com/@types/less/-/less-3.0.2.tgz#2761d477678c8374cb9897666871662eb1d1115e" integrity sha512-62vfe65cMSzYaWmpmhqCMMNl0khen89w57mByPi1OseGfcV/LV03fO8YVrNj7rFQsRWNJo650WWyh6m7p8vZmA== +"@types/lru-cache@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03" + integrity sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w== + "@types/mime@^1": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" @@ -1911,6 +1923,14 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256" integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg== +"@types/node-fetch@^2.5.10": + version "2.5.10" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.10.tgz#9b4d4a0425562f9fcea70b12cb3fcdd946ca8132" + integrity sha512-IpkX0AasN44hgEad0gEF/V6EgR5n69VEqPEgnmoM8GsIGro3PowbWs4tR6IhxUTyPLpOn+fiGG6nrQhcmoCuIQ== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + "@types/node-sass@*": version "4.11.1" resolved "https://registry.yarnpkg.com/@types/node-sass/-/node-sass-4.11.1.tgz#bda27c5181cbf7c090c3058e119633dfb2b6504c" @@ -3665,7 +3685,7 @@ colorette@^1.2.1: resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== -combined-stream@^1.0.6, combined-stream@~1.0.6: +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== @@ -4878,6 +4898,11 @@ esbuild@^0.11.5: resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.11.5.tgz#25b18a2ff2fb9580683edce26a48f64c08c2f2df" integrity sha512-aRs6jAE+bVRp1tyfzUugAw1T/Y0Fwzp4Z2ROikF3h+UifoD5QlEbEYQGc6orNnnSIRhWR5VWBH7LozlAumaLHg== +esbuild@^0.11.6: + version "0.11.12" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.11.12.tgz#8cbe15bcb44212624c3e77c896a835f74dc71c3c" + integrity sha512-c8cso/1RwVj+fbDvLtUgSG4ZJQ0y9Zdrl6Ot/GAjyy4pdMCHaFnDMts5gqFnWRPLajWtEnI+3hlET4R9fVoZng== + esbuild@^0.8.56: version "0.8.57" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.57.tgz#a42d02bc2b57c70bcd0ef897fe244766bb6dd926" @@ -5580,6 +5605,15 @@ fork-ts-checker-webpack-plugin@^6.1.1: semver "^7.3.2" tapable "^1.0.0" +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -7306,11 +7340,16 @@ jest@latest: import-local "^3.0.2" jest-cli "^26.6.3" -jiti@^1.3.0, jiti@^1.6.3, jiti@^1.6.4: +jiti@^1.3.0, jiti@^1.6.4: version "1.6.4" resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.6.4.tgz#63453b602d0234f8bd7ce638f03f0e74ef99be12" integrity sha512-ICUtP0/rAyT/GaaDG0vj6fmWzx5yjFc7v+L1MAEARGl1+lrdJ8wtJNChr+ZGEdPoOhFwdhtcDO5VM2TNNgPpjQ== +jiti@^1.6.3, jiti@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.9.1.tgz#d9e267fa050ddc52191f17d8af815d49a38ebafd" + integrity sha512-AhYrAxJ/IW2257nHkJasUjtxHhmYIUEHEjsofJtGYsPWk8pTjqjbPFlJfOwfY+WX8YBiKHM1l0ViDC/mye2SWg== + joycon@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.0.1.tgz#9074c9b08ccf37a6726ff74a18485f85efcaddaf" @@ -7999,7 +8038,7 @@ micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.0, micromatch@^4.0.2: +micromatch@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== @@ -8007,6 +8046,14 @@ micromatch@^4.0.0, micromatch@^4.0.2: braces "^3.0.1" picomatch "^2.0.5" +micromatch@^4.0.2: + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + dependencies: + braces "^3.0.1" + picomatch "^2.2.3" + miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -8175,7 +8222,7 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mkdist@^0.1.3, mkdist@latest: +mkdist@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/mkdist/-/mkdist-0.1.3.tgz#46787c2493493d8eff1770700da23d5c4fc3d844" integrity sha512-WQ0l+v0ICvxvsmgi2MtePzeyis5kMDRUMnibUDsc1NdVSo+lsoiIYbLrvIBgKJGJ2ITMaLDtCAHiFjF3U7h29A== @@ -8189,6 +8236,20 @@ mkdist@^0.1.3, mkdist@latest: upath "^2.0.1" vue-template-compiler "^2.6.12" +mkdist@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/mkdist/-/mkdist-0.2.0.tgz#4dc2045328229bbe11ec1f1c20af49248ee1f606" + integrity sha512-eDTfFHlGqhMxC54PVH5RjSve4AkEzFwtazIvbDUejG4y6Cp/2b9IySObIZSlHetI76lmE1t/TuFJ54Ssg0HlCg== + dependencies: + defu "^3.2.2" + esbuild "^0.11.6" + fs-extra "^9.1.0" + globby "^11.0.3" + jiti "^1.9.1" + mri "^1.1.6" + upath "^2.0.1" + vue-template-compiler "^2.6.12" + modify-values@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" @@ -9028,11 +9089,16 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= -picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1, picomatch@^2.2.2: +picomatch@^2.0.4, picomatch@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +picomatch@^2.0.5, picomatch@^2.2.1, picomatch@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.3.tgz#465547f359ccc206d3c48e46a1bcb89bf7ee619d" + integrity sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg== + pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -12337,10 +12403,8 @@ watchpack@^1.7.4: resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453" integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ== dependencies: - chokidar "^3.4.1" graceful-fs "^4.1.2" neo-async "^2.5.0" - watchpack-chokidar2 "^2.0.1" optionalDependencies: chokidar "^3.4.1" watchpack-chokidar2 "^2.0.1"