diff --git a/README.md b/README.md index f0ed64bc7f..a36da4a0fb 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,14 @@ by targeting the `/_next/image/*` route: X-Test = 'foobar' ``` +## Disabling included image loader + +If you wish to disable the use of the image loader which is bundled into the runtime by default, set the `DISABLE_IPX` environment variable to `true`. + +This should only be done if the site is not using `next/image` or is using a different loader (such as Cloudinary or Imgix). + +See the [Next.js documentation](https://nextjs.org/docs/api-reference/next/image#built-in-loaders) for image loader options. + ## Next.js Middleware on Netlify Next.js Middleware works out of the box on Netlify. By default, middleware runs using Netlify Edge Functions. For legacy diff --git a/demos/canary/pages/index.js b/demos/canary/pages/index.js index 33b7ca0774..ba410683af 100755 --- a/demos/canary/pages/index.js +++ b/demos/canary/pages/index.js @@ -17,7 +17,7 @@ export default function Home() { Picture of the author (

netlify logomark Picture of the author f.startsWith(`${publish}/static/css/`)) /* eslint-disable no-underscore-dangle */ - netlifyConfig.functions._ipx ||= {} - netlifyConfig.functions._ipx.node_bundler = 'nft' + if (!destr(process.env.DISABLE_IPX)) { + netlifyConfig.functions._ipx ||= {} + netlifyConfig.functions._ipx.node_bundler = 'nft' + } /* eslint-enable no-underscore-dangle */ ;[HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME].forEach((functionName) => { diff --git a/packages/runtime/src/helpers/edge.ts b/packages/runtime/src/helpers/edge.ts index 3829420f4b..ce2b381a4d 100644 --- a/packages/runtime/src/helpers/edge.ts +++ b/packages/runtime/src/helpers/edge.ts @@ -207,7 +207,11 @@ export const writeEdgeFunctions = async (netlifyConfig: NetlifyConfig) => { await copy(getEdgeTemplatePath('../edge-shared'), join(edgeFunctionRoot, 'edge-shared')) await writeJSON(join(edgeFunctionRoot, 'edge-shared', 'nextConfig.json'), nextConfig) - if (!destr(process.env.NEXT_DISABLE_EDGE_IMAGES) && !destr(process.env.NEXT_DISABLE_NETLIFY_EDGE)) { + if ( + !destr(process.env.NEXT_DISABLE_EDGE_IMAGES) && + !destr(process.env.NEXT_DISABLE_NETLIFY_EDGE) && + !destr(process.env.DISABLE_IPX) + ) { console.log( 'Using Netlify Edge Functions for image format detection. Set env var "NEXT_DISABLE_EDGE_IMAGES=true" to disable.', ) diff --git a/packages/runtime/src/helpers/functions.ts b/packages/runtime/src/helpers/functions.ts index f0d407510f..e0bbfa5d47 100644 --- a/packages/runtime/src/helpers/functions.ts +++ b/packages/runtime/src/helpers/functions.ts @@ -1,5 +1,6 @@ import type { NetlifyConfig, NetlifyPluginConstants } from '@netlify/build' import bridgeFile from '@vercel/node-bridge' +import destr from 'destr' import { copyFile, ensureDir, writeFile, writeJSON } from 'fs-extra' import type { ImageConfigComplete, RemotePattern } from 'next/dist/shared/lib/image-config' import { join, relative, resolve } from 'pathe' @@ -55,7 +56,7 @@ export const generatePagesResolver = async ({ // Move our next/image function into the correct functions directory export const setupImageFunction = async ({ - constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC = DEFAULT_FUNCTIONS_SRC }, + constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC = DEFAULT_FUNCTIONS_SRC, IS_LOCAL }, imageconfig = {}, netlifyConfig, basePath, @@ -69,35 +70,50 @@ export const setupImageFunction = async ({ remotePatterns: RemotePattern[] responseHeaders?: Record }): Promise => { - const functionsPath = INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC - const functionName = `${IMAGE_FUNCTION_NAME}.js` - const functionDirectory = join(functionsPath, IMAGE_FUNCTION_NAME) + const imagePath = imageconfig.path || '/_next/image' - await ensureDir(functionDirectory) - await writeJSON(join(functionDirectory, 'imageconfig.json'), { - ...imageconfig, - basePath: [basePath, IMAGE_FUNCTION_NAME].join('/'), - remotePatterns, - responseHeaders, - }) - await copyFile(join(__dirname, '..', '..', 'lib', 'templates', 'ipx.js'), join(functionDirectory, functionName)) + if (destr(process.env.DISABLE_IPX)) { + // If no image loader is specified, need to redirect to a 404 page since there's no + // backing loader to serve local site images once deployed to Netlify + if (!IS_LOCAL && imageconfig.loader === 'default') { + netlifyConfig.redirects.push({ + from: `${imagePath}*`, + query: { url: ':url', w: ':width', q: ':quality' }, + to: '/404.html', + status: 404, + force: true, + }) + } + } else { + const functionsPath = INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC + const functionName = `${IMAGE_FUNCTION_NAME}.js` + const functionDirectory = join(functionsPath, IMAGE_FUNCTION_NAME) - const imagePath = imageconfig.path || '/_next/image' + await ensureDir(functionDirectory) + await writeJSON(join(functionDirectory, 'imageconfig.json'), { + ...imageconfig, + basePath: [basePath, IMAGE_FUNCTION_NAME].join('/'), + remotePatterns, + responseHeaders, + }) - // If we have edge functions then the request will have already been rewritten - // so this won't match. This is matched if edge is disabled or unavailable. - netlifyConfig.redirects.push({ - from: `${imagePath}*`, - query: { url: ':url', w: ':width', q: ':quality' }, - to: `${basePath}/${IMAGE_FUNCTION_NAME}/w_:width,q_:quality/:url`, - status: 301, - }) + await copyFile(join(__dirname, '..', '..', 'lib', 'templates', 'ipx.js'), join(functionDirectory, functionName)) - netlifyConfig.redirects.push({ - from: `${basePath}/${IMAGE_FUNCTION_NAME}/*`, - to: `/.netlify/builders/${IMAGE_FUNCTION_NAME}`, - status: 200, - }) + // If we have edge functions then the request will have already been rewritten + // so this won't match. This is matched if edge is disabled or unavailable. + netlifyConfig.redirects.push({ + from: `${imagePath}*`, + query: { url: ':url', w: ':width', q: ':quality' }, + to: `${basePath}/${IMAGE_FUNCTION_NAME}/w_:width,q_:quality/:url`, + status: 301, + }) + + netlifyConfig.redirects.push({ + from: `${basePath}/${IMAGE_FUNCTION_NAME}/*`, + to: `/.netlify/builders/${IMAGE_FUNCTION_NAME}`, + status: 200, + }) + } if (basePath) { // next/image generates image static URLs that still point at the site root diff --git a/packages/runtime/src/helpers/utils.ts b/packages/runtime/src/helpers/utils.ts index 89416d2f4b..5a3525c4b5 100644 --- a/packages/runtime/src/helpers/utils.ts +++ b/packages/runtime/src/helpers/utils.ts @@ -228,4 +228,5 @@ export const getRemotePatterns = (experimental: ExperimentalConfigWithLegacy, im } return [] } + /* eslint-enable max-lines */ diff --git a/test/helpers/utils.spec.ts b/test/helpers/utils.spec.ts index c19622507a..ae56d3d933 100644 --- a/test/helpers/utils.spec.ts +++ b/test/helpers/utils.spec.ts @@ -59,7 +59,8 @@ describe('getRemotePatterns', () => { formats: [ 'image/avif', 'image/webp' ], dangerouslyAllowSVG: false, contentSecurityPolicy: "script-src 'none'; frame-src 'none'; sandbox;", - unoptimized: false + unoptimized: false, + remotePatterns: [] } as ImagesConfig }) diff --git a/test/index.js b/test/index.js index c5eb0b4f54..c3046db7bd 100644 --- a/test/index.js +++ b/test/index.js @@ -565,13 +565,41 @@ describe('onBuild()', () => { const imageConfigPath = path.join(constants.INTERNAL_FUNCTIONS_SRC, IMAGE_FUNCTION_NAME, 'imageconfig.json') const imageConfigJson = await readJson(imageConfigPath) - expect(imageConfigJson.domains.length).toBe(1) + expect(imageConfigJson.domains.length).toBe(2) expect(imageConfigJson.remotePatterns.length).toBe(1) expect(imageConfigJson.responseHeaders).toStrictEqual({ 'X-Foo': mockHeaderValue, }) }) + test('generates an ipx function by default', async () => { + await moveNextDist() + await nextRuntime.onBuild(defaultArgs) + expect(existsSync(path.join('.netlify', 'functions-internal', '_ipx', '_ipx.js'))).toBeTruthy() + }) + + test('does not generate an ipx function when DISABLE_IPX is set', async () => { + process.env.DISABLE_IPX = '1' + await moveNextDist() + await nextRuntime.onBuild(defaultArgs) + expect(existsSync(path.join('.netlify', 'functions-internal', '_ipx', '_ipx.js'))).toBeFalsy() + delete process.env.DISABLE_IPX + }) + + test('creates 404 redirect when DISABLE_IPX is set', async () => { + process.env.DISABLE_IPX = '1' + await moveNextDist() + await nextRuntime.onBuild(defaultArgs) + const nextImageRedirect = netlifyConfig.redirects.find(redirect => redirect.from.includes('/_next/image')) + + expect(nextImageRedirect).toBeDefined() + expect(nextImageRedirect.to).toEqual("/404.html") + expect(nextImageRedirect.status).toEqual(404) + expect(nextImageRedirect.force).toEqual(true) + + delete process.env.DISABLE_IPX + }) + test('generates an ipx edge function by default', async () => { await moveNextDist() await nextRuntime.onBuild(defaultArgs)