Skip to content

Commit

Permalink
Fix image optimization support for Next 14.1.1 (#377)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
chungweileong94 and conico974 authored Mar 6, 2024
1 parent 3deb202 commit af2d3ce
Show file tree
Hide file tree
Showing 14 changed files with 204 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/old-islands-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"open-next": patch
---

Fix Image Optimization Support for [email protected]
14 changes: 14 additions & 0 deletions examples/app-pages-router/app/image-optimization/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Image from "next/image";

export default function ImageOptimization() {
return (
<div>
<Image
src="/static/corporate_holiday_card.jpg"
alt="Corporate Holiday Card"
width={300}
height={300}
/>
</div>
);
}
3 changes: 3 additions & 0 deletions examples/app-pages-router/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export default function Home() {
<Nav href={"/parallel"} title="Parallel">
Parallel routing
</Nav>
<Nav href={"/image-optimization"} title="Image Optimization">
Image Optimization with next/image
</Nav>
</main>
<h1>Pages Router</h1>
<main className="grid grid-cols-2 gap-4 p-10 [&>a]:border">
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions examples/app-router/app/image-optimization/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Image from "next/image";

export default function ImageOptimization() {
return (
<div>
<Image
src="https://open-next.js.org/architecture.png"
alt="Open Next architecture"
width={300}
height={300}
/>
</div>
);
}
3 changes: 3 additions & 0 deletions examples/app-router/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export default function Home() {
<Nav href={"/sse"} title="Server Sent Events">
Server Sent Events via Streaming
</Nav>
<Nav href={"/image-optimization"} title="Image Optimization">
Image Optimization with next/image
</Nav>
</main>
</>
);
Expand Down
8 changes: 8 additions & 0 deletions examples/app-router/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ const nextConfig = {
experimental: {
serverActions: true,
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "open-next.js.org",
},
],
},
redirects: () => {
return [
{
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 7 additions & 19 deletions packages/open-next/src/adapters/image-optimization-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void>,
) {
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
35 changes: 35 additions & 0 deletions packages/open-next/src/adapters/plugins/image-optimization.ts
Original file line number Diff line number Diff line change
@@ -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<void>,
) {
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
28 changes: 24 additions & 4 deletions packages/open-next/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -315,7 +315,7 @@ function createRevalidationBundle() {
);
}

function createImageOptimizationBundle() {
async function createImageOptimizationBundle() {
logger.info(`Bundling image optimization function...`);

const { appPath, appBuildOutputPath, outputDir } = options;
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions packages/tests-e2e/tests/appPagesRouter/image-optimization.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
20 changes: 20 additions & 0 deletions packages/tests-e2e/tests/appRouter/image-optimization.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});

0 comments on commit af2d3ce

Please sign in to comment.