Skip to content

Commit ee7ceda

Browse files
feat: Add ability to disable ipx (#1653)
* feat: add env var to disable IPX function includes some domain path and test updates * docs: update README * feat: add sharp package to address prod error when ipx is removed, need to use sharp for image optimization * refactor: move check for env var into setupImageFunction ensure basePath redirect is still added * refactor: include sharp module when ipx is disabled don't add ipx function to netlifyConfig if ipx is disabled * style: lint * refactor: serve 404 when ipx is disabled and no custom loader is specified * refactor: update image loader logic * test: confirming behaviour * refactor: remove redirect * refactor: remove build command temporarily * test: add tests also do general cleanup * style: lint * docs: update README based on CR feedback * refactor: remove unnnecessary logic Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 28a0cd6 commit ee7ceda

File tree

12 files changed

+102
-46
lines changed

12 files changed

+102
-46
lines changed

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@ by targeting the `/_next/image/*` route:
4242
X-Test = 'foobar'
4343
```
4444

45+
## Disabling included image loader
46+
47+
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`.
48+
49+
This should only be done if the site is not using `next/image` or is using a different loader (such as Cloudinary or Imgix).
50+
51+
See the [Next.js documentation](https://nextjs.org/docs/api-reference/next/image#built-in-loaders) for image loader options.
52+
4553
## Next.js Middleware on Netlify
4654

4755
Next.js Middleware works out of the box on Netlify. By default, middleware runs using Netlify Edge Functions. For legacy

demos/canary/pages/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default function Home() {
1717
</main>
1818

1919
<Image
20-
src="https://raw.githubusercontent.com/netlify/next-runtime/main/next-on-netlify.png"
20+
src="https://raw.githubusercontent.com/netlify/next-runtime/main/demos/default/public/next-on-netlify.png"
2121
alt="Picture of the author"
2222
width={540}
2323
height={191}

demos/default/netlify.toml

-3
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ NODE_VERSION = "16.15.1"
1818
Strict-Transport-Security = "max-age=31536000"
1919
X-Test = 'foobar'
2020

21-
[dev]
22-
framework = "#static"
23-
2421
[[plugins]]
2522
package = "../plugin-wrapper/"
2623

demos/default/next.config.js

+7-9
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,18 @@ module.exports = {
7171
},
7272
// https://nextjs.org/docs/basic-features/image-optimization#domains
7373
images: {
74-
domains: ['raw.githubusercontent.com'],
74+
domains: ['raw.githubusercontent.com', 'upload.wikimedia.org'],
75+
remotePatterns: [
76+
{
77+
hostname: '*.imgur.com',
78+
}
79+
]
7580
},
7681
// https://nextjs.org/docs/basic-features/built-in-css-support#customizing-sass-options
7782
sassOptions: {
7883
includePaths: [path.join(__dirname, 'styles-sass-test')],
7984
},
8085
experimental: {
8186
optimizeCss: false,
82-
images: {
83-
remotePatterns: [
84-
{
85-
hostname: '*.imgur.com',
86-
},
87-
],
88-
},
89-
},
87+
}
9088
}

demos/default/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@
3535
"npm-run-all": "^4.1.5",
3636
"typescript": "^4.6.3"
3737
}
38-
}
38+
}

