Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/thirty-bears-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/aws": minor
---

Add support for Next 16
6 changes: 1 addition & 5 deletions examples/app-pages-router/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
poweredByHeader: false,
cleanDistDir: true,
transpilePackages: ["@example/shared"],
// transpilePackages: ["@example/shared"],
output: "standalone",
// outputFileTracingRoot: "../sst",
eslint: {
ignoreDuringBuilds: true,
},
trailingSlash: true,
skipTrailingSlashRedirect: true,
};
Expand Down
5 changes: 4 additions & 1 deletion examples/app-pages-router/open-next.config.local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export default {
},
loader: "fs-dev",
},
dangerous: {
enableCacheInterception: true,
},
// You can override the build command here so that you don't have to rebuild next every time you make a change
//buildCommand: "echo 'No build command'",
// buildCommand: "echo 'No build command'",
} satisfies OpenNextConfig;
2 changes: 1 addition & 1 deletion examples/app-pages-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"openbuild": "node ../../packages/open-next/dist/index.js build --build-command \"npx turbo build\"",
"openbuild:local": "node ../../packages/open-next/dist/index.js build --config-path open-next.config.local.ts",
"openbuild:local:start": "PORT=3003 tsx proxy.ts",
"openbuild:local:start": "PORT=3003 tsx on-proxy.ts",
"dev": "next dev --turbopack --port 3003",
"build": "next build",
"start": "next start --port 3003",
Expand Down
12 changes: 9 additions & 3 deletions examples/app-pages-router/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
Expand All @@ -23,11 +23,17 @@
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules",
"open-next.config.ts",
"open-next.config.local.ts",
"proxy.ts"
"on-proxy.ts"
]
}
3 changes: 2 additions & 1 deletion examples/app-router/app/api/after/revalidate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export function POST() {
() =>
new Promise<void>((resolve) =>
setTimeout(() => {
revalidateTag("date");
// We want to expire the "date" tag immediately
revalidateTag("date", { expire: 0 });
resolve();
}, 5000),
),
Expand Down
10 changes: 7 additions & 3 deletions examples/app-router/app/api/after/ssg/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import { NextResponse } from "next/server";
export const dynamic = "force-static";

export async function GET() {
const dateFn = unstable_cache(() => new Date().toISOString(), ["date"], {
tags: ["date"],
});
const dateFn = unstable_cache(
async () => new Date().toISOString(),
["date"],
{
tags: ["date"],
},
);
const date = await dateFn();
return NextResponse.json({ date });
}
2 changes: 1 addition & 1 deletion examples/app-router/app/api/revalidate-tag/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { revalidateTag } from "next/cache";
export const dynamic = "force-dynamic";

export async function GET() {
revalidateTag("revalidate");
revalidateTag("revalidate", { expire: 0 });

return new Response("ok");
}
2 changes: 1 addition & 1 deletion examples/app-router/app/isr-data-cache/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ async function getTime() {
return new Date().toISOString();
}

const cachedTime = unstable_cache(getTime, { revalidate: false });
const cachedTime = unstable_cache(getTime, ["getTime"], { revalidate: false });

export const revalidate = 10;

Expand Down
3 changes: 3 additions & 0 deletions examples/app-router/app/og/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export default async function Image() {
// For convenience, we can re-use the exported opengraph-image
// size config to also set the ImageResponse's width and height.
...size,
headers: {
"cache-control": "public, immutable, no-transform, max-age=31536000",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you set this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Next does not automatically set the cache on this anymore. Need to test with next start

},
},
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";

export function middleware(request: NextRequest) {
export default function middleware(request: NextRequest) {
const path = request.nextUrl.pathname; //new URL(request.url).pathname;

const host = request.headers.get("host");
Expand Down
10 changes: 8 additions & 2 deletions examples/app-router/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
Expand All @@ -23,7 +23,13 @@
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules",
"open-next.config.ts",
Expand Down
4 changes: 0 additions & 4 deletions examples/pages-router/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ const nextConfig: NextConfig = {
cleanDistDir: true,
reactStrictMode: true,
output: "standalone",
// outputFileTracingRoot: "../sst",
eslint: {
ignoreDuringBuilds: true,
},
headers: async () => [
{
source: "/",
Expand Down
2 changes: 1 addition & 1 deletion examples/pages-router/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"baseUrl": ".",
"paths": {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"openbuild:local": "turbo run openbuild:local",
"openbuild:local:start": "turbo run openbuild:local:start",
"clean": "turbo run clean && rm -rf node_modules pnpm-lock.yaml",
"lint": "biome check",
"lint:fix": "biome check --fix",
Expand Down
9 changes: 9 additions & 0 deletions packages/open-next/src/adapters/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,14 @@ export default class Cache {
}
if (cacheData?.type === "page" || cacheData?.type === "app") {
if (globalThis.isNextAfter15 && cacheData?.type === "app") {
const segmentData = new Map<string, Buffer>();
if (cacheData.segmentData) {
for (const [segmentPath, segmentContent] of Object.entries(
cacheData.segmentData ?? {},
)) {
segmentData.set(segmentPath, Buffer.from(segmentContent));
}
}
return {
lastModified: _lastModified,
value: {
Expand All @@ -157,6 +165,7 @@ export default class Cache {
status: meta?.status,
headers: meta?.headers,
postponed: meta?.postponed,
segmentData,
},
} as CacheHandlerValue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export async function optimizeImage(
const { isAbsolute, href } = imageParams;

const imageUpstream = isAbsolute
? await fetchExternalImage(href)
? //@ts-expect-error - fetchExternalImage signature has changed in Next.js 16, it has an extra boolean parameter.
await fetchExternalImage(href)
: await fetchInternalImage(
href,
// @ts-expect-error - It is supposed to be an IncomingMessage object, but only the headers are used.
Expand Down
13 changes: 11 additions & 2 deletions packages/open-next/src/build/copyTracedFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ File ${serverPath} does not exist

// Only files that are actually copied
const tracedFiles: string[] = [];
const erroredFiles: string[] = [];
//Actually copy the files
filesToCopy.forEach((to, from) => {
// We don't want to copy excluded packages (e.g. sharp)
Expand All @@ -285,7 +286,15 @@ File ${serverPath} does not exist
}
}
} else {
copyFileSync(from, to);
// Adding this inside a try-catch to handle errors on Next 16+
// where some files listed in the .nft.json might not be present in the standalone folder
// TODO: investigate that further - is it expected?
try {
copyFileSync(from, to);
} catch (e) {
logger.debug("Error copying file:", e);
erroredFiles.push(to);
}
}
});

Expand Down Expand Up @@ -383,7 +392,7 @@ File ${serverPath} does not exist
logger.debug("copyTracedFiles:", Date.now() - tsStart, "ms");

return {
tracedFiles,
tracedFiles: tracedFiles.filter((f) => !erroredFiles.includes(f)),
nodePackages,
manifests,
};
Expand Down
22 changes: 22 additions & 0 deletions packages/open-next/src/build/createAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ export function createCacheAssets(options: buildHelper.BuildOptions) {
const isFileSkipped = (relativePath: string) =>
relativePath.endsWith(".js") ||
relativePath.endsWith(".js.nft.json") ||
// We skip manifest files as well
relativePath.endsWith("-manifest.json") ||
// We skip the segment rsc files as they are treated in a different way
relativePath.endsWith(".segment.rsc") ||
(relativePath.endsWith(".html") && htmlPages.has(relativePath));

// Merge cache files into a single file
Expand Down Expand Up @@ -169,6 +173,23 @@ export function createCacheAssets(options: buildHelper.BuildOptions) {
logger.warn(`Skipping invalid cache file: ${cacheFilePath}`);
return;
}

// If we have a meta file, and it contains segmentPaths, we need to add them to the cache file
const segments: Record<string, string> = Array.isArray(
cacheFileMeta?.segmentPaths,
)
? Object.fromEntries(
cacheFileMeta!.segmentPaths.map((segmentPath: string) => {
const absoluteSegmentPath = path.join(
files.meta!.replace(/\.meta$/, ".segments"),
`${segmentPath}.segment.rsc`,
);
const segmentContent = fs.readFileSync(absoluteSegmentPath, "utf8");
return [segmentPath, segmentContent];
}),
)
: {};

const cacheFileContent = {
type: files.body ? "route" : files.json ? "page" : "app",
meta: cacheFileMeta,
Expand All @@ -184,6 +205,7 @@ export function createCacheAssets(options: buildHelper.BuildOptions) {
: "utf8",
)
: undefined,
segmentData: Object.keys(segments).length > 0 ? segments : undefined,
};

// Ensure directory exists before writing
Expand Down
67 changes: 53 additions & 14 deletions packages/open-next/src/core/routing/cacheInterceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { emptyReadableStream, toReadableStream } from "utils/stream";

import { isBinaryContentType } from "utils/binary";
import { getTagsFromValue, hasBeenRevalidated } from "utils/cache";
import { debug } from "../../adapters/logger";
import { debug, error } from "../../adapters/logger";
import { localizePath } from "./i18n";
import { generateMessageGroupId } from "./queue";

Expand All @@ -29,6 +29,9 @@ const CACHE_ONE_MONTH = 60 * 60 * 24 * 30;
*/
const VARY_HEADER =
"RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch, Next-Url";
const NEXT_SEGMENT_PREFETCH_HEADER = "next-router-segment-prefetch";
const NEXT_PRERENDER_HEADER = "x-nextjs-prerender";
const NEXT_POSTPONED_HEADER = "x-nextjs-postponed";

async function computeCacheControl(
path: string,
Expand Down Expand Up @@ -103,6 +106,34 @@ async function computeCacheControl(
};
}

function getBodyForAppRouter(
event: MiddlewareEvent,
cachedValue: CacheValue<"cache">,
): { body: string; additionalHeaders: Record<string, string> } {
if (cachedValue.type !== "app") {
throw new Error("getBodyForAppRouter called with non-app cache value");
}
try {
const segmentHeader = `${event.headers[NEXT_SEGMENT_PREFETCH_HEADER]}`;
const isSegmentResponse =
Boolean(segmentHeader) &&
segmentHeader in (cachedValue.segmentData || {});

const body = isSegmentResponse
? cachedValue.segmentData![segmentHeader]
: cachedValue.rsc;
return {
body,
additionalHeaders: isSegmentResponse
? { [NEXT_PRERENDER_HEADER]: "1", [NEXT_POSTPONED_HEADER]: "2" }
: {},
};
} catch (e) {
error("Error while getting body for app router from cache:", e);
return { body: cachedValue.rsc, additionalHeaders: {} };
}
}

async function generateResult(
event: MiddlewareEvent,
localizedPath: string,
Expand All @@ -113,19 +144,26 @@ async function generateResult(
let body = "";
let type = "application/octet-stream";
let isDataRequest = false;
switch (cachedValue.type) {
case "app":
isDataRequest = Boolean(event.headers.rsc);
body = isDataRequest ? cachedValue.rsc : cachedValue.html;
type = isDataRequest ? "text/x-component" : "text/html; charset=utf-8";
break;
case "page":
isDataRequest = Boolean(event.query.__nextDataReq);
body = isDataRequest
? JSON.stringify(cachedValue.json)
: cachedValue.html;
type = isDataRequest ? "application/json" : "text/html; charset=utf-8";
break;
let additionalHeaders = {};
if (cachedValue.type === "app") {
isDataRequest = Boolean(event.headers.rsc);
if (isDataRequest) {
const { body: appRouterBody, additionalHeaders: appHeaders } =
getBodyForAppRouter(event, cachedValue);
body = appRouterBody;
additionalHeaders = appHeaders;
} else {
body = cachedValue.html;
}
type = isDataRequest ? "text/x-component" : "text/html; charset=utf-8";
} else if (cachedValue.type === "page") {
isDataRequest = Boolean(event.query.__nextDataReq);
body = isDataRequest ? JSON.stringify(cachedValue.json) : cachedValue.html;
type = isDataRequest ? "application/json" : "text/html; charset=utf-8";
} else {
throw new Error(
"generateResult called with unsupported cache value type, only 'app' and 'page' are supported",
);
}
const cacheControl = await computeCacheControl(
localizedPath,
Expand All @@ -149,6 +187,7 @@ async function generateResult(
"content-type": type,
...cachedValue.meta?.headers,
vary: VARY_HEADER,
...additionalHeaders,
},
};
}
Expand Down
Loading
Loading