diff --git a/knip.json b/knip.json index 3de9ffb9c..a35955013 100644 --- a/knip.json +++ b/knip.json @@ -9,7 +9,8 @@ "scripts/*" ], "ignoreUnresolved": [ - "#app/nuxt" + "#app/nuxt", + "#image-options" ], "ignoreDependencies": [ "vitest-environment-nuxt" diff --git a/playground/server/api/image.ts b/playground/server/api/image.ts new file mode 100644 index 000000000..0b4da2ef1 --- /dev/null +++ b/playground/server/api/image.ts @@ -0,0 +1,10 @@ +export default defineEventHandler(async (event) => { + const image = useImage(event) + return image.getImage('/image.jpg', { + provider: 'ipx', + modifiers: { + format: 'webp', + quality: 75, + }, + }) +}) diff --git a/src/module.ts b/src/module.ts index 1d251cb85..4082b21b7 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,10 +1,10 @@ import process from 'node:process' import { parseURL, withLeadingSlash } from 'ufo' -import { defineNuxtModule, addTemplate, addImports, createResolver, addComponent, addPlugin } from '@nuxt/kit' +import { defineNuxtModule, addTemplate, addImports, addServerImports, createResolver, addComponent, addPlugin, addServerTemplate } from '@nuxt/kit' import { resolve } from 'pathe' import { resolveProviders, detectProvider, resolveProvider } from './provider' -import type { ImageProviders, ImageOptions, InputProvider, CreateImageOptions } from './types' +import type { ImageProviders, ImageOptions, InputProvider, CreateImageOptions, ImageModuleProvider } from './types' export interface ModuleOptions extends ImageProviders { inject: boolean @@ -121,15 +121,21 @@ export default defineNuxtModule({ addTemplate({ filename: 'image-options.mjs', getContents() { - return ` -${providers.map(p => `import * as ${p.importName} from '${p.runtime}'`).join('\n')} + return generateImageOptions(providers, imageOptions) + }, + }) -export const imageOptions = ${JSON.stringify(imageOptions, null, 2)} + addServerImports([ + { + name: 'useImage', + from: resolver.resolve('runtime/server/utils/image'), + }, + ]) -imageOptions.providers = { -${providers.map(p => ` ['${p.name}']: { provider: ${p.importName}, defaults: ${JSON.stringify(p.runtimeOptions)} }`).join(',\n')} -} - ` + addServerTemplate({ + filename: '#image-options', + getContents() { + return generateImageOptions(providers, imageOptions) }, }) @@ -180,3 +186,15 @@ function pick, K extends keyof O>(obj: O, keys: K[]): } return newobj } + +function generateImageOptions(providers: ImageModuleProvider[], imageOptions: Omit): string { + return ` + ${providers.map(p => `import * as ${p.importName} from '${p.runtime}'`).join('\n')} + + export const imageOptions = ${JSON.stringify(imageOptions, null, 2)} + + imageOptions.providers = { + ${providers.map(p => ` ['${p.name}']: { provider: ${p.importName}, defaults: ${JSON.stringify(p.runtimeOptions)} }`).join(',\n')} + } + ` +} diff --git a/src/runtime/components/NuxtImg.vue b/src/runtime/components/NuxtImg.vue index 55b8e367a..301ab9499 100644 --- a/src/runtime/components/NuxtImg.vue +++ b/src/runtime/components/NuxtImg.vue @@ -33,7 +33,7 @@ import { prerenderStaticImages } from '../utils/prerender' import { markFeatureUsage } from '../utils/performance' import { imgProps, useBaseImage } from './_base' -import { useHead } from '#imports' +import { useHead, useRequestEvent } from '#imports' import { useNuxtApp } from '#app/nuxt' const props = defineProps(imgProps) @@ -142,7 +142,7 @@ if (import.meta.server && props.preload) { // Prerender static images if (import.meta.server && import.meta.prerender) { - prerenderStaticImages(src.value, sizes.value.srcset) + prerenderStaticImages(src.value, sizes.value.srcset, useRequestEvent()) } const nuxtApp = useNuxtApp() diff --git a/src/runtime/components/NuxtPicture.vue b/src/runtime/components/NuxtPicture.vue index c19817de8..506f5ad02 100644 --- a/src/runtime/components/NuxtPicture.vue +++ b/src/runtime/components/NuxtPicture.vue @@ -34,7 +34,7 @@ import { getFileExtension } from '../utils' import { useImage } from '../composables' import { useBaseImage, pictureProps, baseImageProps } from './_base' -import { useHead } from '#imports' +import { useHead, useRequestEvent } from '#imports' import { useNuxtApp } from '#app/nuxt' const props = defineProps(pictureProps) @@ -135,7 +135,7 @@ const imgEl = ref() // Prerender static images if (import.meta.server && import.meta.prerender) { for (const src of sources.value) { - prerenderStaticImages(src.src, src.srcset) + prerenderStaticImages(src.src, src.srcset, useRequestEvent()) } } diff --git a/src/runtime/composables.ts b/src/runtime/composables.ts index 254e6c6b9..4d5ff788e 100644 --- a/src/runtime/composables.ts +++ b/src/runtime/composables.ts @@ -1,15 +1,17 @@ +import type { H3Event } from 'h3' import type { $Img } from '../module' import { createImage } from './image' import { imageOptions } from '#build/image-options.mjs' import { useNuxtApp, useRuntimeConfig } from '#imports' -export const useImage = (): $Img => { +export const useImage = (event?: H3Event): $Img => { const config = useRuntimeConfig() const nuxtApp = useNuxtApp() return nuxtApp.$img as $Img || nuxtApp._img || (nuxtApp._img = createImage({ ...imageOptions, + event: event || nuxtApp.ssrContext?.event, nuxt: { baseURL: config.app.baseURL, }, diff --git a/src/runtime/image.ts b/src/runtime/image.ts index b7742ba58..f2dc61f66 100644 --- a/src/runtime/image.ts +++ b/src/runtime/image.ts @@ -14,8 +14,8 @@ export function createImage(globalOptions: CreateImageOptions) { const image = resolveImage(ctx, input, options) // Prerender static images - if (import.meta.server && import.meta.prerender) { - prerenderStaticImages(image.url) + if (import.meta.server && import.meta.prerender && globalOptions.event) { + prerenderStaticImages(image.url, undefined, globalOptions.event) } return image diff --git a/src/runtime/server/utils/image.ts b/src/runtime/server/utils/image.ts new file mode 100644 index 000000000..9d4ec12c8 --- /dev/null +++ b/src/runtime/server/utils/image.ts @@ -0,0 +1,21 @@ +import type { H3Event } from 'h3' + +import type { Img } from '../../../module' +import { createImage } from '../../image' + +// @ts-expect-error virtual file +import { imageOptions } from '#image-options' +import { useRuntimeConfig } from '#imports' + +export const useImage = (event?: H3Event): Img => { + const config = useRuntimeConfig() + + return createImage({ + ...imageOptions, + nuxt: { + baseURL: config.app.baseURL, + }, + event, + runtimeConfig: config, + }) +} diff --git a/src/runtime/utils/prerender.ts b/src/runtime/utils/prerender.ts index dfc018c1a..4a768785a 100644 --- a/src/runtime/utils/prerender.ts +++ b/src/runtime/utils/prerender.ts @@ -1,8 +1,8 @@ +import type { H3Event } from 'h3' import { appendHeader } from 'h3' -import { useRequestEvent } from '#imports' -export function prerenderStaticImages(src = '', srcset = '') { - if (!import.meta.server || !import.meta.prerender) { +export function prerenderStaticImages(src = '', srcset = '', event?: H3Event) { + if (!import.meta.server || !import.meta.prerender || !event) { return } @@ -15,5 +15,5 @@ export function prerenderStaticImages(src = '', srcset = '') { return } - appendHeader(useRequestEvent()!, 'x-nitro-prerender', paths.map(p => encodeURIComponent(p)).join(', ')) + appendHeader(event, 'x-nitro-prerender', paths.map(p => encodeURIComponent(p)).join(', ')) } diff --git a/src/types/image.ts b/src/types/image.ts index 5f17c4773..9ca5813a8 100644 --- a/src/types/image.ts +++ b/src/types/image.ts @@ -1,4 +1,5 @@ import type { RuntimeConfig } from '@nuxt/schema' +import type { H3Event } from 'h3' export interface ImageModifiers { width: number @@ -39,6 +40,7 @@ export interface CreateImageOptions { nuxt: { baseURL: string } + event?: H3Event presets: { [name: string]: ImageOptions } provider: string screens: Record diff --git a/test/e2e/ssr.test.ts b/test/e2e/ssr.test.ts index 3d58e3fbf..24f81cb81 100644 --- a/test/e2e/ssr.test.ts +++ b/test/e2e/ssr.test.ts @@ -1,7 +1,7 @@ import { fileURLToPath } from 'node:url' import { describe, it, expect } from 'vitest' -import { setup, createPage, url, fetch } from '@nuxt/test-utils' +import { $fetch, setup, createPage, url, fetch } from '@nuxt/test-utils' import { providers } from '../../playground/providers' @@ -69,4 +69,13 @@ describe('browser (ssr: true)', () => { const res = await fetch(url('/_ipx/s_300x300/images/colors.jpg')) expect(res.headers.get('content-type')).toBe('image/jpeg') }) + + it('works with server-side useImage', async () => { + expect(await $fetch('/api/image')).toMatchInlineSnapshot(` + { + "format": "webp", + "url": "/_ipx/f_webp&q_75/image.jpg", + } + `) + }) })