Skip to content

Commit

Permalink
feat: fix full static support
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Jan 15, 2021
1 parent dd2bf78 commit caaff86
Show file tree
Hide file tree
Showing 12 changed files with 119 additions and 140 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nuxt/image",
"version": "0.2.0-alpha.5",
"version": "0.2.0-alpha.6",
"description": "Nuxt Image Module",
"repository": "nuxt/image",
"license": "MIT",
Expand All @@ -24,6 +24,7 @@
"allowlist": "^0.1.1",
"consola": "^2.15.0",
"defu": "^3.2.2",
"fs-extra": "^9.0.1",
"hasha": "^5.2.2",
"image-meta": "^0.0.1",
"ipx": "^0.4.8",
Expand Down
2 changes: 1 addition & 1 deletion playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { } from '../src/types'

export default <NuxtConfig> {
components: true,
target: 'server',
target: 'static',
modules: [
'../src/module.ts'
],
Expand Down
41 changes: 21 additions & 20 deletions src/generate.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,41 @@

import { createWriteStream, promises as fsp } from 'fs'
import { createWriteStream } from 'fs'
import { promisify } from 'util'
import stream from 'stream'
import { join } from 'upath'
import { mkdirp } from 'fs-extra'
import { dirname, join, relative, resolve, extname } from 'upath'
import fetch from 'node-fetch'
import { joinURL, hasProtocol } from 'ufo'
import { ModuleOptions } from './types'
import { getFileExtension } from './runtime/utils'
import { joinURL, hasProtocol, parseURL } from 'ufo'
import { ModuleOptions, MapToStatic, ResolvedImage } from './types'
import { hash, logger } from './utils'

const pipeline = promisify(stream.pipeline)

export function setupStaticGeneration (nuxt: any, options: ModuleOptions) {
const staticImages = {} // url ~> hash
const staticImages = {} // url ~> hashed file name

nuxt.hook('vue-renderer:ssr:prepareContext', (renderContext) => {
renderContext.isGenerating = true
renderContext.mapImage = ({ url, isStatic, format, src }) => {
if (!isStatic) {
return url
}
renderContext.image = renderContext.image || {}
renderContext.image.mapToStatic = <MapToStatic> function ({ url, format }: ResolvedImage) {
if (!staticImages[url]) {
format = format || getFileExtension(src)
staticImages[url] = '_image/' + hash(url) + '.' + format
const ext = (format && `.${format}`) || extname(parseURL(url).pathname) || '.png'
staticImages[url] = hash(url) + ext
}
return nuxt.options.router.base + staticImages[url]
return staticImages[url]
}
})

nuxt.hook('generate:done', async () => {
const { dir: generateDir } = nuxt.options.generate
try { await fsp.mkdir(join(generateDir, '_image')) } catch { }

const downloads = Object.entries(staticImages).map(([url, name]) => {
if (!hasProtocol(url)) {
url = joinURL(options.internalUrl, url)
}
return downloadImage({ url, name, outDir: generateDir })
return downloadImage({
url,
name,
outDir: resolve(generateDir, '_nuxt/image' /* TODO: staticImagesBase */)
})
})
await Promise.all(downloads)
})
Expand All @@ -45,9 +44,11 @@ export function setupStaticGeneration (nuxt: any, options: ModuleOptions) {
async function downloadImage ({ url, name, outDir }) {
try {
const response = await fetch(url)
if (!response.ok) { throw new Error(`unexpected response ${response.statusText}`) }
await pipeline(response.body, createWriteStream(join(outDir, name)))
logger.success('Generated image ' + name)
if (!response.ok) { throw new Error(`Unexpected response ${response.statusText}`) }
const dstFile = join(outDir, name)
await mkdirp(dirname(dstFile))
await pipeline(response.body, createWriteStream(dstFile))
logger.success('Generated static image ' + relative(process.cwd(), dstFile))
} catch (error) {
logger.error(error.message)
}
Expand Down
7 changes: 6 additions & 1 deletion src/ipx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ export function createIPXMiddleware (options) {
const ipx = new IPX({
inputs: [
{
name: 'default',
name: 'remote',
adapter: 'remote',
accept: [/.*/]
},
{
name: 'static',
adapter: 'fs',
dir: options.dir
}
],
cache: {
Expand Down
16 changes: 8 additions & 8 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ async function imageModule (moduleOptions: ModuleOptions) {
const { nuxt, addPlugin, addServerMiddleware } = this

const defaults: ModuleOptions = {
provider: 'local',
provider: 'static',
presets: [],
local: {
baseURL: '/_img/ipx',
dir: nuxt.options.dir.static,
static: {
baseURL: '/_img',
dir: resolve(nuxt.options.srcDir, nuxt.options.dir.static),
clearCache: false,
cacheDir: 'node_modules/.cache/nuxt-image', /* TODO */
accept: [],
Expand All @@ -23,13 +23,13 @@ async function imageModule (moduleOptions: ModuleOptions) {
sizes: [320, 420, 768, 1024, 1200, 1600],
internalUrl: '',
providers: {},
accept: ['http://localhost'],
accept: [],
intersectOptions: {}
}

const options: ModuleOptions = defu(moduleOptions, nuxt.options.image, defaults)

options.provider = process.env.NUXT_IMAGE_PROVIDER || options.provider || 'local'
options.provider = process.env.NUXT_IMAGE_PROVIDER || options.provider || 'static'

const imageOptions = pick(options, ['sizes', 'presets', 'provider', 'intersectOptions', 'accept'])
const providers = await resolveProviders(nuxt, options)
Expand All @@ -50,8 +50,8 @@ async function imageModule (moduleOptions: ModuleOptions) {
})

addServerMiddleware({
path: options.local.baseURL,
handle: createIPXMiddleware(options.local)
path: options.static.baseURL,
handle: createIPXMiddleware(options.static)
})

nuxt.options.build.loaders = defu({
Expand Down
3 changes: 2 additions & 1 deletion src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const BuiltInProviders = [
'fastly',
'imagekit',
'imgix',
'local',
'ipx',
'static',
'twicpics'
]

Expand Down
101 changes: 29 additions & 72 deletions src/runtime/image.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,9 @@
import { allowList } from 'allowlist'
import type { Matcher } from 'allowlist'
import { hasProtocol, joinURL } from 'ufo'
import requrl from 'requrl'
import type { ImageOptions, CreateImageOptions, ResolvedImage } from '../types/image'
import type { ImageOptions, CreateImageOptions, ResolvedImage, MapToStatic, ImageCTX } from '../types/image'
import { imageMeta } from './utils/meta'
import { parseSize } from './utils'

export interface ImageCTX {
options: CreateImageOptions,
allow: Matcher<any>
nuxtContext: {
ssrContext: any
cache?: any
isDev: boolean
isStatic: boolean
nuxtState?: any
}
$img?: Function
}

export function createImage (globalOptions: CreateImageOptions, nuxtContext) {
const ctx: ImageCTX = {
options: globalOptions,
Expand All @@ -27,43 +12,30 @@ export function createImage (globalOptions: CreateImageOptions, nuxtContext) {
}

function $img (input: string, options: ImageOptions = {}) {
const { image } = resolveImage(ctx, input, options)
const image = resolveImage(ctx, input, options)

// Full static
// @ts-ignore
if (typeof window !== 'undefined' && window.$nuxt && window.$nuxt._pagePayload) {
// @ts-ignore
const jsonPData = window.$nuxt._pagePayload.data[0]
if (jsonPData.nuxtImageMap[input]) {
// Hydration with hash
image.url = jsonPData.nuxtImageMap[input]
} else if (image.isStatic) {
image.url = input
}
// Return original source on cache fail in full static mode
return image
}
if (process.static && image.isStatic) {
const staticImagesBase = '/_nuxt/image' // TODO

// Client-Side rendering without server
if (typeof window !== 'undefined' && !ctx.nuxtContext.isDev && ctx.nuxtContext.isStatic && image.isStatic) {
image.url = input
return image
}
if (process.client && 'fetchPayload' in window.$nuxt) {
const mappedURL = (window.$nuxt as any)?._pagePayload?.pagePayload?.data?.[0]?._img[image.url]
image.url = mappedURL || input
return image
}

const nuxtState = ctx.nuxtContext.nuxtState || ctx.nuxtContext.ssrContext.nuxt
if (!nuxtState.data || !nuxtState.data.length) { nuxtState.data = [{}] }
const data = nuxtState.data[0]
data.nuxtImageMap = data.nuxtImageMap || {}

if (data.nuxtImageMap[image.url]) {
// Hydration with hash
image.url = data.nuxtImageMap[image.url]
} else if (typeof ctx.nuxtContext.ssrContext?.mapImage === 'function') {
// Full Static
const mappedUrl = ctx.nuxtContext.ssrContext?.mapImage({ input, image, options })
if (mappedUrl) {
image.url = mappedUrl
data.nuxtImageMap[image.url] = mappedUrl
if (process.server) {
const { ssrContext } = ctx.nuxtContext
const ssrData = ssrContext.nuxt.data[0]
const staticImages = ssrData._img = ssrData._img || {}
const mapToStatic: MapToStatic = ssrContext.image?.mapToStatic
if (typeof mapToStatic === 'function') {
const mappedURL = mapToStatic(image)
if (mappedURL) {
staticImages[image.url] = mappedURL
image.url = joinURL(staticImagesBase, mappedURL)
}
}
}
}

Expand All @@ -86,17 +58,13 @@ export function createImage (globalOptions: CreateImageOptions, nuxtContext) {
}

async function getMeta (ctx: ImageCTX, input: string, options: ImageOptions) {
const { image } = resolveImage(ctx, input, { ...options })
const image = resolveImage(ctx, input, { ...options })

const meta = {}

if (typeof image.getMeta === 'function') {
Object.assign(meta, await image.getMeta())
} else {
if (process.server && !hasProtocol(image.url)) {
const url = requrl(ctx.nuxtContext.ssrContext.req)
image.url = joinURL(url, image.url) // TODO: Is modification safe?
}
Object.assign(meta, await imageMeta(ctx, image.url))
}

Expand All @@ -110,39 +78,28 @@ function resolveImage (ctx: ImageCTX, input: string, options: ImageOptions): Res

if (input.startsWith('data:') || (hasProtocol(input) && !ctx.allow(input))) {
return {
input,
provider: null,
preset: null,
image: {
url: input,
isStatic: false
}
url: input
}
}

const { provider, defaults } = getProvider(ctx, options.provider || ctx.options.provider)
const preset = getPreset(ctx, options.preset)

const _options = { ...defaults, ...options }
const _options = { ...defaults, ...preset, ...options }
if (_options.modifiers?.width) {
_options.modifiers.width = parseSize(_options.modifiers.width)
}
if (_options.modifiers?.height) {
_options.modifiers.height = parseSize(_options.modifiers.height)
}
const image = provider.getImage(input, _options)

if (process.server && !hasProtocol(image.url)) {
const url = requrl(ctx.nuxtContext.ssrContext.req)
image.url = joinURL(url, image.url)
}
const image = provider.getImage(input, _options, ctx)

return {
input,
provider,
preset,
image
if (_options.modifiers?.format && !image.format) {
image.format = _options.modifiers.format
}

return image
}

function getProvider (ctx: ImageCTX, name: string): ImageCTX['options']['providers'][0] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,11 @@ export const getImage: ProviderGetImage = (src, { modifiers = {}, baseURL = '/'
delete modifiers.height
}

src = hasProtocol(src) ? src : joinURL('http://localhost:3000', src)

const operationsString = operationsGenerator(modifiers) || '_'

const type = hasProtocol(src) ? 'remote' : 'static'

return {
url: joinURL(baseURL, 'default', format, operationsString, encodeURIComponent(src)),
isStatic: true
url: joinURL(baseURL, type, format, operationsString, src)
}
}
6 changes: 6 additions & 0 deletions src/runtime/providers/static.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { getImage as _getImage } from './ipx'

export const getImage: typeof _getImage = (src, options, ctx) => ({
..._getImage(src, options, ctx),
isStatic: true
})
3 changes: 1 addition & 2 deletions src/runtime/utils/meta.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { ImageInfo } from '../../types/image'
import type { ImageCTX } from '../image'
import type { ImageInfo, ImageCTX } from '../../types/image'

export async function imageMeta (ctx: ImageCTX, url): Promise<ImageInfo> {
const cache = getCache(ctx)
Expand Down
Loading

0 comments on commit caaff86

Please sign in to comment.