-
Notifications
You must be signed in to change notification settings - Fork 86
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: handle /_next/image through Netlify Image CDN for local images (#…
…149) * image rewrite * check for default loader * add back image redirect * add nextconfig and netlifyconfig * tmp: revert back to using redirect for lambda until netlify/proxy#1402 is deployed to workaround redirect clash * refactor: move image-cdn handling to it's own module * chore: eslint ignore for short param names that we can't change * test: add netlifyConfig.redirects array to mocked plugin options * test: add unit test cases for image-cdn * test: add e2e test for image-cdn * Revert "tmp: revert back to using redirect for lambda until netlify/proxy#1402 is deployed to workaround redirect clash" This reverts commit bf3269db27cc0c9e0224bb2777e0f113d31d0741. * test: assert content-type instead of internal header that might change in e2e test * fix: use import type when importing types for safety --------- Co-authored-by: Tatyana <[email protected]>
- Loading branch information
Showing
8 changed files
with
185 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
/* eslint-disable id-length */ | ||
import type { NetlifyPluginOptions } from '@netlify/build' | ||
import type { NextConfigComplete } from 'next/dist/server/config-shared.js' | ||
import { beforeEach, describe, expect, test, vi, TestContext } from 'vitest' | ||
|
||
import { setImageConfig } from './image-cdn.js' | ||
import { PluginContext } from './plugin-context.js' | ||
|
||
type DeepPartial<T> = T extends object | ||
? { | ||
[P in keyof T]?: DeepPartial<T[P]> | ||
} | ||
: T | ||
|
||
type ImageCDNTestContext = TestContext & { | ||
pluginContext: PluginContext | ||
mockNextConfig?: DeepPartial<NextConfigComplete> | ||
} | ||
|
||
describe('Image CDN', () => { | ||
beforeEach<ImageCDNTestContext>((ctx) => { | ||
ctx.mockNextConfig = undefined | ||
ctx.pluginContext = new PluginContext({ | ||
netlifyConfig: { | ||
redirects: [], | ||
}, | ||
} as unknown as NetlifyPluginOptions) | ||
vi.spyOn(ctx.pluginContext, 'getBuildConfig').mockImplementation(() => | ||
Promise.resolve((ctx.mockNextConfig ?? {}) as NextConfigComplete), | ||
) | ||
}) | ||
|
||
test<ImageCDNTestContext>('adds redirect to Netlify Image CDN when default image loader is used', async (ctx) => { | ||
ctx.mockNextConfig = { | ||
images: { | ||
path: '/_next/image', | ||
loader: 'default', | ||
}, | ||
} | ||
|
||
await setImageConfig(ctx.pluginContext) | ||
|
||
expect(ctx.pluginContext.netlifyConfig.redirects).toEqual( | ||
expect.arrayContaining([ | ||
{ | ||
from: '/_next/image', | ||
query: { | ||
q: ':quality', | ||
url: ':url', | ||
w: ':width', | ||
}, | ||
to: '/.netlify/images?url=:url&w=:width&q=:quality', | ||
status: 200, | ||
}, | ||
]), | ||
) | ||
}) | ||
|
||
test<ImageCDNTestContext>('does not add redirect to Netlify Image CDN when non-default loader is used', async (ctx) => { | ||
ctx.mockNextConfig = { | ||
images: { | ||
path: '/_next/image', | ||
loader: 'custom', | ||
loaderFile: './custom-loader.js', | ||
}, | ||
} | ||
|
||
await setImageConfig(ctx.pluginContext) | ||
|
||
expect(ctx.pluginContext.netlifyConfig.redirects).not.toEqual( | ||
expect.arrayContaining([ | ||
{ | ||
from: '/_next/image', | ||
query: { | ||
q: ':quality', | ||
url: ':url', | ||
w: ':width', | ||
}, | ||
to: '/.netlify/images?url=:url&w=:width&q=:quality', | ||
status: 200, | ||
}, | ||
]), | ||
) | ||
}) | ||
|
||
test<ImageCDNTestContext>('handles custom images.path', async (ctx) => { | ||
ctx.mockNextConfig = { | ||
images: { | ||
// Next.js automatically adds basePath to images.path (when user does not set custom `images.path` in their config) | ||
// if user sets custom `images.path` - it will be used as-is (so user need to cover their basePath by themselves | ||
// if they want to have it in their custom image endpoint | ||
// see https://github.com/vercel/next.js/blob/bb105ef4fbfed9d96a93794eeaed956eda2116d8/packages/next/src/server/config.ts#L426-L432) | ||
// either way `images.path` we get is final config with everything combined so we want to use it as-is | ||
path: '/base/path/_custom/image/endpoint', | ||
loader: 'default', | ||
}, | ||
} | ||
|
||
await setImageConfig(ctx.pluginContext) | ||
|
||
expect(ctx.pluginContext.netlifyConfig.redirects).toEqual( | ||
expect.arrayContaining([ | ||
{ | ||
from: '/base/path/_custom/image/endpoint', | ||
query: { | ||
q: ':quality', | ||
url: ':url', | ||
w: ':width', | ||
}, | ||
to: '/.netlify/images?url=:url&w=:width&q=:quality', | ||
status: 200, | ||
}, | ||
]), | ||
) | ||
}) | ||
}) | ||
/* eslint-enable id-length */ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { PluginContext } from './plugin-context.js' | ||
|
||
/** | ||
* Rewrite next/image to netlify image cdn | ||
*/ | ||
export const setImageConfig = async (ctx: PluginContext): Promise<void> => { | ||
const { | ||
images: { path: imageEndpointPath, loader: imageLoader }, | ||
} = await ctx.getBuildConfig() | ||
|
||
if (imageLoader === 'default') { | ||
ctx.netlifyConfig.redirects.push({ | ||
from: imageEndpointPath, | ||
// w and q are too short to be used as params with id-length rule | ||
// but we are forced to do so because of the next/image loader decides on their names | ||
// eslint-disable-next-line id-length | ||
query: { url: ':url', w: ':width', q: ':quality' }, | ||
to: '/.netlify/images?url=:url&w=:width&q=:quality', | ||
status: 200, | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import Image from 'next/image' | ||
|
||
export default function NextImageUsingNetlifyImageCDN() { | ||
return ( | ||
<main> | ||
<h1>Next/Image + Netlify Image CDN</h1> | ||
<Image src="/squirrel.jpg" alt="a cute squirrel (next/image)" width={300} height={278} /> | ||
</main> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters