Skip to content

Commit 3e5ac6e

Browse files
Afshar07danielroe
andauthored
feat: add support for image helpers in nitro endpoints (#1473)
Co-authored-by: Daniel Roe <daniel@roe.dev>
1 parent a12c220 commit 3e5ac6e

File tree

11 files changed

+86
-22
lines changed

11 files changed

+86
-22
lines changed

knip.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"scripts/*"
1010
],
1111
"ignoreUnresolved": [
12-
"#app/nuxt"
12+
"#app/nuxt",
13+
"#internal/nuxt-image"
1314
],
1415
"ignoreDependencies": [
1516
"vitest-environment-nuxt"

playground/server/api/image.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export default defineEventHandler(async (event) => {
2+
const image = useImage(event)
3+
return image.getImage('/image.jpg', {
4+
provider: 'ipx',
5+
modifiers: {
6+
format: 'webp',
7+
quality: 75,
8+
},
9+
})
10+
})

src/module.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import process from 'node:process'
22

33
import { parseURL, withLeadingSlash } from 'ufo'
4-
import { defineNuxtModule, addTemplate, addImports, createResolver, addComponent, addPlugin } from '@nuxt/kit'
4+
import { defineNuxtModule, addTemplate, addImports, addServerImports, createResolver, addComponent, addPlugin, addServerTemplate } from '@nuxt/kit'
55
import { resolve } from 'pathe'
66
import { resolveProviders, detectProvider, resolveProvider } from './provider'
7-
import type { ImageProviders, ImageOptions, InputProvider, CreateImageOptions } from './types'
7+
import type { ImageProviders, ImageOptions, InputProvider, CreateImageOptions, ImageModuleProvider } from './types'
88

99
export interface ModuleOptions extends ImageProviders {
1010
inject: boolean
@@ -121,15 +121,21 @@ export default defineNuxtModule<ModuleOptions>({
121121
addTemplate({
122122
filename: 'image-options.mjs',
123123
getContents() {
124-
return `
125-
${providers.map(p => `import * as ${p.importName} from '${p.runtime}'`).join('\n')}
124+
return generateImageOptions(providers, imageOptions)
125+
},
126+
})
126127

127-
export const imageOptions = ${JSON.stringify(imageOptions, null, 2)}
128+
addServerImports([
129+
{
130+
name: 'useImage',
131+
from: resolver.resolve('runtime/server/utils/image'),
132+
},
133+
])
128134

129-
imageOptions.providers = {
130-
${providers.map(p => ` ['${p.name}']: { provider: ${p.importName}, defaults: ${JSON.stringify(p.runtimeOptions)} }`).join(',\n')}
131-
}
132-
`
135+
addServerTemplate({
136+
filename: '#internal/nuxt-image',
137+
getContents() {
138+
return generateImageOptions(providers, imageOptions)
133139
},
134140
})
135141

@@ -180,3 +186,16 @@ function pick<O extends Record<any, any>, K extends keyof O>(obj: O, keys: K[]):
180186
}
181187
return newobj
182188
}
189+
190+
function generateImageOptions(providers: ImageModuleProvider[], imageOptions: Omit<CreateImageOptions, 'providers' | 'nuxt' | 'runtimeConfig'>): string {
191+
return `
192+
${providers.map(p => `import * as ${p.importName} from '${p.runtime}'`).join('\n')}
193+
194+
export const imageOptions = {
195+
...${JSON.stringify(imageOptions, null, 2)},
196+
providers: {
197+
${providers.map(p => ` ['${p.name}']: { provider: ${p.importName}, defaults: ${JSON.stringify(p.runtimeOptions)} }`).join(',\n')}
198+
}
199+
}
200+
`
201+
}

src/runtime/components/NuxtImg.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { prerenderStaticImages } from '../utils/prerender'
3333
import { markFeatureUsage } from '../utils/performance'
3434
import { imgProps, useBaseImage } from './_base'
3535
36-
import { useHead } from '#imports'
36+
import { useHead, useRequestEvent } from '#imports'
3737
import { useNuxtApp } from '#app/nuxt'
3838
3939
const props = defineProps(imgProps)
@@ -142,7 +142,7 @@ if (import.meta.server && props.preload) {
142142
143143
// Prerender static images
144144
if (import.meta.server && import.meta.prerender) {
145-
prerenderStaticImages(src.value, sizes.value.srcset)
145+
prerenderStaticImages(src.value, sizes.value.srcset, useRequestEvent())
146146
}
147147
148148
const nuxtApp = useNuxtApp()

src/runtime/components/NuxtPicture.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { getFileExtension } from '../utils'
3434
import { useImage } from '../composables'
3535
import { useBaseImage, pictureProps, baseImageProps } from './_base'
3636
37-
import { useHead } from '#imports'
37+
import { useHead, useRequestEvent } from '#imports'
3838
import { useNuxtApp } from '#app/nuxt'
3939
4040
const props = defineProps(pictureProps)
@@ -135,7 +135,7 @@ const imgEl = ref<HTMLImageElement>()
135135
// Prerender static images
136136
if (import.meta.server && import.meta.prerender) {
137137
for (const src of sources.value) {
138-
prerenderStaticImages(src.src, src.srcset)
138+
prerenderStaticImages(src.src, src.srcset, useRequestEvent())
139139
}
140140
}
141141

src/runtime/composables.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1+
import type { H3Event } from 'h3'
12
import type { $Img } from '../module'
23

34
import { createImage } from './image'
45
import { imageOptions } from '#build/image-options.mjs'
56
import { useNuxtApp, useRuntimeConfig } from '#imports'
67

7-
export const useImage = (): $Img => {
8+
export const useImage = (event?: H3Event): $Img => {
89
const config = useRuntimeConfig()
910
const nuxtApp = useNuxtApp()
1011

1112
return nuxtApp.$img as $Img || nuxtApp._img || (nuxtApp._img = createImage({
1213
...imageOptions,
14+
event: event || nuxtApp.ssrContext?.event,
1315
nuxt: {
1416
baseURL: config.app.baseURL,
1517
},

src/runtime/image.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ export function createImage(globalOptions: CreateImageOptions) {
1414
const image = resolveImage(ctx, input, options)
1515

1616
// Prerender static images
17-
if (import.meta.server && import.meta.prerender) {
18-
prerenderStaticImages(image.url)
17+
if (import.meta.server && import.meta.prerender && globalOptions.event) {
18+
prerenderStaticImages(image.url, undefined, globalOptions.event)
1919
}
2020

2121
return image

src/runtime/server/utils/image.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { H3Event } from 'h3'
2+
3+
import type { Img } from '../../../module'
4+
import { createImage } from '../../image'
5+
6+
// @ts-expect-error virtual file
7+
import { imageOptions } from '#internal/nuxt-image'
8+
import { useRuntimeConfig } from '#imports'
9+
10+
export const useImage = (event?: H3Event): Img => {
11+
const config = useRuntimeConfig()
12+
13+
return createImage({
14+
...imageOptions,
15+
nuxt: {
16+
baseURL: config.app.baseURL,
17+
},
18+
event,
19+
runtimeConfig: config,
20+
})
21+
}

src/runtime/utils/prerender.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import type { H3Event } from 'h3'
12
import { appendHeader } from 'h3'
2-
import { useRequestEvent } from '#imports'
33

4-
export function prerenderStaticImages(src = '', srcset = '') {
5-
if (!import.meta.server || !import.meta.prerender) {
4+
export function prerenderStaticImages(src = '', srcset = '', event?: H3Event) {
5+
if (!import.meta.server || !import.meta.prerender || !event) {
66
return
77
}
88

@@ -15,5 +15,5 @@ export function prerenderStaticImages(src = '', srcset = '') {
1515
return
1616
}
1717

18-
appendHeader(useRequestEvent()!, 'x-nitro-prerender', paths.map(p => encodeURIComponent(p)).join(', '))
18+
appendHeader(event, 'x-nitro-prerender', paths.map(p => encodeURIComponent(p)).join(', '))
1919
}

src/types/image.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { RuntimeConfig } from '@nuxt/schema'
2+
import type { H3Event } from 'h3'
23

34
export interface ImageModifiers {
45
width: number
@@ -39,6 +40,7 @@ export interface CreateImageOptions {
3940
nuxt: {
4041
baseURL: string
4142
}
43+
event?: H3Event
4244
presets: { [name: string]: ImageOptions }
4345
provider: string
4446
screens: Record<string, number>

test/e2e/ssr.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { fileURLToPath } from 'node:url'
22

33
import { describe, it, expect } from 'vitest'
4-
import { setup, createPage, url, fetch } from '@nuxt/test-utils'
4+
import { $fetch, setup, createPage, url, fetch } from '@nuxt/test-utils'
55

66
import { providers } from '../../playground/providers'
77

@@ -69,4 +69,13 @@ describe('browser (ssr: true)', () => {
6969
const res = await fetch(url('/_ipx/s_300x300/images/colors.jpg'))
7070
expect(res.headers.get('content-type')).toBe('image/jpeg')
7171
})
72+
73+
it('works with server-side useImage', async () => {
74+
expect(await $fetch('/api/image')).toMatchInlineSnapshot(`
75+
{
76+
"format": "webp",
77+
"url": "/_ipx/f_webp&q_75/image.jpg",
78+
}
79+
`)
80+
})
7281
})

0 commit comments

Comments
 (0)