diff --git a/.changeset/old-islands-invite.md b/.changeset/old-islands-invite.md
new file mode 100644
index 00000000..0a4b5586
--- /dev/null
+++ b/.changeset/old-islands-invite.md
@@ -0,0 +1,5 @@
+---
+"open-next": patch
+---
+
+Fix Image Optimization Support for Next@14.1.1
diff --git a/examples/app-pages-router/app/image-optimization/page.tsx b/examples/app-pages-router/app/image-optimization/page.tsx
new file mode 100644
index 00000000..f43655af
--- /dev/null
+++ b/examples/app-pages-router/app/image-optimization/page.tsx
@@ -0,0 +1,14 @@
+import Image from "next/image";
+
+export default function ImageOptimization() {
+ return (
+
+
+
+ );
+}
diff --git a/examples/app-pages-router/app/page.tsx b/examples/app-pages-router/app/page.tsx
index 0fd02970..1b32a5c3 100644
--- a/examples/app-pages-router/app/page.tsx
+++ b/examples/app-pages-router/app/page.tsx
@@ -33,6 +33,9 @@ export default function Home() {
+
Pages Router
diff --git a/examples/app-pages-router/public/static/corporate_holiday_card.jpg b/examples/app-pages-router/public/static/corporate_holiday_card.jpg
new file mode 100644
index 00000000..0df96ae2
Binary files /dev/null and b/examples/app-pages-router/public/static/corporate_holiday_card.jpg differ
diff --git a/examples/app-router/app/image-optimization/page.tsx b/examples/app-router/app/image-optimization/page.tsx
new file mode 100644
index 00000000..baba473b
--- /dev/null
+++ b/examples/app-router/app/image-optimization/page.tsx
@@ -0,0 +1,14 @@
+import Image from "next/image";
+
+export default function ImageOptimization() {
+ return (
+
+
+
+ );
+}
diff --git a/examples/app-router/app/page.tsx b/examples/app-router/app/page.tsx
index 66da3ac8..f2084c5d 100644
--- a/examples/app-router/app/page.tsx
+++ b/examples/app-router/app/page.tsx
@@ -44,6 +44,9 @@ export default function Home() {
+
>
);
diff --git a/examples/app-router/next.config.js b/examples/app-router/next.config.js
index 44797914..ce0565a8 100644
--- a/examples/app-router/next.config.js
+++ b/examples/app-router/next.config.js
@@ -8,6 +8,14 @@ const nextConfig = {
experimental: {
serverActions: true,
},
+ images: {
+ remotePatterns: [
+ {
+ protocol: "https",
+ hostname: "open-next.js.org",
+ },
+ ],
+ },
redirects: () => {
return [
{
diff --git a/examples/app-router/public/static/corporate_holiday_card.jpg b/examples/app-router/public/static/corporate_holiday_card.jpg
new file mode 100644
index 00000000..0df96ae2
Binary files /dev/null and b/examples/app-router/public/static/corporate_holiday_card.jpg differ
diff --git a/packages/open-next/src/adapters/image-optimization-adapter.ts b/packages/open-next/src/adapters/image-optimization-adapter.ts
index f5fbb26b..dc7b920e 100644
--- a/packages/open-next/src/adapters/image-optimization-adapter.ts
+++ b/packages/open-next/src/adapters/image-optimization-adapter.ts
@@ -14,7 +14,6 @@ import type {
// @ts-ignore
import { defaultConfig } from "next/dist/server/config-shared";
import {
- imageOptimizer,
ImageOptimizerCache,
// @ts-ignore
} from "next/dist/server/image-optimizer";
@@ -23,6 +22,7 @@ import type { NextUrlWithParsedQuery } from "next/dist/server/request-meta";
import { loadConfig } from "./config/util.js";
import { awsLogger, debug, error } from "./logger.js";
+import { optimizeImage } from "./plugins/image-optimization.js";
import { setNodeEnv } from "./util.js";
// Expected environment variables
@@ -64,7 +64,12 @@ export async function handler(
headers,
queryString === null ? undefined : queryString,
);
- const result = await optimizeImage(headers, imageParams);
+ const result = await optimizeImage(
+ headers,
+ imageParams,
+ nextConfig,
+ downloadHandler,
+ );
return buildSuccessResponse(result);
} catch (e: any) {
@@ -110,23 +115,6 @@ function validateImageParams(
return imageParams;
}
-async function optimizeImage(
- headers: APIGatewayProxyEventHeaders,
- imageParams: any,
-) {
- const result = await imageOptimizer(
- // @ts-ignore
- { headers },
- {}, // res object is not necessary as it's not actually used.
- imageParams,
- nextConfig,
- false, // not in dev mode
- downloadHandler,
- );
- debug("optimized result", result);
- return result;
-}
-
function buildSuccessResponse(result: any) {
return {
statusCode: 200,
diff --git a/packages/open-next/src/adapters/plugins/image-optimization.replacement.ts b/packages/open-next/src/adapters/plugins/image-optimization.replacement.ts
new file mode 100644
index 00000000..c041f370
--- /dev/null
+++ b/packages/open-next/src/adapters/plugins/image-optimization.replacement.ts
@@ -0,0 +1,51 @@
+import type { IncomingMessage, ServerResponse } from "node:http";
+
+import type { APIGatewayProxyEventHeaders } from "aws-lambda";
+import type { NextConfig } from "next/dist/server/config-shared";
+//#override imports
+import {
+ // @ts-ignore
+ fetchExternalImage,
+ // @ts-ignore
+ fetchInternalImage,
+ imageOptimizer,
+} from "next/dist/server/image-optimizer";
+//#endOverride
+import type { NextUrlWithParsedQuery } from "next/dist/server/request-meta";
+
+import { debug } from "../logger.js";
+
+//#override optimizeImage
+export async function optimizeImage(
+ headers: APIGatewayProxyEventHeaders,
+ imageParams: any,
+ nextConfig: NextConfig,
+ handleRequest: (
+ newReq: IncomingMessage,
+ newRes: ServerResponse,
+ newParsedUrl?: NextUrlWithParsedQuery,
+ ) => Promise,
+) {
+ const { isAbsolute, href } = imageParams;
+
+ const imageUpstream = isAbsolute
+ ? await fetchExternalImage(href)
+ : await fetchInternalImage(
+ href,
+ // @ts-ignore
+ { headers },
+ {}, // res object is not necessary as it's not actually used.
+ handleRequest,
+ );
+
+ // @ts-ignore
+ const result = await imageOptimizer(
+ imageUpstream,
+ imageParams,
+ nextConfig,
+ false, // not in dev mode
+ );
+ debug("optimized result", result);
+ return result;
+}
+//#endOverride
diff --git a/packages/open-next/src/adapters/plugins/image-optimization.ts b/packages/open-next/src/adapters/plugins/image-optimization.ts
new file mode 100644
index 00000000..8cbedbd0
--- /dev/null
+++ b/packages/open-next/src/adapters/plugins/image-optimization.ts
@@ -0,0 +1,35 @@
+import { IncomingMessage, ServerResponse } from "node:http";
+
+import { APIGatewayProxyEventHeaders } from "aws-lambda";
+import { NextConfig } from "next/dist/server/config-shared";
+//#override imports
+import { imageOptimizer } from "next/dist/server/image-optimizer";
+//#endOverride
+import { NextUrlWithParsedQuery } from "next/dist/server/request-meta";
+
+import { debug } from "../logger.js";
+
+//#override optimizeImage
+export async function optimizeImage(
+ headers: APIGatewayProxyEventHeaders,
+ imageParams: any,
+ nextConfig: NextConfig,
+ handleRequest: (
+ newReq: IncomingMessage,
+ newRes: ServerResponse,
+ newParsedUrl: NextUrlWithParsedQuery,
+ ) => Promise,
+) {
+ const result = await imageOptimizer(
+ // @ts-ignore
+ { headers },
+ {}, // res object is not necessary as it's not actually used.
+ imageParams,
+ nextConfig,
+ false, // not in dev mode
+ handleRequest,
+ );
+ debug("optimized result", result);
+ return result;
+}
+//#endOverride
diff --git a/packages/open-next/src/build.ts b/packages/open-next/src/build.ts
index 63da181e..4453d5ed 100755
--- a/packages/open-next/src/build.ts
+++ b/packages/open-next/src/build.ts
@@ -112,7 +112,7 @@ export async function build(opts: BuildOptions = {}) {
}
await createServerBundle(monorepoRoot, options.streaming);
createRevalidationBundle();
- createImageOptimizationBundle();
+ await createImageOptimizationBundle();
createWarmerBundle();
if (options.minify) {
await minifyServerBundle();
@@ -315,7 +315,7 @@ function createRevalidationBundle() {
);
}
-function createImageOptimizationBundle() {
+async function createImageOptimizationBundle() {
logger.info(`Bundling image optimization function...`);
const { appPath, appBuildOutputPath, outputDir } = options;
@@ -324,16 +324,36 @@ function createImageOptimizationBundle() {
const outputPath = path.join(outputDir, "image-optimization-function");
fs.mkdirSync(outputPath, { recursive: true });
+ const plugins =
+ compareSemver(options.nextVersion, "14.1.1") >= 0
+ ? [
+ openNextPlugin({
+ name: "opennext-14.1.1-image-optimization",
+ target: /plugins\/image-optimization\.js/g,
+ replacements: ["./image-optimization.replacement.js"],
+ }),
+ ]
+ : undefined;
+
+ if (plugins && plugins.length > 0) {
+ logger.debug(
+ `Applying plugins:: [${plugins
+ .map(({ name }) => name)
+ .join(",")}] for Next version: ${options.nextVersion}`,
+ );
+ }
+
// Build Lambda code (1st pass)
// note: bundle in OpenNext package b/c the adapter relies on the
// "@aws-sdk/client-s3" package which is not a dependency in user's
// Next.js app.
- esbuildSync({
+ await esbuildAsync({
entryPoints: [
path.join(__dirname, "adapters", "image-optimization-adapter.js"),
],
external: ["sharp", "next"],
outfile: path.join(outputPath, "index.mjs"),
+ plugins,
});
// Build Lambda code (2nd pass)
@@ -367,7 +387,7 @@ function createImageOptimizationBundle() {
// For SHARP_IGNORE_GLOBAL_LIBVIPS see: https://github.com/lovell/sharp/blob/main/docs/install.md#aws-lambda
const nodeOutputPath = path.resolve(outputPath);
- const sharpVersion = process.env.SHARP_VERSION ?? "0.33.2";
+ const sharpVersion = process.env.SHARP_VERSION ?? "0.32.6";
//check if we are running in Windows environment then set env variables accordingly.
try {
diff --git a/packages/tests-e2e/tests/appPagesRouter/image-optimization.test.ts b/packages/tests-e2e/tests/appPagesRouter/image-optimization.test.ts
new file mode 100644
index 00000000..c26cfe6e
--- /dev/null
+++ b/packages/tests-e2e/tests/appPagesRouter/image-optimization.test.ts
@@ -0,0 +1,20 @@
+import { expect, test } from "@playwright/test";
+
+test("Image Optimization", async ({ page }) => {
+ await page.goto("/");
+
+ const imageResponsePromise = page.waitForResponse(
+ /corporate_holiday_card.jpg/,
+ );
+ await page.locator('[href="/image-optimization"]').click();
+ const imageResponse = await imageResponsePromise;
+
+ await page.waitForURL("/image-optimization");
+
+ const imageContentType = imageResponse.headers()["content-type"];
+ expect(imageContentType).toBe("image/webp");
+
+ let el = page.locator("img");
+ await expect(el).toHaveJSProperty("complete", true);
+ await expect(el).not.toHaveJSProperty("naturalWidth", 0);
+});
diff --git a/packages/tests-e2e/tests/appRouter/image-optimization.test.ts b/packages/tests-e2e/tests/appRouter/image-optimization.test.ts
new file mode 100644
index 00000000..66eb64ad
--- /dev/null
+++ b/packages/tests-e2e/tests/appRouter/image-optimization.test.ts
@@ -0,0 +1,20 @@
+import { expect, test } from "@playwright/test";
+
+test("Image Optimization", async ({ page }) => {
+ await page.goto("/");
+
+ const imageResponsePromise = page.waitForResponse(
+ /https%3A%2F%2Fopen-next.js.org%2Farchitecture.png/,
+ );
+ await page.locator('[href="/image-optimization"]').click();
+ const imageResponse = await imageResponsePromise;
+
+ await page.waitForURL("/image-optimization");
+
+ const imageContentType = imageResponse.headers()["content-type"];
+ expect(imageContentType).toBe("image/webp");
+
+ let el = page.locator("img");
+ await expect(el).toHaveJSProperty("complete", true);
+ await expect(el).not.toHaveJSProperty("naturalWidth", 0);
+});