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 ( +
+ Corporate Holiday Card +
+ ); +} 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 ( +
+ Open Next architecture +
+ ); +} 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); +});