Repository: nuxt/image
Pull request #840 (2023-05-31 14:24:12)

feat: add `none` provider and only enable `ipx` when node present

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *nuxt-image-v1* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | e711efb1dc8dc384ed77082ec361a2e8c27691fb |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/nuxt-image-v1/deploys/6477588ff15d160008b0a6ad |

Patch:
diff --git a/package.json b/package.json
index bfea54a..c93d400 100755
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
     "node-fetch-native": "^1.1.1",
     "ohash": "^1.1.2",
     "pathe": "^1.1.1",
+    "std-env": "^3.3.3",
     "ufo": "^1.1.2"
   },
   "devDependencies": {
diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts
index 5382f54..065beb2 100644
--- a/playground/nuxt.config.ts
+++ b/playground/nuxt.config.ts
@@ -11,6 +11,7 @@ export default defineNuxtConfig({
     screens: {
       750: 750
     },
+    none: {},
     alias: {
       unsplash: 'https://images.unsplash.com', // ipx
       blog: '/remote/nuxt-org/blog' // cloudinary
diff --git a/playground/providers.ts b/playground/providers.ts
index dbe5bd8..cc30b08 100644
--- a/playground/providers.ts
+++ b/playground/providers.ts
@@ -11,6 +11,40 @@ export interface Provider {
 }
 
 export const providers: Provider[] = [
+  // null provider (for non-node environments)
+  {
+    name: 'none',
+    samples: [
+      {
+        src: '/images/colors.jpg',
+        from: 'Jeremy Thomas',
+        width: 300,
+        height: 300,
+        link: 'https://unsplash.com/@jeremythomasphoto?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText'
+      },
+      {
+        src: '/images/everest.jpg',
+        from: 'Mount Everest Wikipedia page (alias)',
+        width: 300,
+        height: 300,
+        link: 'https://en.wikipedia.org/wiki/Mount_Everest'
+      },
+      {
+        src: '/images/tacos.svg',
+        from: 'Illustration from Icons8',
+        width: 300,
+        height: 300,
+        link: 'https://icons8.com/illustrations/illustration/abstract-1419'
+      },
+      {
+        src: '/unsplash/photo-1606112219348-204d7d8b94ee',
+        from: 'Photo by Omid Armin',
+        width: 300,
+        height: 300,
+        link: 'https://unsplash.com/@omidarmin?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText'
+      }
+    ]
+  },
   // IPX
   {
     name: 'ipx',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index eb5410b..ae7b3a1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -36,6 +36,9 @@ importers:
       pathe:
         specifier: ^1.1.1
         version: 1.1.1
+      std-env:
+        specifier: ^3.3.3
+        version: 3.3.3
       ufo:
         specifier: ^1.1.2
         version: 1.1.2
diff --git a/src/ipx.ts b/src/ipx.ts
index 0112698..4b774f9 100644
--- a/src/ipx.ts
+++ b/src/ipx.ts
@@ -1,10 +1,12 @@
 import { relative, resolve } from 'pathe'
 import { eventHandler } from 'h3'
-import { useNuxt, createResolver } from '@nuxt/kit'
+import { useNuxt, createResolver, useNitro } from '@nuxt/kit'
+import type { NitroEventHandler, NitroDevEventHandler } from 'nitropack'
 
 import type { ProviderSetup, ImageProviders } from './types'
 
 export const ipxSetup: ProviderSetup = async (providerOptions, moduleOptions) => {
+  const nitro = useNitro()
   const nuxt = useNuxt()
 
   // Add IPX middleware unless nuxtrc or user added a custom middleware
@@ -28,20 +30,22 @@ export const ipxSetup: ProviderSetup = async (providerOptions, moduleOptions) =>
   if (!nuxt.options.dev) {
     // TODO: Avoid adding for non-Node.js environments with a warning
     const resolver = createResolver(import.meta.url)
-    nuxt.hook('nitro:init', (nitro) => {
-      // Use absolute path for prerenderer
-      // TODO: Workaround for prerender support
-      // https://github.com/nuxt/image/pull/784
-      nitro.options._config.runtimeConfig = nitro.options._config.runtimeConfig || {}
-      nitro.options._config.runtimeConfig.ipx = { ...ipxOptions }
-      // Use relative path for built app
-      ipxOptions.dir = relative(nitro.options.output.serverDir, nitro.options.output.publicDir)
-      nitro.options.runtimeConfig.ipx = ipxOptions
-    })
-    nuxt.options.serverHandlers.push({
+    // Use absolute path for prerenderer
+    // TODO: Workaround for prerender support
+    // https://github.com/nuxt/image/pull/784
+    nitro.options._config.runtimeConfig = nitro.options._config.runtimeConfig || {}
+    nitro.options._config.runtimeConfig.ipx = { ...ipxOptions }
+    // Use relative path for built app
+    ipxOptions.dir = relative(nitro.options.output.serverDir, nitro.options.output.publicDir)
+    nitro.options.runtimeConfig.ipx = ipxOptions
+
+    const handler: NitroEventHandler = {
       route: '/_ipx/**',
       handler: resolver.resolve('./runtime/ipx')
-    })
+    }
+    nitro.options.handlers.push(handler)
+    // TODO: Workaround for prerender support
+    nitro.options._config.handlers!.push(handler)
     return
   }
 
@@ -54,10 +58,13 @@ export const ipxSetup: ProviderSetup = async (providerOptions, moduleOptions) =>
     })
   const ipx = createIPX(ipxOptions)
   const middleware = createIPXMiddleware(ipx)
-  nuxt.options.devServerHandlers.push({
+  const devHandler: NitroDevEventHandler = {
     route: '/_ipx',
     handler: eventHandler(async (event) => {
       await middleware(event.node.req, event.node.res)
     })
-  })
+  }
+  nitro.options.devHandlers.push(devHandler)
+  // TODO: Workaround for prerender support
+  nitro.options._config.devHandlers!.push(devHandler)
 }
diff --git a/src/module.ts b/src/module.ts
index 13d0787..68c8f11 100644
--- a/src/module.ts
+++ b/src/module.ts
@@ -1,7 +1,7 @@
 import { parseURL, withLeadingSlash } from 'ufo'
 import { defineNuxtModule, addTemplate, addImports, createResolver, addComponent, addPlugin } from '@nuxt/kit'
 import { resolve } from 'pathe'
-import { resolveProviders, detectProvider } from './provider'
+import { resolveProviders, detectProvider, resolveProvider } from './provider'
 import type { ImageProviders, ImageOptions, InputProvider, CreateImageOptions } from './types'
 
 export interface ModuleOptions extends ImageProviders {
@@ -64,8 +64,10 @@ export default defineNuxtModule<ModuleOptions>({
     // Normalize alias to start with leading slash
     options.alias = Object.fromEntries(Object.entries(options.alias).map(e => [withLeadingSlash(e[0]), e[1]]))
 
-    options.provider = detectProvider(options.provider)
-    options[options.provider] = options[options.provider] || {}
+    options.provider = detectProvider(options.provider)!
+    if (options.provider) {
+      options[options.provider] = options[options.provider] || {}
+    }
 
     const imageOptions: Omit<CreateImageOptions, 'providers' | 'nuxt'> = pick(options, [
       'screens',
@@ -79,7 +81,7 @@ export default defineNuxtModule<ModuleOptions>({
 
     // Run setup
     for (const p of providers) {
-      if (typeof p.setup === 'function') {
+      if (typeof p.setup === 'function' && p.name !== 'ipx') {
         await p.setup(p, options, nuxt)
       }
     }
@@ -120,6 +122,23 @@ ${providers.map(p => `  ['${p.name}']: { provider: ${p.importName}, defaults: ${
       }
     })
 
+    nuxt.hook('nitro:init', async (nitro) => {
+      if (!options.provider || options.provider === 'ipx') {
+        imageOptions.provider = options.provider = nitro.options.node ? 'ipx' : 'none'
+        options[options.provider] = options[options.provider] || {}
+
+        if (options.provider === 'none') { return }
+
+        const p = await resolveProvider(nuxt, 'ipx', options.ipx as {})
+        if (!providers.some(p => p.name === 'ipx')) {
+          providers.push(p)
+        }
+        if (typeof p.setup === 'function') {
+          await p.setup(p, options, nuxt)
+        }
+      }
+    })
+
     // Add runtime plugin
     addPlugin({ src: resolver.resolve('./runtime/plugin') })
 
diff --git a/src/provider.ts b/src/provider.ts
index cfa655e..2cb8221 100644
--- a/src/provider.ts
+++ b/src/provider.ts
@@ -4,6 +4,7 @@ import type { Nuxt } from '@nuxt/schema'
 import type { NitroConfig } from 'nitropack'
 import { createResolver, resolvePath } from '@nuxt/kit'
 import { hash } from 'ohash'
+import { provider } from 'std-env'
 import type { InputProvider, ImageModuleProvider, ProviderSetup } from './types'
 import type { ModuleOptions } from './module'
 import { ipxSetup } from './ipx'
@@ -24,6 +25,7 @@ const BuiltInProviders = [
   'ipx',
   'layer0',
   'netlify',
+  'none',
   'prismic',
   'sanity',
   'storyblok',
@@ -108,9 +110,7 @@ export function detectProvider (userInput?: string) {
     return userInput
   }
 
-  if (process.env.VERCEL || process.env.VERCEL_ENV || process.env.NOW_BUILDER) {
+  if (provider === 'vercel') {
     return 'vercel'
   }
-
-  return 'ipx'
 }
diff --git a/src/runtime/providers/none.ts b/src/runtime/providers/none.ts
new file mode 100644
index 0000000..f0ed771
--- /dev/null
+++ b/src/runtime/providers/none.ts
@@ -0,0 +1,3 @@
+import type { ProviderGetImage } from '../../types'
+
+export const getImage: ProviderGetImage = url => ({ url })


Test patch:
diff --git a/test/e2e/__snapshots__/no-ssr.test.ts.snap b/test/e2e/__snapshots__/no-ssr.test.ts.snap
index 05ce120..4df2025 100644
--- a/test/e2e/__snapshots__/no-ssr.test.ts.snap
+++ b/test/e2e/__snapshots__/no-ssr.test.ts.snap
@@ -206,6 +206,24 @@ exports[`browser (ssr: false) > netlify should render images 2`] = `
 ]
 `;
 
+exports[`browser (ssr: false) > none should render images 1`] = `
+[
+  "/images/colors.jpg",
+  "/images/everest.jpg",
+  "/images/tacos.svg",
+  "https://images.unsplash.com/photo-1606112219348-204d7d8b94ee",
+]
+`;
+
+exports[`browser (ssr: false) > none should render images 2`] = `
+[
+  "/images/colors.jpg",
+  "/images/everest.jpg",
+  "/images/tacos.svg",
+  "https://images.unsplash.com/photo-1606112219348-204d7d8b94ee",
+]
+`;
+
 exports[`browser (ssr: false) > prismic should render images 1`] = `
 [
   "https://images.prismic.io/200629-sms-hoy/f596a543-d593-4296-9abd-3d3ac15f1e39_ray-hennessy-mpw37yXc_WQ-unsplash.jpg?auto=compress,format&w=600&h=900",
diff --git a/test/e2e/__snapshots__/ssr.test.ts.snap b/test/e2e/__snapshots__/ssr.test.ts.snap
index 69c5d5f..ac87b1f 100644
--- a/test/e2e/__snapshots__/ssr.test.ts.snap
+++ b/test/e2e/__snapshots__/ssr.test.ts.snap
@@ -206,6 +206,24 @@ exports[`browser (ssr: true) > netlify should render images 2`] = `
 ]
 `;
 
+exports[`browser (ssr: true) > none should render images 1`] = `
+[
+  "/images/colors.jpg",
+  "/images/everest.jpg",
+  "/images/tacos.svg",
+  "https://images.unsplash.com/photo-1606112219348-204d7d8b94ee",
+]
+`;
+
+exports[`browser (ssr: true) > none should render images 2`] = `
+[
+  "/images/colors.jpg",
+  "/images/everest.jpg",
+  "/images/tacos.svg",
+  "https://images.unsplash.com/photo-1606112219348-204d7d8b94ee",
+]
+`;
+
 exports[`browser (ssr: true) > prismic should render images 1`] = `
 [
   "https://images.prismic.io/200629-sms-hoy/f596a543-d593-4296-9abd-3d3ac15f1e39_ray-hennessy-mpw37yXc_WQ-unsplash.jpg?auto=compress,format&w=600&h=900",
diff --git a/test/providers.ts b/test/providers.ts
index 1eb819a..f7b368a 100644
--- a/test/providers.ts
+++ b/test/providers.ts
@@ -1,6 +1,7 @@
 export const images = [
   {
     args: ['/test.png', {}],
+    none: { url: '/test.png' },
     ipx: { url: '/_ipx/_/test.png' },
     cloudflare: { url: '/test.png' },
     cloudinary: { url: '/f_auto,q_auto/test' },
@@ -23,6 +24,7 @@ export const images = [
   },
   {
     args: ['/test.png', { width: 200 }],
+    none: { url: '/test.png' },
     ipx: { url: '/_ipx/w_200/test.png' },
     cloudflare: { url: '/cdn-cgi/image/w=200/test.png' },
     cloudinary: { url: '/f_auto,q_auto,w_200/test' },
@@ -45,6 +47,7 @@ export const images = [
   },
   {
     args: ['/test.png', { height: 200 }],
+    none: { url: '/test.png' },
     ipx: { url: '/_ipx/h_200/test.png' },
     cloudflare: { url: '/cdn-cgi/image/h=200/test.png' },
     cloudinary: { url: '/f_auto,q_auto,h_200/test' },
@@ -67,6 +70,7 @@ export const images = [
   },
   {
     args: ['/test.png', { width: 200, height: 200 }],
+    none: { url: '/test.png' },
     ipx: { url: '/_ipx/s_200x200/test.png' },
     cloudflare: { url: '/cdn-cgi/image/w=200,h=200/test.png' },
     cloudinary: { url: '/f_auto,q_auto,w_200,h_200/test' },
@@ -89,6 +93,7 @@ export const images = [
   },
   {
     args: ['/test.png', { width: 200, height: 200, fit: 'contain' }],
+    none: { url: '/test.png' },
     ipx: { url: '/_ipx/fit_contain&s_200x200/test.png' },
     cloudflare: { url: '/cdn-cgi/image/w=200,h=200,fit=contain/test.png' },
     cloudinary: { url: '/f_auto,q_auto,w_200,h_200,c_scale/test' },
@@ -111,6 +116,7 @@ export const images = [
   },
   {
     args: ['/test.png', { width: 200, height: 200, fit: 'contain', format: 'jpeg' }],
+    none: { url: '/test.png' },
     ipx: { url: '/_ipx/fit_contain&f_jpeg&s_200x200/test.png' },
     cloudflare: { url: '/cdn-cgi/image/w=200,h=200,fit=contain,f=jpeg/test.png' },
     cloudinary: { url: '/f_jpg,q_auto,w_200,h_200,c_scale/test' },
diff --git a/test/unit/providers.test.ts b/test/unit/providers.test.ts
index 18197aa..9a751bc 100644
--- a/test/unit/providers.test.ts
+++ b/test/unit/providers.test.ts
@@ -6,6 +6,7 @@ import { images } from '../providers'
 
 import { cleanDoubleSlashes } from '#image/utils'
 import * as ipx from '#image/providers/ipx'
+import * as none from '~/src/runtime/providers/none'
 import * as cloudflare from '#image/providers/cloudflare'
 import * as cloudinary from '#image/providers/cloudinary'
 import * as twicpics from '#image/providers/twicpics'
@@ -351,4 +352,15 @@ describe('Providers', () => {
       expect(generated).toMatchObject(image.vercel)
     }
   })
+
+  it('none', () => {
+    const providerOptions = {
+    }
+
+    for (const image of images) {
+      const [src, modifiers] = image.args
+      const generated = none.getImage(src, { modifiers, ...providerOptions }, emptyContext)
+      expect(generated).toMatchObject(image.none)
+    }
+  })
 })