Skip to content

Commit af2d3ce

Browse files
Fix image optimization support for Next 14.1.1 (#377)
* Move image optimization to plugin * Refactor image optimization code * Added image optimization plugin for 14.1.1 * Fix image optimization plugin * Add changeset * Revert default sharp version to 0.32.6 * e2e test for image optimization * change one of the test to use an external image --------- Co-authored-by: Dorseuil Nicolas <nicodorseuil@yahoo.fr>
1 parent 3deb202 commit af2d3ce

File tree

14 files changed

+204
-23
lines changed

14 files changed

+204
-23
lines changed

.changeset/old-islands-invite.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"open-next": patch
3+
---
4+
5+
Fix Image Optimization Support for Next@14.1.1
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Image from "next/image";
2+
3+
export default function ImageOptimization() {
4+
return (
5+
<div>
6+
<Image
7+
src="/static/corporate_holiday_card.jpg"
8+
alt="Corporate Holiday Card"
9+
width={300}
10+
height={300}
11+
/>
12+
</div>
13+
);
14+
}

examples/app-pages-router/app/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export default function Home() {
3333
<Nav href={"/parallel"} title="Parallel">
3434
Parallel routing
3535
</Nav>
36+
<Nav href={"/image-optimization"} title="Image Optimization">
37+
Image Optimization with next/image
38+
</Nav>
3639
</main>
3740
<h1>Pages Router</h1>
3841
<main className="grid grid-cols-2 gap-4 p-10 [&>a]:border">
Loading
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Image from "next/image";
2+
3+
export default function ImageOptimization() {
4+
return (
5+
<div>
6+
<Image
7+
src="https://open-next.js.org/architecture.png"
8+
alt="Open Next architecture"
9+
width={300}
10+
height={300}
11+
/>
12+
</div>
13+
);
14+
}

examples/app-router/app/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ export default function Home() {
4444
<Nav href={"/sse"} title="Server Sent Events">
4545
Server Sent Events via Streaming
4646
</Nav>
47+
<Nav href={"/image-optimization"} title="Image Optimization">
48+
Image Optimization with next/image
49+
</Nav>
4750
</main>
4851
</>
4952
);

examples/app-router/next.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ const nextConfig = {
88
experimental: {
99
serverActions: true,
1010
},
11+
images: {
12+
remotePatterns: [
13+
{
14+
protocol: "https",
15+
hostname: "open-next.js.org",
16+
},
17+
],
18+
},
1119
redirects: () => {
1220
return [
1321
{
Loading

packages/open-next/src/adapters/image-optimization-adapter.ts

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import type {
1414
// @ts-ignore
1515
import { defaultConfig } from "next/dist/server/config-shared";
1616
import {
17-
imageOptimizer,
1817
ImageOptimizerCache,
1918
// @ts-ignore
2019
} from "next/dist/server/image-optimizer";
@@ -23,6 +22,7 @@ import type { NextUrlWithParsedQuery } from "next/dist/server/request-meta";
2322

2423
import { loadConfig } from "./config/util.js";
2524
import { awsLogger, debug, error } from "./logger.js";
25+
import { optimizeImage } from "./plugins/image-optimization.js";
2626
import { setNodeEnv } from "./util.js";
2727

2828
// Expected environment variables
@@ -64,7 +64,12 @@ export async function handler(
6464
headers,
6565
queryString === null ? undefined : queryString,
6666
);
67-
const result = await optimizeImage(headers, imageParams);
67+
const result = await optimizeImage(
68+
headers,
69+
imageParams,
70+
nextConfig,
71+
downloadHandler,
72+
);
6873

6974
return buildSuccessResponse(result);
7075
} catch (e: any) {
@@ -110,23 +115,6 @@ function validateImageParams(
110115
return imageParams;
111116
}
112117

113-
async function optimizeImage(
114-
headers: APIGatewayProxyEventHeaders,
115-
imageParams: any,
116-
) {
117-
const result = await imageOptimizer(
118-
// @ts-ignore
119-
{ headers },
120-
{}, // res object is not necessary as it's not actually used.
121-
imageParams,
122-
nextConfig,
123-
false, // not in dev mode
124-
downloadHandler,
125-
);
126-
debug("optimized result", result);
127-
return result;
128-
}
129-
130118
function buildSuccessResponse(result: any) {
131119
return {
132120
statusCode: 200,
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { IncomingMessage, ServerResponse } from "node:http";
2+
3+
import type { APIGatewayProxyEventHeaders } from "aws-lambda";
4+
import type { NextConfig } from "next/dist/server/config-shared";
5+
//#override imports
6+
import {
7+
// @ts-ignore
8+
fetchExternalImage,
9+
// @ts-ignore
10+
fetchInternalImage,
11+
imageOptimizer,
12+
} from "next/dist/server/image-optimizer";
13+
//#endOverride
14+
import type { NextUrlWithParsedQuery } from "next/dist/server/request-meta";
15+
16+
import { debug } from "../logger.js";
17+
18+
//#override optimizeImage
19+
export async function optimizeImage(
20+
headers: APIGatewayProxyEventHeaders,
21+
imageParams: any,
22+
nextConfig: NextConfig,
23+
handleRequest: (
24+
newReq: IncomingMessage,
25+
newRes: ServerResponse,
26+
newParsedUrl?: NextUrlWithParsedQuery,
27+
) => Promise<void>,
28+
) {
29+
const { isAbsolute, href } = imageParams;
30+
31+
const imageUpstream = isAbsolute
32+
? await fetchExternalImage(href)
33+
: await fetchInternalImage(
34+
href,
35+
// @ts-ignore
36+
{ headers },
37+
{}, // res object is not necessary as it's not actually used.
38+
handleRequest,
39+
);
40+
41+
// @ts-ignore
42+
const result = await imageOptimizer(
43+
imageUpstream,
44+
imageParams,
45+
nextConfig,
46+
false, // not in dev mode
47+
);
48+
debug("optimized result", result);
49+
return result;
50+
}
51+
//#endOverride
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { IncomingMessage, ServerResponse } from "node:http";
2+
3+
import { APIGatewayProxyEventHeaders } from "aws-lambda";
4+
import { NextConfig } from "next/dist/server/config-shared";
5+
//#override imports
6+
import { imageOptimizer } from "next/dist/server/image-optimizer";
7+
//#endOverride
8+
import { NextUrlWithParsedQuery } from "next/dist/server/request-meta";
9+
10+
import { debug } from "../logger.js";
11+
12+
//#override optimizeImage
13+
export async function optimizeImage(
14+
headers: APIGatewayProxyEventHeaders,
15+
imageParams: any,
16+
nextConfig: NextConfig,
17+
handleRequest: (
18+
newReq: IncomingMessage,
19+
newRes: ServerResponse,
20+
newParsedUrl: NextUrlWithParsedQuery,
21+
) => Promise<void>,
22+
) {
23+
const result = await imageOptimizer(
24+
// @ts-ignore
25+
{ headers },
26+
{}, // res object is not necessary as it's not actually used.
27+
imageParams,
28+
nextConfig,
29+
false, // not in dev mode
30+
handleRequest,
31+
);
32+
debug("optimized result", result);
33+
return result;
34+
}
35+
//#endOverride

packages/open-next/src/build.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export async function build(opts: BuildOptions = {}) {
112112
}
113113
await createServerBundle(monorepoRoot, options.streaming);
114114
createRevalidationBundle();
115-
createImageOptimizationBundle();
115+
await createImageOptimizationBundle();
116116
createWarmerBundle();
117117
if (options.minify) {
118118
await minifyServerBundle();
@@ -315,7 +315,7 @@ function createRevalidationBundle() {
315315
);
316316
}
317317

318-
function createImageOptimizationBundle() {
318+
async function createImageOptimizationBundle() {
319319
logger.info(`Bundling image optimization function...`);
320320

321321
const { appPath, appBuildOutputPath, outputDir } = options;
@@ -324,16 +324,36 @@ function createImageOptimizationBundle() {
324324
const outputPath = path.join(outputDir, "image-optimization-function");
325325
fs.mkdirSync(outputPath, { recursive: true });
326326

327+
const plugins =
328+
compareSemver(options.nextVersion, "14.1.1") >= 0
329+
? [
330+
openNextPlugin({
331+
name: "opennext-14.1.1-image-optimization",
332+
target: /plugins\/image-optimization\.js/g,
333+
replacements: ["./image-optimization.replacement.js"],
334+
}),
335+
]
336+
: undefined;
337+
338+
if (plugins && plugins.length > 0) {
339+
logger.debug(
340+
`Applying plugins:: [${plugins
341+
.map(({ name }) => name)
342+
.join(",")}] for Next version: ${options.nextVersion}`,
343+
);
344+
}
345+
327346
// Build Lambda code (1st pass)
328347
// note: bundle in OpenNext package b/c the adapter relies on the
329348
// "@aws-sdk/client-s3" package which is not a dependency in user's
330349
// Next.js app.
331-
esbuildSync({
350+
await esbuildAsync({
332351
entryPoints: [
333352
path.join(__dirname, "adapters", "image-optimization-adapter.js"),
334353
],
335354
external: ["sharp", "next"],
336355
outfile: path.join(outputPath, "index.mjs"),
356+
plugins,
337357
});
338358

339359
// Build Lambda code (2nd pass)
@@ -367,7 +387,7 @@ function createImageOptimizationBundle() {
367387
// For SHARP_IGNORE_GLOBAL_LIBVIPS see: https://github.com/lovell/sharp/blob/main/docs/install.md#aws-lambda
368388

369389
const nodeOutputPath = path.resolve(outputPath);
370-
const sharpVersion = process.env.SHARP_VERSION ?? "0.33.2";
390+
const sharpVersion = process.env.SHARP_VERSION ?? "0.32.6";
371391

372392
//check if we are running in Windows environment then set env variables accordingly.
373393
try {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
test("Image Optimization", async ({ page }) => {
4+
await page.goto("/");
5+
6+
const imageResponsePromise = page.waitForResponse(
7+
/corporate_holiday_card.jpg/,
8+
);
9+
await page.locator('[href="/image-optimization"]').click();
10+
const imageResponse = await imageResponsePromise;
11+
12+
await page.waitForURL("/image-optimization");
13+
14+
const imageContentType = imageResponse.headers()["content-type"];
15+
expect(imageContentType).toBe("image/webp");
16+
17+
let el = page.locator("img");
18+
await expect(el).toHaveJSProperty("complete", true);
19+
await expect(el).not.toHaveJSProperty("naturalWidth", 0);
20+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { expect, test } from "@playwright/test";
2+
3+
test("Image Optimization", async ({ page }) => {
4+
await page.goto("/");
5+
6+
const imageResponsePromise = page.waitForResponse(
7+
/https%3A%2F%2Fopen-next.js.org%2Farchitecture.png/,
8+
);
9+
await page.locator('[href="/image-optimization"]').click();
10+
const imageResponse = await imageResponsePromise;
11+
12+
await page.waitForURL("/image-optimization");
13+
14+
const imageContentType = imageResponse.headers()["content-type"];
15+
expect(imageContentType).toBe("image/webp");
16+
17+
let el = page.locator("img");
18+
await expect(el).toHaveJSProperty("complete", true);
19+
await expect(el).not.toHaveJSProperty("naturalWidth", 0);
20+
});

0 commit comments

Comments
 (0)