demos/default/pages/image.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const Images = () => (
1515
<p>
1616
<Image src={logo} alt="netlify logomark" />
1717
<Image
18-
src="https://raw.githubusercontent.com/netlify/next-runtime/main/next-on-netlify.png"
18+
src="https://raw.githubusercontent.com/netlify/next-runtime/main/demos/default/public/next-on-netlify.png"
1919
alt="Picture of the author"
2020
width={500}
2121
height={500}

packages/runtime/src/helpers/config.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { NetlifyConfig } from '@netlify/build'
2+
import destr from 'destr'
23
import { readJSON, writeJSON } from 'fs-extra'
34
import type { Header } from 'next/dist/lib/load-custom-routes'
45
import type { NextConfigComplete } from 'next/dist/server/config-shared'
@@ -85,8 +86,10 @@ export const configureHandlerFunctions = async ({ netlifyConfig, publish, ignore
8586
const cssFilesToInclude = files.filter((f) => f.startsWith(`${publish}/static/css/`))
8687

8788
/* eslint-disable no-underscore-dangle */
88-
netlifyConfig.functions._ipx ||= {}
89-
netlifyConfig.functions._ipx.node_bundler = 'nft'
89+
if (!destr(process.env.DISABLE_IPX)) {
90+
netlifyConfig.functions._ipx ||= {}
91+
netlifyConfig.functions._ipx.node_bundler = 'nft'
92+
}
9093

9194
/* eslint-enable no-underscore-dangle */
9295
;[HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME].forEach((functionName) => {

packages/runtime/src/helpers/edge.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,11 @@ export const writeEdgeFunctions = async (netlifyConfig: NetlifyConfig) => {
207207
await copy(getEdgeTemplatePath('../edge-shared'), join(edgeFunctionRoot, 'edge-shared'))
208208
await writeJSON(join(edgeFunctionRoot, 'edge-shared', 'nextConfig.json'), nextConfig)
209209

210-
if (!destr(process.env.NEXT_DISABLE_EDGE_IMAGES) && !destr(process.env.NEXT_DISABLE_NETLIFY_EDGE)) {
210+
if (
211+
!destr(process.env.NEXT_DISABLE_EDGE_IMAGES) &&
212+
!destr(process.env.NEXT_DISABLE_NETLIFY_EDGE) &&
213+
!destr(process.env.DISABLE_IPX)
214+
) {
211215
console.log(
212216
'Using Netlify Edge Functions for image format detection. Set env var "NEXT_DISABLE_EDGE_IMAGES=true" to disable.',
213217
)

packages/runtime/src/helpers/functions.ts

+42-26
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { NetlifyConfig, NetlifyPluginConstants } from '@netlify/build'
22
import bridgeFile from '@vercel/node-bridge'
3+
import destr from 'destr'
34
import { copyFile, ensureDir, writeFile, writeJSON } from 'fs-extra'
45
import type { ImageConfigComplete, RemotePattern } from 'next/dist/shared/lib/image-config'
56
import { join, relative, resolve } from 'pathe'
@@ -55,7 +56,7 @@ export const generatePagesResolver = async ({
5556

5657
// Move our next/image function into the correct functions directory
5758
export const setupImageFunction = async ({
58-
constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC = DEFAULT_FUNCTIONS_SRC },
59+
constants: { INTERNAL_FUNCTIONS_SRC, FUNCTIONS_SRC = DEFAULT_FUNCTIONS_SRC, IS_LOCAL },
5960
imageconfig = {},
6061
netlifyConfig,
6162
basePath,
@@ -69,35 +70,50 @@ export const setupImageFunction = async ({
6970
remotePatterns: RemotePattern[]
7071
responseHeaders?: Record<string, string>
7172
}): Promise<void> => {
72-
const functionsPath = INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC
73-
const functionName = `${IMAGE_FUNCTION_NAME}.js`
74-
const functionDirectory = join(functionsPath, IMAGE_FUNCTION_NAME)
73+
const imagePath = imageconfig.path || '/_next/image'
7574

76-
await ensureDir(functionDirectory)
77-
await writeJSON(join(functionDirectory, 'imageconfig.json'), {
78-
...imageconfig,
79-
basePath: [basePath, IMAGE_FUNCTION_NAME].join('/'),
80-
remotePatterns,
81-
responseHeaders,
82-
})
83-
await copyFile(join(__dirname, '..', '..', 'lib', 'templates', 'ipx.js'), join(functionDirectory, functionName))
75+
if (destr(process.env.DISABLE_IPX)) {
76+
// If no image loader is specified, need to redirect to a 404 page since there's no
77+
// backing loader to serve local site images once deployed to Netlify
78+
if (!IS_LOCAL && imageconfig.loader === 'default') {
79+
netlifyConfig.redirects.push({
80+
from: `${imagePath}*`,
81+
query: { url: ':url', w: ':width', q: ':quality' },
82+
to: '/404.html',
83+
status: 404,
84+
force: true,
85+
})
86+
}
87+
} else {
88+
const functionsPath = INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC
89+
const functionName = `${IMAGE_FUNCTION_NAME}.js`
90+
const functionDirectory = join(functionsPath, IMAGE_FUNCTION_NAME)
8491

85-
const imagePath = imageconfig.path || '/_next/image'
92+
await ensureDir(functionDirectory)
93+
await writeJSON(join(functionDirectory, 'imageconfig.json'), {
94+
...imageconfig,
95+
basePath: [basePath, IMAGE_FUNCTION_NAME].join('/'),
96+
remotePatterns,
97+
responseHeaders,
98+
})
8699

87-
// If we have edge functions then the request will have already been rewritten
88-
// so this won't match. This is matched if edge is disabled or unavailable.
89-
netlifyConfig.redirects.push({
90-
from: `${imagePath}*`,
91-
query: { url: ':url', w: ':width', q: ':quality' },
92-
to: `${basePath}/${IMAGE_FUNCTION_NAME}/w_:width,q_:quality/:url`,
93-
status: 301,
94-
})
100+
await copyFile(join(__dirname, '..', '..', 'lib', 'templates', 'ipx.js'), join(functionDirectory, functionName))
95101

96-
netlifyConfig.redirects.push({
97-
from: `${basePath}/${IMAGE_FUNCTION_NAME}/*`,
98-
to: `/.netlify/builders/${IMAGE_FUNCTION_NAME}`,
99-
status: 200,
100-
})
102+
// If we have edge functions then the request will have already been rewritten
103+
// so this won't match. This is matched if edge is disabled or unavailable.
104+
netlifyConfig.redirects.push({
105+
from: `${imagePath}*`,
106+
query: { url: ':url', w: ':width', q: ':quality' },
107+
to: `${basePath}/${IMAGE_FUNCTION_NAME}/w_:width,q_:quality/:url`,
108+
status: 301,
109+
})
110+
111+
netlifyConfig.redirects.push({
112+
from: `${basePath}/${IMAGE_FUNCTION_NAME}/*`,
113+
to: `/.netlify/builders/${IMAGE_FUNCTION_NAME}`,
114+
status: 200,
115+
})
116+
}
101117

102118
if (basePath) {
103119
// next/image generates image static URLs that still point at the site root

packages/runtime/src/helpers/utils.ts

+1
Original file line numberDiff line numberDiff line change
@@ -228,4 +228,5 @@ export const getRemotePatterns = (experimental: ExperimentalConfigWithLegacy, im
228228
}
229229
return []
230230
}
231+
231232
/* eslint-enable max-lines */

test/helpers/utils.spec.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ describe('getRemotePatterns', () => {
5959
formats: [ 'image/avif', 'image/webp' ],
6060
dangerouslyAllowSVG: false,
6161
contentSecurityPolicy: "script-src 'none'; frame-src 'none'; sandbox;",
62-
unoptimized: false
62+
unoptimized: false,
63+
remotePatterns: []
6364
} as ImagesConfig
6465

6566
})

test/index.js

+29-1
Original file line numberDiff line numberDiff line change
@@ -565,13 +565,41 @@ describe('onBuild()', () => {
565565
const imageConfigPath = path.join(constants.INTERNAL_FUNCTIONS_SRC, IMAGE_FUNCTION_NAME, 'imageconfig.json')
566566
const imageConfigJson = await readJson(imageConfigPath)
567567

568-
expect(imageConfigJson.domains.length).toBe(1)
568+
expect(imageConfigJson.domains.length).toBe(2)
569569
expect(imageConfigJson.remotePatterns.length).toBe(1)
570570
expect(imageConfigJson.responseHeaders).toStrictEqual({
571571
'X-Foo': mockHeaderValue,
572572
})
573573
})
574574

575+
test('generates an ipx function by default', async () => {
576+
await moveNextDist()
577+
await nextRuntime.onBuild(defaultArgs)
578+
expect(existsSync(path.join('.netlify', 'functions-internal', '_ipx', '_ipx.js'))).toBeTruthy()
579+
})
580+
581+
test('does not generate an ipx function when DISABLE_IPX is set', async () => {
582+
process.env.DISABLE_IPX = '1'
583+
await moveNextDist()
584+
await nextRuntime.onBuild(defaultArgs)
585+
expect(existsSync(path.join('.netlify', 'functions-internal', '_ipx', '_ipx.js'))).toBeFalsy()
586+
delete process.env.DISABLE_IPX
587+
})
588+
589+
test('creates 404 redirect when DISABLE_IPX is set', async () => {
590+
process.env.DISABLE_IPX = '1'
591+
await moveNextDist()
592+
await nextRuntime.onBuild(defaultArgs)
593+
const nextImageRedirect = netlifyConfig.redirects.find(redirect => redirect.from.includes('/_next/image'))
594+
595+
expect(nextImageRedirect).toBeDefined()
596+
expect(nextImageRedirect.to).toEqual("/404.html")
597+
expect(nextImageRedirect.status).toEqual(404)
598+
expect(nextImageRedirect.force).toEqual(true)
599+
600+
delete process.env.DISABLE_IPX
601+
})
602+
575603
test('generates an ipx edge function by default', async () => {
576604
await moveNextDist()
577605
await nextRuntime.onBuild(defaultArgs)

0 commit comments

Comments
 (0)