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 000000000..f43655af4 --- /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 0fd02970e..1b32a5c3d 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 000000000..0df96ae23 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 000000000..baba473bc --- /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 2f347d3ca..e07d65a31 100644 --- a/examples/app-router/app/page.tsx +++ b/examples/app-router/app/page.tsx @@ -47,6 +47,9 @@ export default function Home() { +
); diff --git a/examples/app-router/next.config.js b/examples/app-router/next.config.js index 447979148..ce0565a83 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 000000000..0df96ae23 Binary files /dev/null and b/examples/app-router/public/static/corporate_holiday_card.jpg differ diff --git a/packages/open-next/CHANGELOG.md b/packages/open-next/CHANGELOG.md index 40998f46b..e3fb0e95c 100644 --- a/packages/open-next/CHANGELOG.md +++ b/packages/open-next/CHANGELOG.md @@ -1,5 +1,18 @@ # open-next +## 2.3.7 + +### Patch Changes + +- 3235392: fix: prevent duplication of location header +- af2d3ce: Fix Image Optimization Support for Next@14.1.1 + +## 2.3.6 + +### Patch Changes + +- f9b90b6: Security fix: sharp@0.33.2 + ## 2.3.5 ### Patch Changes diff --git a/packages/open-next/src/adapters/image-optimization-adapter.ts b/packages/open-next/src/adapters/image-optimization-adapter.ts index f27ef0f42..f8a0b1434 100644 --- a/packages/open-next/src/adapters/image-optimization-adapter.ts +++ b/packages/open-next/src/adapters/image-optimization-adapter.ts @@ -8,12 +8,10 @@ import path from "node:path"; import { Writable } from "node:stream"; import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"; -import type { APIGatewayProxyEventHeaders } from "aws-lambda"; import { loadConfig } from "config/util.js"; // @ts-ignore import { defaultConfig } from "next/dist/server/config-shared"; import { - imageOptimizer, ImageOptimizerCache, // @ts-ignore } from "next/dist/server/image-optimizer"; @@ -23,6 +21,7 @@ import { ImageLoader, InternalEvent, InternalResult } from "types/open-next.js"; import { createGenericHandler } from "../core/createGenericHandler.js"; import { awsLogger, debug, error } from "./logger.js"; +import { optimizeImage } from "./plugins/image-optimization/image-optimization.js"; import { setNodeEnv } from "./util.js"; // Expected environment variables @@ -69,7 +68,12 @@ export async function defaultHandler( 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) { @@ -115,23 +119,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): InternalResult { return { type: "core", diff --git a/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.replacement.ts b/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.replacement.ts new file mode 100644 index 000000000..4b948be4d --- /dev/null +++ b/packages/open-next/src/adapters/plugins/image-optimization/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/image-optimization.ts b/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.ts new file mode 100644 index 000000000..5a9fc75ea --- /dev/null +++ b/packages/open-next/src/adapters/plugins/image-optimization/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 b1642a863..7b19e6a10 100755 --- a/packages/open-next/src/build.ts +++ b/packages/open-next/src/build.ts @@ -1,5 +1,6 @@ import cp from "node:child_process"; import fs, { readFileSync } from "node:fs"; +import { createRequire as topLevelCreateRequire } from "node:module"; import os from "node:os"; import path from "node:path"; import url from "node:url"; @@ -13,6 +14,7 @@ import { buildEdgeBundle } from "./build/edge/createEdgeBundle.js"; import { generateOutput } from "./build/generateOutput.js"; import { BuildOptions, + compareSemver, copyOpenNextConfig, esbuildAsync, esbuildSync, @@ -24,9 +26,11 @@ import { } from "./build/helper.js"; import { validateConfig } from "./build/validateConfig.js"; import logger from "./logger.js"; +import { openNextReplacementPlugin } from "./plugins/replacement.js"; import { openNextResolvePlugin } from "./plugins/resolve.js"; import { OpenNextConfig } from "./types/open-next.js"; +const require = topLevelCreateRequire(import.meta.url); const __dirname = url.fileURLToPath(new URL(".", import.meta.url)); let options: BuildOptions; let config: OpenNextConfig; @@ -74,9 +78,10 @@ export async function build(openNextConfigPath?: string) { createStaticAssets(); await createCacheAssets(monorepoRoot); + await createServerBundle(config, options); await createRevalidationBundle(); - createImageOptimizationBundle(); + await createImageOptimizationBundle(); await createWarmerBundle(); await generateOutput(options.appBuildOutputPath, config); logger.info("OpenNext build complete."); @@ -314,7 +319,7 @@ async function createRevalidationBundle() { ); } -function createImageOptimizationBundle() { +async function createImageOptimizationBundle() { logger.info(`Bundling image optimization function...`); const { appPath, appBuildOutputPath, outputDir } = options; @@ -326,17 +331,33 @@ function createImageOptimizationBundle() { // Copy open-next.config.mjs into the bundle copyOpenNextConfig(options.tempDir, outputPath); + const plugins = + compareSemver(options.nextVersion, "14.1.1") >= 0 + ? [ + openNextReplacementPlugin({ + name: "opennext-14.1.1-image-optimization", + target: /plugins\/image-optimization\/image-optimization\.js/g, + replacements: [ + require.resolve( + "./adapters/plugins/image-optimization/image-optimization.replacement.js", + ), + ], + }), + ] + : undefined; + // 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, }, options, ); @@ -375,7 +396,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.32.5"; + 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/open-next/src/core/routing/middleware.ts b/packages/open-next/src/core/routing/middleware.ts index b7dd0e0f6..2de20cfcc 100644 --- a/packages/open-next/src/core/routing/middleware.ts +++ b/packages/open-next/src/core/routing/middleware.ts @@ -122,21 +122,19 @@ export async function handleMiddleware( // If the middleware returned a Redirect, we set the `Location` header with // the redirected url and end the response. if (statusCode >= 300 && statusCode < 400) { - const location = result.headers - .get("location") - ?.replace( - "http://localhost:3000", - `https://${internalEvent.headers.host}`, - ); + resHeaders.location = + responseHeaders + .get("location") + ?.replace( + "http://localhost:3000", + `https://${internalEvent.headers.host}`, + ) ?? resHeaders.location; // res.setHeader("Location", location); return { body: "", type: internalEvent.type, statusCode: statusCode, - headers: { - ...resHeaders, - Location: location ?? "", - }, + headers: resHeaders, isBase64Encoded: false, }; } 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 000000000..c26cfe6ec --- /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 000000000..66eb64ad0 --- /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); +}); diff --git a/packages/tests-unit/CHANGELOG.md b/packages/tests-unit/CHANGELOG.md index 090bd5dd5..c95583030 100644 --- a/packages/tests-unit/CHANGELOG.md +++ b/packages/tests-unit/CHANGELOG.md @@ -4,6 +4,21 @@ ### Patch Changes +- Updated dependencies [3235392] +- Updated dependencies [af2d3ce] + - open-next@2.3.7 + +## null + +### Patch Changes + +- Updated dependencies [f9b90b6] + - open-next@2.3.6 + +## null + +### Patch Changes + - Updated dependencies [b9eefca] - Updated dependencies [c80f1be] - Updated dependencies [186e28f]