Skip to content

Commit 556ec8f

Browse files
committed
spike: restore ipx handling to runtime v5
1 parent c993184 commit 556ec8f

File tree

8 files changed

+2417
-308
lines changed

8 files changed

+2417
-308
lines changed

package-lock.json

+2,210-246
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
"url": "https://github.com/netlify/next-runtime/issues"
4848
},
4949
"homepage": "https://github.com/netlify/next-runtime#readme",
50+
"dependencies": {
51+
"@netlify/ipx": "^1.4.6"
52+
},
5053
"devDependencies": {
5154
"@fastly/http-compute-js": "1.1.4",
5255
"@netlify/blobs": "^7.3.0",
@@ -96,7 +99,6 @@
9699
"indent": 2,
97100
"remove": [
98101
"clean-package",
99-
"dependencies",
100102
"devDependencies",
101103
"scripts"
102104
]

src/build/functions/edge.ts

+7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { pathToRegexp } from 'path-to-regexp'
88

99
import { EDGE_HANDLER_NAME, PluginContext } from '../plugin-context.js'
1010

11+
import { createIpxEdgeAcceptHandler } from './ipx.js'
12+
1113
const writeEdgeManifest = async (ctx: PluginContext, manifest: Manifest) => {
1214
await mkdir(ctx.edgeFunctionsDir, { recursive: true })
1315
await writeFile(join(ctx.edgeFunctionsDir, 'manifest.json'), JSON.stringify(manifest, null, 2))
@@ -176,5 +178,10 @@ export const createEdgeHandlers = async (ctx: PluginContext) => {
176178
version: 1,
177179
functions: netlifyDefinitions,
178180
}
181+
182+
if (ctx.imageService === 'ipx') {
183+
await createIpxEdgeAcceptHandler(ctx, netlifyManifest)
184+
}
185+
179186
await writeEdgeManifest(ctx, netlifyManifest)
180187
}

src/build/functions/ipx.ts

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { cp, mkdir, writeFile } from 'fs/promises'
2+
import { join } from 'path'
3+
4+
import { Manifest } from '@netlify/edge-functions'
5+
6+
import { IPX_HANDLER_NAME, type PluginContext } from '../plugin-context.js'
7+
8+
const sanitizeEdgePath = (imagesPath: string) =>
9+
new URL(imagesPath, process.env.URL || 'http://n').pathname as `/${string}`
10+
11+
const getAdjustedImageConfig = (ctx: PluginContext) => {
12+
return {
13+
...ctx.buildConfig.images,
14+
basePath: [ctx.buildConfig.basePath, IPX_HANDLER_NAME].join('/'),
15+
}
16+
}
17+
18+
export const createIpxHandler = async (ctx: PluginContext) => {
19+
await mkdir(ctx.ipxHandlerRootDir, { recursive: true })
20+
21+
await cp(
22+
join(ctx.pluginDir, 'dist/build/templates/ipx.ts'),
23+
join(ctx.ipxHandlerRootDir, '_ipx.ts'),
24+
)
25+
26+
await writeFile(
27+
join(ctx.ipxHandlerRootDir, 'imageconfig.json'),
28+
JSON.stringify(getAdjustedImageConfig(ctx)),
29+
)
30+
31+
await writeFile(
32+
join(ctx.ipxHandlerRootDir, '_ipx.json'),
33+
JSON.stringify({
34+
version: 1,
35+
config: {
36+
name: 'next/image handler',
37+
generator: `${ctx.pluginName}@${ctx.pluginVersion}`,
38+
timeout: 120,
39+
},
40+
}),
41+
)
42+
43+
ctx.netlifyConfig.redirects.push(
44+
{
45+
from: ctx.buildConfig.images.path,
46+
// eslint-disable-next-line id-length
47+
query: { url: ':url', w: ':width', q: ':quality' },
48+
to: `${ctx.buildConfig.basePath}/${IPX_HANDLER_NAME}/w_:width,q_:quality/:url`,
49+
status: 301,
50+
},
51+
{
52+
from: `${ctx.buildConfig.basePath}/${IPX_HANDLER_NAME}/*`,
53+
to: `/.netlify/builders/${IPX_HANDLER_NAME}`,
54+
status: 200,
55+
},
56+
)
57+
}
58+
59+
export const createIpxEdgeAcceptHandler = async (ctx: PluginContext, netlifyManifest: Manifest) => {
60+
await mkdir(ctx.ipxEdgeHandlerRootDir, { recursive: true })
61+
await cp(
62+
join(ctx.pluginDir, 'dist/build/templates/ipx-edge-accept-handler.ts'),
63+
join(ctx.ipxEdgeHandlerRootDir, 'index.ts'),
64+
)
65+
await writeFile(
66+
join(ctx.ipxEdgeHandlerRootDir, 'imageconfig.json'),
67+
JSON.stringify(getAdjustedImageConfig(ctx)),
68+
)
69+
70+
netlifyManifest.functions.push({
71+
function: IPX_HANDLER_NAME,
72+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
73+
// @ts-ignore
74+
name: 'next/image handler',
75+
path: ctx.buildConfig.images.path
76+
? sanitizeEdgePath(ctx.buildConfig.images.path)
77+
: '/_next/image',
78+
generator: `${ctx.pluginName}@${ctx.pluginVersion}`,
79+
})
80+
81+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
82+
// @ts-ignore
83+
netlifyManifest.layers ??= []
84+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
85+
// @ts-ignore
86+
netlifyManifest.layers.push({
87+
name: `https://ipx-edge-function-layer.netlify.app/mod.ts`,
88+
flag: 'ipx-edge-function-layer-url',
89+
})
90+
}

src/build/image-cdn.ts

+66-61
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { RemotePattern } from 'next/dist/shared/lib/image-config.js'
22
import { makeRe } from 'picomatch'
33

4+
import { createIpxHandler } from './functions/ipx.js'
45
import { PluginContext } from './plugin-context.js'
56

67
function generateRegexFromPattern(pattern: string): string {
@@ -18,75 +19,79 @@ export const setImageConfig = async (ctx: PluginContext): Promise<void> => {
1819
return
1920
}
2021

21-
ctx.netlifyConfig.redirects.push(
22-
{
23-
from: imageEndpointPath,
24-
// w and q are too short to be used as params with id-length rule
25-
// but we are forced to do so because of the next/image loader decides on their names
26-
// eslint-disable-next-line id-length
27-
query: { url: ':url', w: ':width', q: ':quality' },
28-
to: '/.netlify/images?url=:url&w=:width&q=:quality',
29-
status: 200,
30-
},
31-
// when migrating from @netlify/plugin-nextjs@4 image redirect to ipx might be cached in the browser
32-
{
33-
from: '/_ipx/*',
34-
// w and q are too short to be used as params with id-length rule
35-
// but we are forced to do so because of the next/image loader decides on their names
36-
// eslint-disable-next-line id-length
37-
query: { url: ':url', w: ':width', q: ':quality' },
38-
to: '/.netlify/images?url=:url&w=:width&q=:quality',
39-
status: 200,
40-
},
41-
)
22+
if (ctx.imageService === 'ipx') {
23+
await createIpxHandler(ctx)
24+
} else {
25+
ctx.netlifyConfig.redirects.push(
26+
{
27+
from: imageEndpointPath,
28+
// w and q are too short to be used as params with id-length rule
29+
// but we are forced to do so because of the next/image loader decides on their names
30+
// eslint-disable-next-line id-length
31+
query: { url: ':url', w: ':width', q: ':quality' },
32+
to: '/.netlify/images?url=:url&w=:width&q=:quality',
33+
status: 200,
34+
},
35+
// when migrating from @netlify/plugin-nextjs@4 image redirect to ipx might be cached in the browser
36+
{
37+
from: '/_ipx/*',
38+
// w and q are too short to be used as params with id-length rule
39+
// but we are forced to do so because of the next/image loader decides on their names
40+
// eslint-disable-next-line id-length
41+
query: { url: ':url', w: ':width', q: ':quality' },
42+
to: '/.netlify/images?url=:url&w=:width&q=:quality',
43+
status: 200,
44+
},
45+
)
4246

43-
if (remotePatterns?.length !== 0 || domains?.length !== 0) {
44-
ctx.netlifyConfig.images ||= { remote_images: [] }
45-
ctx.netlifyConfig.images.remote_images ||= []
47+
if (remotePatterns?.length !== 0 || domains?.length !== 0) {
48+
ctx.netlifyConfig.images ||= { remote_images: [] }
49+
ctx.netlifyConfig.images.remote_images ||= []
4650

47-
if (remotePatterns && remotePatterns.length !== 0) {
48-
for (const remotePattern of remotePatterns) {
49-
let { protocol, hostname, port, pathname }: RemotePattern = remotePattern
51+
if (remotePatterns && remotePatterns.length !== 0) {
52+
for (const remotePattern of remotePatterns) {
53+
let { protocol, hostname, port, pathname }: RemotePattern = remotePattern
5054

51-
if (pathname) {
52-
pathname = pathname.startsWith('/') ? pathname : `/${pathname}`
53-
}
55+
if (pathname) {
56+
pathname = pathname.startsWith('/') ? pathname : `/${pathname}`
57+
}
5458

55-
const combinedRemotePattern = `${protocol ?? 'http?(s)'}://${hostname}${
56-
port ? `:${port}` : ''
57-
}${pathname ?? '/**'}`
59+
const combinedRemotePattern = `${protocol ?? 'http?(s)'}://${hostname}${
60+
port ? `:${port}` : ''
61+
}${pathname ?? '/**'}`
5862

59-
try {
60-
ctx.netlifyConfig.images.remote_images.push(
61-
generateRegexFromPattern(combinedRemotePattern),
62-
)
63-
} catch (error) {
64-
ctx.failBuild(
65-
`Failed to generate Image CDN remote image regex from Next.js remote pattern: ${JSON.stringify(
66-
{ remotePattern, combinedRemotePattern },
67-
null,
68-
2,
69-
)}`,
70-
error,
71-
)
63+
try {
64+
ctx.netlifyConfig.images.remote_images.push(
65+
generateRegexFromPattern(combinedRemotePattern),
66+
)
67+
} catch (error) {
68+
ctx.failBuild(
69+
`Failed to generate Image CDN remote image regex from Next.js remote pattern: ${JSON.stringify(
70+
{ remotePattern, combinedRemotePattern },
71+
null,
72+
2,
73+
)}`,
74+
error,
75+
)
76+
}
7277
}
7378
}
74-
}
7579

76-
if (domains && domains.length !== 0) {
77-
for (const domain of domains) {
78-
const patternFromDomain = `http?(s)://${domain}/**`
79-
try {
80-
ctx.netlifyConfig.images.remote_images.push(generateRegexFromPattern(patternFromDomain))
81-
} catch (error) {
82-
ctx.failBuild(
83-
`Failed to generate Image CDN remote image regex from Next.js domain: ${JSON.stringify(
84-
{ domain, patternFromDomain },
85-
null,
86-
2,
87-
)}`,
88-
error,
89-
)
80+
if (domains && domains.length !== 0) {
81+
for (const domain of domains) {
82+
const patternFromDomain = `http?(s)://${domain}/**`
83+
try {
84+
ctx.netlifyConfig.images.remote_images.push(generateRegexFromPattern(patternFromDomain))
85+
} catch (error) {
86+
ctx.failBuild(
87+
`Failed to generate Image CDN remote image regex from Next.js domain: ${JSON.stringify(
88+
{ domain, patternFromDomain },
89+
null,
90+
2,
91+
)}`,
92+
error,
93+
)
94+
}
9095
}
9196
}
9297
}

src/build/plugin-context.ts

+21
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const PLUGIN_DIR = join(MODULE_DIR, '../..')
2020
const DEFAULT_PUBLISH_DIR = '.next'
2121

2222
export const SERVER_HANDLER_NAME = '___netlify-server-handler'
23+
export const IPX_HANDLER_NAME = '_ipx'
2324
export const EDGE_HANDLER_NAME = '___netlify-edge-handler'
2425

2526
// copied from https://github.com/vercel/next.js/blob/af5b4db98ac1acccc3f167cc6aba2f0c9e7094df/packages/next/src/build/index.ts#L388-L395
@@ -162,6 +163,16 @@ export class PluginContext {
162163
return satisfies(this.buildVersion, REQUIRED_BUILD_VERSION, { includePrerelease: true })
163164
}
164165

166+
get imageService(): 'image-cdn' | 'ipx' {
167+
return 'ipx'
168+
// uncomment if/when feature flag is set up
169+
// if ((this.featureFlags || {})['next-runtime-use-ipx']) {
170+
// return 'ipx'
171+
// }
172+
173+
// return 'image-cdn'
174+
}
175+
165176
/**
166177
* Absolute path of the directory containing the files for the serverless lambda function
167178
* `.netlify/functions-internal`
@@ -175,6 +186,11 @@ export class PluginContext {
175186
return join(this.serverFunctionsDir, SERVER_HANDLER_NAME)
176187
}
177188

189+
/** Absolute path of the ipx handler */
190+
get ipxHandlerRootDir(): string {
191+
return join(this.serverFunctionsDir, IPX_HANDLER_NAME)
192+
}
193+
178194
get serverHandlerDir(): string {
179195
if (this.relativeAppDir.length === 0) {
180196
return this.serverHandlerRootDir
@@ -206,6 +222,11 @@ export class PluginContext {
206222
return join(this.edgeFunctionsDir, EDGE_HANDLER_NAME)
207223
}
208224

225+
/** Absolute path of the ipx edge handler */
226+
get ipxEdgeHandlerRootDir(): string {
227+
return join(this.edgeFunctionsDir, IPX_HANDLER_NAME)
228+
}
229+
209230
constructor(options: NetlifyPluginOptions) {
210231
this.constants = options.constants
211232
this.featureFlags = options.featureFlags
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
2+
// @ts-ignore
3+
import { getHandler } from 'https://ipx-edge-function-layer.netlify.app/mod.ts'
4+
5+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
6+
// @ts-ignore Injected at build time
7+
import imageconfig from './imageconfig.json' assert { type: 'json' }
8+
9+
export default getHandler({ formats: imageconfig?.formats })

src/build/templates/ipx.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { createIPXHandler } from '@netlify/ipx'
2+
3+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
4+
// @ts-ignore Injected at build time
5+
import { basePath, domains, remotePatterns } from './imageconfig.json'
6+
7+
export const handler = createIPXHandler({
8+
basePath,
9+
domains,
10+
remotePatterns,
11+
})

0 commit comments

Comments
 (0)