Skip to content

feat: Add ability to disable ipx #1653

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion demos/canary/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function Home() {
</main>

<Image
src="https://raw.githubusercontent.com/netlify/next-runtime/main/next-on-netlify.png"
src="https://raw.githubusercontent.com/netlify/next-runtime/main/demos/default/public/next-on-netlify.png"
alt="Picture of the author"
width={540}
height={191}
Expand Down
3 changes: 0 additions & 3 deletions demos/default/netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ NODE_VERSION = "16.15.1"
Strict-Transport-Security = "max-age=31536000"
X-Test = 'foobar'

[dev]
framework = "#static"

[[plugins]]
package = "../plugin-wrapper/"

Expand Down
16 changes: 7 additions & 9 deletions demos/default/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,18 @@ module.exports = {
},
// https://nextjs.org/docs/basic-features/image-optimization#domains
images: {
domains: ['raw.githubusercontent.com'],
domains: ['raw.githubusercontent.com', 'upload.wikimedia.org'],
remotePatterns: [
{
hostname: '*.imgur.com',
}
]
},
// https://nextjs.org/docs/basic-features/built-in-css-support#customizing-sass-options
sassOptions: {
includePaths: [path.join(__dirname, 'styles-sass-test')],
},
experimental: {
optimizeCss: false,
images: {
remotePatterns: [
{
hostname: '*.imgur.com',
},
],
},
},
}
}
2 changes: 1 addition & 1 deletion demos/default/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@
"npm-run-all": "^4.1.5",
"typescript": "^4.6.3"
}
}
}
2 changes: 1 addition & 1 deletion demos/default/pages/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const Images = () => (
<p>
<Image src={logo} alt="netlify logomark" />
<Image
src="https://raw.githubusercontent.com/netlify/next-runtime/main/next-on-netlify.png"
src="https://raw.githubusercontent.com/netlify/next-runtime/main/demos/default/public/next-on-netlify.png"
alt="Picture of the author"
width={500}
height={500}
Expand Down
7 changes: 5 additions & 2 deletions packages/runtime/src/helpers/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { NetlifyConfig } from '@netlify/build'
import destr from 'destr'
import { readJSON, writeJSON } from 'fs-extra'
import type { Header } from 'next/dist/lib/load-custom-routes'
import type { NextConfigComplete } from 'next/dist/server/config-shared'
Expand Down Expand Up @@ -85,8 +86,10 @@ export const configureHandlerFunctions = async ({ netlifyConfig, publish, ignore
const cssFilesToInclude = files.filter((f) => 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) => {
Expand Down
6 changes: 5 additions & 1 deletion packages/runtime/src/helpers/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
)
Expand Down
68 changes: 42 additions & 26 deletions packages/runtime/src/helpers/functions.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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,
Expand All @@ -69,35 +70,50 @@ export const setupImageFunction = async ({
remotePatterns: RemotePattern[]
responseHeaders?: Record<string, string>
}): Promise<void> => {
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
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/src/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,5 @@ export const getRemotePatterns = (experimental: ExperimentalConfigWithLegacy, im
}
return []
}

/* eslint-enable max-lines */
3 changes: 2 additions & 1 deletion test/helpers/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

})
Expand Down
30 changes: 29 additions & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down