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 14 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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ by targeting the `/_next/image/*` route:
X-Test = 'foobar'
```

## Disabling `ipx`

If you wish to disable the use of the `ipx` package, set the `DISABLE_IPX` environment variable to `true`. Please note that some requests to `/_next/image/*` may fail unless an image loader, such as Cloudinary or Imgix, is specified as a replacement for `ipx`.

See the [Next.js documentation](https://nextjs.org/docs/api-reference/next/image#built-in-loaders) for 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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions packages/runtime/src/helpers/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import slash from 'slash'
import { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME } from '../constants'

import type { RoutesManifest } from './types'
import { isEnvSet } from './utils'

const ROUTES_MANIFEST_FILE = 'routes-manifest.json'

Expand Down Expand Up @@ -85,11 +86,13 @@ 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 (!isEnvSet('DISABLE_IPX')) {
netlifyConfig.functions._ipx ||= {}
netlifyConfig.functions._ipx.node_bundler = 'nft'
}

/* eslint-enable no-underscore-dangle */
;[HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME].forEach((functionName) => {
[HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME].forEach((functionName) => {
netlifyConfig.functions[functionName] ||= { included_files: [], external_node_modules: [] }
netlifyConfig.functions[functionName].node_bundler = 'nft'
netlifyConfig.functions[functionName].included_files ||= []
Expand Down
3 changes: 2 additions & 1 deletion packages/runtime/src/helpers/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { MiddlewareManifest } from 'next/dist/build/webpack/plugins/middlew
import type { RouteHas } from 'next/dist/lib/load-custom-routes'

import { getRequiredServerFiles } from './config'
import { isEnvSet } from './utils'

// This is the format as of [email protected]
interface EdgeFunctionDefinitionV1 {
Expand Down Expand Up @@ -205,7 +206,7 @@ export const writeEdgeFunctions = async (netlifyConfig: NetlifyConfig) => {
const nextConfig = nextConfigFile.config
await writeJSON(join(edgeFunctionRoot, 'edge-shared', 'nextConfig.json'), nextConfig)

if (!process.env.NEXT_DISABLE_EDGE_IMAGES) {
if (!process.env.NEXT_DISABLE_EDGE_IMAGES || !isEnvSet('DISABLE_IPX')) {
console.log(
'Using Netlify Edge Functions for image format detection. Set env var "NEXT_DISABLE_EDGE_IMAGES=true" to disable.',
)
Expand Down
69 changes: 43 additions & 26 deletions packages/runtime/src/helpers/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, IMAGE_FUNCTION_NAME, DEFAULT_
import { getHandler } from '../templates/getHandler'
import { getPageResolver } from '../templates/getPageResolver'

import { isEnvSet } from './utils'

export const generateFunctions = async (
{ FUNCTIONS_SRC = DEFAULT_FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC, PUBLISH_DIR }: NetlifyPluginConstants,
appDir: string,
Expand Down Expand Up @@ -55,7 +57,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 +71,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 (isEnvSet('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 && imageconfig.loader === 'default') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[sand] does this need the check for imageconfig.loader?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I had written that out of habit when a property is optional, but this could be achieved with the imageconfig.loader === 'default' check

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
3 changes: 3 additions & 0 deletions packages/runtime/src/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,7 @@ export const getRemotePatterns = (experimental: ExperimentalConfigWithLegacy, im
}
return []
}

export const isEnvSet = (envVar: string) => process.env[envVar] === 'true' || process.env[envVar] === '1'

/* 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,12 +565,40 @@ 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
})
})

describe('onPostBuild', () => {
Expand Down