diff --git a/.changeset/lucky-ghosts-knock.md b/.changeset/lucky-ghosts-knock.md
new file mode 100644
index 000000000..d973d0f52
--- /dev/null
+++ b/.changeset/lucky-ghosts-knock.md
@@ -0,0 +1,5 @@
+---
+"@opennextjs/aws": patch
+---
+
+pass revalidate for ISR/SSG cache
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index af953ae09..d903a6025 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -111,6 +111,7 @@ jobs:
- name: Install Nextjs in app-pages-router
working-directory: examples/app-pages-router
run: pnpm add next@${{ needs.check_next_version.outputs.previousNextVersion }}
+ # We do not install the latest canary of Next in the experimental app.
- name: Get Playwright version
id: playwright-version
@@ -157,6 +158,8 @@ jobs:
echo "PAGES_ROUTER_URL=$PAGES_ROUTER_URL" >> $GITHUB_ENV
APP_PAGES_ROUTER_URL=$(jq -r '.["e2e-example-AppPagesRouter"].url' .sst/outputs.json)
echo "APP_PAGES_ROUTER_URL=$APP_PAGES_ROUTER_URL" >> $GITHUB_ENV
+ EXPERIMENTAL_APP_URL=$(jq -r '.["e2e-example-Experimental"].url' .sst/outputs.json)
+ echo "EXPERIMENTAL_APP_URL=$EXPERIMENTAL_APP_URL" >> $GITHUB_ENV
- name: Run e2e Test
run: npm run e2e:test
diff --git a/examples/experimental/.gitignore b/examples/experimental/.gitignore
new file mode 100644
index 000000000..5ef6a5207
--- /dev/null
+++ b/examples/experimental/.gitignore
@@ -0,0 +1,41 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/examples/experimental/README.md b/examples/experimental/README.md
new file mode 100644
index 000000000..0009a0255
--- /dev/null
+++ b/examples/experimental/README.md
@@ -0,0 +1,3 @@
+# Experimental
+
+This project is meant to test experimental features that are only available on canary builds of Next.js.
\ No newline at end of file
diff --git a/examples/experimental/next.config.ts b/examples/experimental/next.config.ts
new file mode 100644
index 000000000..c6ebc28eb
--- /dev/null
+++ b/examples/experimental/next.config.ts
@@ -0,0 +1,17 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+ /* config options here */
+ cleanDistDir: true,
+ output: "standalone",
+ eslint: {
+ ignoreDuringBuilds: true,
+ },
+ experimental: {
+ ppr: "incremental",
+ nodeMiddleware: true,
+ dynamicIO: true,
+ },
+};
+
+export default nextConfig;
diff --git a/examples/experimental/open-next.config.ts b/examples/experimental/open-next.config.ts
new file mode 100644
index 000000000..5ff6edf53
--- /dev/null
+++ b/examples/experimental/open-next.config.ts
@@ -0,0 +1,14 @@
+const config = {
+ default: {
+ override: {
+ wrapper: "aws-lambda-streaming",
+ queue: "sqs-lite",
+ incrementalCache: "s3-lite",
+ tagCache: "dynamodb-lite",
+ },
+ },
+ functions: {},
+ buildCommand: "npx turbo build",
+};
+
+export default config;
diff --git a/examples/experimental/package.json b/examples/experimental/package.json
new file mode 100644
index 000000000..795194259
--- /dev/null
+++ b/examples/experimental/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "experimental",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "openbuild": "node ../../packages/open-next/dist/index.js build",
+ "dev": "next dev --turbopack --port 3004",
+ "build": "next build",
+ "start": "next start --port 3004",
+ "lint": "next lint",
+ "clean": "rm -rf .turbo node_modules .next .open-next"
+ },
+ "dependencies": {
+ "next": "15.3.1-canary.13",
+ "react": "catalog:",
+ "react-dom": "catalog:"
+ },
+ "devDependencies": {
+ "@types/node": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "typescript": "catalog:"
+ }
+}
diff --git a/examples/experimental/src/app/api/revalidate/route.ts b/examples/experimental/src/app/api/revalidate/route.ts
new file mode 100644
index 000000000..da6b1e027
--- /dev/null
+++ b/examples/experimental/src/app/api/revalidate/route.ts
@@ -0,0 +1,6 @@
+import { revalidateTag } from "next/cache";
+
+export function GET() {
+ revalidateTag("fullyTagged");
+ return new Response("DONE");
+}
diff --git a/examples/experimental/src/app/favicon.ico b/examples/experimental/src/app/favicon.ico
new file mode 100644
index 000000000..718d6fea4
Binary files /dev/null and b/examples/experimental/src/app/favicon.ico differ
diff --git a/examples/experimental/src/app/globals.css b/examples/experimental/src/app/globals.css
new file mode 100644
index 000000000..e3734be15
--- /dev/null
+++ b/examples/experimental/src/app/globals.css
@@ -0,0 +1,42 @@
+:root {
+ --background: #ffffff;
+ --foreground: #171717;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --background: #0a0a0a;
+ --foreground: #ededed;
+ }
+}
+
+html,
+body {
+ max-width: 100vw;
+ overflow-x: hidden;
+}
+
+body {
+ color: var(--foreground);
+ background: var(--background);
+ font-family: Arial, Helvetica, sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+* {
+ box-sizing: border-box;
+ padding: 0;
+ margin: 0;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+@media (prefers-color-scheme: dark) {
+ html {
+ color-scheme: dark;
+ }
+}
diff --git a/examples/experimental/src/app/layout.tsx b/examples/experimental/src/app/layout.tsx
new file mode 100644
index 000000000..42fc323e4
--- /dev/null
+++ b/examples/experimental/src/app/layout.tsx
@@ -0,0 +1,32 @@
+import type { Metadata } from "next";
+import { Geist, Geist_Mono } from "next/font/google";
+import "./globals.css";
+
+const geistSans = Geist({
+ variable: "--font-geist-sans",
+ subsets: ["latin"],
+});
+
+const geistMono = Geist_Mono({
+ variable: "--font-geist-mono",
+ subsets: ["latin"],
+});
+
+export const metadata: Metadata = {
+ title: "Create Next App",
+ description: "Generated by create next app",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/examples/experimental/src/app/page.module.css b/examples/experimental/src/app/page.module.css
new file mode 100644
index 000000000..5c7b57e9e
--- /dev/null
+++ b/examples/experimental/src/app/page.module.css
@@ -0,0 +1,165 @@
+.page {
+ --gray-rgb: 0, 0, 0;
+ --gray-alpha-200: rgba(var(--gray-rgb), 0.08);
+ --gray-alpha-100: rgba(var(--gray-rgb), 0.05);
+
+ --button-primary-hover: #383838;
+ --button-secondary-hover: #f2f2f2;
+
+ display: grid;
+ grid-template-rows: 20px 1fr 20px;
+ align-items: center;
+ justify-items: center;
+ min-height: 100svh;
+ padding: 80px;
+ gap: 64px;
+ font-family: var(--font-geist-sans);
+}
+
+@media (prefers-color-scheme: dark) {
+ .page {
+ --gray-rgb: 255, 255, 255;
+ --gray-alpha-200: rgba(var(--gray-rgb), 0.145);
+ --gray-alpha-100: rgba(var(--gray-rgb), 0.06);
+
+ --button-primary-hover: #ccc;
+ --button-secondary-hover: #1a1a1a;
+ }
+}
+
+.main {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+ grid-row-start: 2;
+}
+
+.main ol {
+ font-family: var(--font-geist-mono);
+ padding-left: 0;
+ margin: 0;
+ font-size: 14px;
+ line-height: 24px;
+ letter-spacing: -0.01em;
+ list-style-position: inside;
+}
+
+.main li:not(:last-of-type) {
+ margin-bottom: 8px;
+}
+
+.main code {
+ font-family: inherit;
+ background: var(--gray-alpha-100);
+ padding: 2px 4px;
+ border-radius: 4px;
+ font-weight: 600;
+}
+
+.ctas {
+ display: flex;
+ gap: 16px;
+}
+
+.ctas a {
+ appearance: none;
+ border-radius: 128px;
+ height: 48px;
+ padding: 0 20px;
+ border: none;
+ border: 1px solid transparent;
+ transition: background 0.2s, color 0.2s, border-color 0.2s;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+ line-height: 20px;
+ font-weight: 500;
+}
+
+a.primary {
+ background: var(--foreground);
+ color: var(--background);
+ gap: 8px;
+}
+
+a.secondary {
+ border-color: var(--gray-alpha-200);
+ min-width: 158px;
+}
+
+.footer {
+ grid-row-start: 3;
+ display: flex;
+ gap: 24px;
+}
+
+.footer a {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.footer img {
+ flex-shrink: 0;
+}
+
+/* Enable hover only on non-touch devices */
+@media (hover: hover) and (pointer: fine) {
+ a.primary:hover {
+ background: var(--button-primary-hover);
+ border-color: transparent;
+ }
+
+ a.secondary:hover {
+ background: var(--button-secondary-hover);
+ border-color: transparent;
+ }
+
+ .footer a:hover {
+ text-decoration: underline;
+ text-underline-offset: 4px;
+ }
+}
+
+@media (max-width: 600px) {
+ .page {
+ padding: 32px;
+ padding-bottom: 80px;
+ }
+
+ .main {
+ align-items: center;
+ }
+
+ .main ol {
+ text-align: center;
+ }
+
+ .ctas {
+ flex-direction: column;
+ }
+
+ .ctas a {
+ font-size: 14px;
+ height: 40px;
+ padding: 0 16px;
+ }
+
+ a.secondary {
+ min-width: auto;
+ }
+
+ .footer {
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ .logo {
+ filter: invert();
+ }
+}
diff --git a/examples/experimental/src/app/page.tsx b/examples/experimental/src/app/page.tsx
new file mode 100644
index 000000000..f14e5dad0
--- /dev/null
+++ b/examples/experimental/src/app/page.tsx
@@ -0,0 +1,14 @@
+import Link from "next/link";
+import styles from "./page.module.css";
+
+export default function Home() {
+ return (
+
+
+
+ Incremental PPR
+
+
+
+ );
+}
diff --git a/examples/experimental/src/app/ppr/page.tsx b/examples/experimental/src/app/ppr/page.tsx
new file mode 100644
index 000000000..11d017a76
--- /dev/null
+++ b/examples/experimental/src/app/ppr/page.tsx
@@ -0,0 +1,16 @@
+import { DynamicComponent } from "@/components/dynamic";
+import { StaticComponent } from "@/components/static";
+import { Suspense } from "react";
+
+export const experimental_ppr = true;
+
+export default function PPRPage() {
+ return (
+
+
+ Loading...
}>
+
+
+
+ );
+}
diff --git a/examples/experimental/src/app/use-cache/isr/page.tsx b/examples/experimental/src/app/use-cache/isr/page.tsx
new file mode 100644
index 000000000..dd02f8a63
--- /dev/null
+++ b/examples/experimental/src/app/use-cache/isr/page.tsx
@@ -0,0 +1,17 @@
+import { FullyCachedComponent, ISRComponent } from "@/components/cached";
+import { Suspense } from "react";
+
+export default async function Page() {
+ // Not working for now, need a patch in next to disable full revalidation during ISR revalidation
+ return (
+
+
Cache
+ Loading...}>
+
+
+ Loading...}>
+
+
+
+ );
+}
diff --git a/examples/experimental/src/app/use-cache/layout.tsx b/examples/experimental/src/app/use-cache/layout.tsx
new file mode 100644
index 000000000..b21b82fe6
--- /dev/null
+++ b/examples/experimental/src/app/use-cache/layout.tsx
@@ -0,0 +1,13 @@
+import { Suspense } from "react";
+
+export default function Layout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ Loading...}>{children}
+
+ );
+}
diff --git a/examples/experimental/src/app/use-cache/ssr/page.tsx b/examples/experimental/src/app/use-cache/ssr/page.tsx
new file mode 100644
index 000000000..41a413d1b
--- /dev/null
+++ b/examples/experimental/src/app/use-cache/ssr/page.tsx
@@ -0,0 +1,20 @@
+import { FullyCachedComponent, ISRComponent } from "@/components/cached";
+import { headers } from "next/headers";
+import { Suspense } from "react";
+
+export default async function Page() {
+ // To opt into SSR
+ const _headers = await headers();
+ return (
+
+
Cache
+
{_headers.get("accept") ?? "No accept headers"}
+
Loading...}>
+
+
+
Loading...}>
+
+
+
+ );
+}
diff --git a/examples/experimental/src/components/cached.tsx b/examples/experimental/src/components/cached.tsx
new file mode 100644
index 000000000..7abaa010c
--- /dev/null
+++ b/examples/experimental/src/components/cached.tsx
@@ -0,0 +1,24 @@
+import { unstable_cacheLife, unstable_cacheTag } from "next/cache";
+
+export async function FullyCachedComponent() {
+ "use cache";
+ unstable_cacheTag("fullyTagged");
+ return (
+
+ );
+}
+
+export async function ISRComponent() {
+ "use cache";
+ unstable_cacheLife({
+ stale: 1,
+ revalidate: 5,
+ });
+ return (
+
+ );
+}
diff --git a/examples/experimental/src/components/dynamic.tsx b/examples/experimental/src/components/dynamic.tsx
new file mode 100644
index 000000000..a57a493e8
--- /dev/null
+++ b/examples/experimental/src/components/dynamic.tsx
@@ -0,0 +1,18 @@
+import { headers } from "next/headers";
+
+export async function DynamicComponent() {
+ const _headers = await headers();
+ // Simulate a delay to mimic server-side calls
+ const date = await new Promise((resolve) =>
+ setTimeout(() => {
+ resolve(new Date().toString());
+ }, 1000),
+ );
+ return (
+
+
Dynamic Component
+
This component should be SSR
+
{_headers.get("referer")}
+
+ );
+}
diff --git a/examples/experimental/src/components/static.tsx b/examples/experimental/src/components/static.tsx
new file mode 100644
index 000000000..8af73f9f0
--- /dev/null
+++ b/examples/experimental/src/components/static.tsx
@@ -0,0 +1,8 @@
+export function StaticComponent() {
+ return (
+
+
Static Component
+
This is a static component that does not change.
+
+ );
+}
diff --git a/examples/experimental/src/middleware.ts b/examples/experimental/src/middleware.ts
new file mode 100644
index 000000000..bb097d692
--- /dev/null
+++ b/examples/experimental/src/middleware.ts
@@ -0,0 +1,27 @@
+import crypto from "node:crypto";
+import { type NextRequest, NextResponse } from "next/server";
+
+export default function middleware(request: NextRequest) {
+ if (request.nextUrl.pathname === "/api/hello") {
+ return NextResponse.json({
+ name: "World",
+ });
+ }
+ if (request.nextUrl.pathname === "/redirect") {
+ return NextResponse.redirect(new URL("/", request.url));
+ }
+ if (request.nextUrl.pathname === "/rewrite") {
+ return NextResponse.rewrite(new URL("/", request.url));
+ }
+
+ return NextResponse.next({
+ headers: {
+ "x-middleware-test": "1",
+ "x-random-node": crypto.randomUUID(),
+ },
+ });
+}
+
+export const config = {
+ runtime: "nodejs",
+};
diff --git a/examples/experimental/tsconfig.json b/examples/experimental/tsconfig.json
new file mode 100644
index 000000000..c1334095f
--- /dev/null
+++ b/examples/experimental/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/examples/sst/sst.config.ts b/examples/sst/sst.config.ts
index 7d68f3f11..32881d67c 100644
--- a/examples/sst/sst.config.ts
+++ b/examples/sst/sst.config.ts
@@ -2,6 +2,7 @@ import type { SSTConfig } from "sst";
import { AppPagesRouter } from "./stacks/AppPagesRouter";
import { AppRouter } from "./stacks/AppRouter";
+import { Experimental } from "./stacks/Experimental";
import { PagesRouter } from "./stacks/PagesRouter";
export default {
@@ -12,6 +13,10 @@ export default {
};
},
stacks(app) {
- app.stack(AppRouter).stack(PagesRouter).stack(AppPagesRouter);
+ app
+ .stack(AppRouter)
+ .stack(PagesRouter)
+ .stack(AppPagesRouter)
+ .stack(Experimental);
},
} satisfies SSTConfig;
diff --git a/examples/sst/stacks/Experimental.ts b/examples/sst/stacks/Experimental.ts
new file mode 100644
index 000000000..d41603db2
--- /dev/null
+++ b/examples/sst/stacks/Experimental.ts
@@ -0,0 +1,14 @@
+import { OpenNextCdkReferenceImplementation } from "./OpenNextReferenceImplementation";
+
+export function Experimental({ stack }) {
+ const site = new OpenNextCdkReferenceImplementation(stack, "experimental", {
+ path: "../experimental",
+ environment: {
+ OPEN_NEXT_FORCE_NON_EMPTY_RESPONSE: "true",
+ },
+ });
+
+ stack.addOutputs({
+ url: `https://${site.distribution.domainName}`,
+ });
+}
diff --git a/packages/open-next/src/adapters/cache.ts b/packages/open-next/src/adapters/cache.ts
index fe573f7d7..48a9d067d 100644
--- a/packages/open-next/src/adapters/cache.ts
+++ b/packages/open-next/src/adapters/cache.ts
@@ -57,7 +57,7 @@ export default class Cache {
async getFetchCache(key: string, softTags?: string[], tags?: string[]) {
debug("get fetch cache", { key, softTags, tags });
try {
- const cachedEntry = await globalThis.incrementalCache.get(key, true);
+ const cachedEntry = await globalThis.incrementalCache.get(key, "fetch");
if (cachedEntry?.value === undefined) return null;
@@ -107,7 +107,7 @@ export default class Cache {
async getIncrementalCache(key: string): Promise {
try {
- const cachedEntry = await globalThis.incrementalCache.get(key, false);
+ const cachedEntry = await globalThis.incrementalCache.get(key, "cache");
if (!cachedEntry?.value) {
return null;
@@ -207,6 +207,7 @@ export default class Cache {
if (data === null || data === undefined) {
await globalThis.incrementalCache.delete(key);
} else {
+ const revalidate = this.extractRevalidateForSet(ctx);
switch (data.kind) {
case "ROUTE":
case "APP_ROUTE": {
@@ -224,8 +225,9 @@ export default class Cache {
status,
headers,
},
+ revalidate,
},
- false,
+ "cache",
);
break;
}
@@ -244,8 +246,9 @@ export default class Cache {
status,
headers,
},
+ revalidate,
},
- false,
+ "cache",
);
} else {
await globalThis.incrementalCache.set(
@@ -254,8 +257,9 @@ export default class Cache {
type: "page",
html,
json: pageData,
+ revalidate,
},
- false,
+ "cache",
);
}
break;
@@ -272,13 +276,14 @@ export default class Cache {
status,
headers,
},
+ revalidate,
},
- false,
+ "cache",
);
break;
}
case "FETCH":
- await globalThis.incrementalCache.set(key, data, true);
+ await globalThis.incrementalCache.set(key, data, "fetch");
break;
case "REDIRECT":
await globalThis.incrementalCache.set(
@@ -286,8 +291,9 @@ export default class Cache {
{
type: "redirect",
props: data.props,
+ revalidate,
},
- false,
+ "cache",
);
break;
case "IMAGE":
@@ -424,7 +430,8 @@ export default class Cache {
// If we use an in house version of getDerivedTags in build we should use it here instead of next's one
const derivedTags: string[] =
data?.kind === "FETCH"
- ? (ctx?.tags ?? data?.data?.tags ?? []) // before version 14 next.js used data?.data?.tags so we keep it for backward compatibility
+ ? //@ts-expect-error - On older versions of next, ctx was a number, but for these cases we use data?.data?.tags
+ (ctx?.tags ?? data?.data?.tags ?? []) // before version 14 next.js used data?.data?.tags so we keep it for backward compatibility
: data?.kind === "PAGE"
? (data.headers?.["x-next-cache-tags"]?.split(",") ?? [])
: [];
@@ -446,4 +453,22 @@ export default class Cache {
);
}
}
+
+ private extractRevalidateForSet(
+ ctx?: IncrementalCacheContext,
+ ): number | false | undefined {
+ if (ctx === undefined) {
+ return undefined;
+ }
+ if (typeof ctx === "number" || ctx === false) {
+ return ctx;
+ }
+ if ("revalidate" in ctx) {
+ return ctx.revalidate;
+ }
+ if ("cacheControl" in ctx) {
+ return ctx.cacheControl?.revalidate;
+ }
+ return undefined;
+ }
}
diff --git a/packages/open-next/src/adapters/composable-cache.ts b/packages/open-next/src/adapters/composable-cache.ts
new file mode 100644
index 000000000..72d624569
--- /dev/null
+++ b/packages/open-next/src/adapters/composable-cache.ts
@@ -0,0 +1,115 @@
+import type { ComposableCacheEntry, ComposableCacheHandler } from "types/cache";
+import { fromReadableStream, toReadableStream } from "utils/stream";
+import { debug } from "./logger";
+
+export default {
+ async get(cacheKey: string) {
+ try {
+ const result = await globalThis.incrementalCache.get(
+ cacheKey,
+ "composable",
+ );
+ if (!result || !result.value?.value) {
+ return undefined;
+ }
+
+ debug("composable cache result", result);
+
+ // We need to check if the tags associated with this entry has been revalidated
+ if (
+ globalThis.tagCache.mode === "nextMode" &&
+ result.value.tags.length > 0
+ ) {
+ const hasBeenRevalidated = await globalThis.tagCache.hasBeenRevalidated(
+ result.value.tags,
+ result.lastModified,
+ );
+ if (hasBeenRevalidated) return undefined;
+ } else if (
+ globalThis.tagCache.mode === "original" ||
+ globalThis.tagCache.mode === undefined
+ ) {
+ const hasBeenRevalidated =
+ (await globalThis.tagCache.getLastModified(
+ cacheKey,
+ result.lastModified,
+ )) === -1;
+ if (hasBeenRevalidated) return undefined;
+ }
+
+ return {
+ ...result.value,
+ value: toReadableStream(result.value.value),
+ };
+ } catch (e) {
+ debug("Cannot read composable cache entry");
+ return undefined;
+ }
+ },
+
+ async set(cacheKey: string, pendingEntry: Promise) {
+ const entry = await pendingEntry;
+ const valueToStore = await fromReadableStream(entry.value);
+ await globalThis.incrementalCache.set(
+ cacheKey,
+ {
+ ...entry,
+ value: valueToStore,
+ },
+ "composable",
+ );
+ if (globalThis.tagCache.mode === "original") {
+ const storedTags = await globalThis.tagCache.getByPath(cacheKey);
+ const tagsToWrite = entry.tags.filter((tag) => !storedTags.includes(tag));
+ if (tagsToWrite.length > 0) {
+ await globalThis.tagCache.writeTags(
+ tagsToWrite.map((tag) => ({ tag, path: cacheKey })),
+ );
+ }
+ }
+ },
+
+ async refreshTags() {
+ // We don't do anything for now, do we want to do something here ???
+ return;
+ },
+ async getExpiration(...tags: string[]) {
+ if (globalThis.tagCache.mode === "nextMode") {
+ return globalThis.tagCache.getLastRevalidated(tags);
+ }
+ // We always return 0 here, original tag cache are handled directly in the get part
+ // TODO: We need to test this more, i'm not entirely sure that this is working as expected
+ return 0;
+ },
+ async expireTags(...tags: string[]) {
+ if (globalThis.tagCache.mode === "nextMode") {
+ return globalThis.tagCache.writeTags(tags);
+ }
+ const tagCache = globalThis.tagCache;
+ const revalidatedAt = Date.now();
+ // For the original mode, we have more work to do here.
+ // We need to find all paths linked to to these tags
+ const pathsToUpdate = await Promise.all(
+ tags.map(async (tag) => {
+ const paths = await tagCache.getByTag(tag);
+ return paths.map((path) => ({
+ path,
+ tag,
+ revalidatedAt,
+ }));
+ }),
+ );
+ // We need to deduplicate paths, we use a set for that
+ const setToWrite = new Set<{ path: string; tag: string }>();
+ for (const entry of pathsToUpdate.flat()) {
+ setToWrite.add(entry);
+ }
+ await globalThis.tagCache.writeTags(Array.from(setToWrite));
+ },
+
+ // This one is necessary for older versions of next
+ async receiveExpiredTags(...tags: string[]) {
+ // This function does absolutely nothing
+ return;
+ },
+} satisfies ComposableCacheHandler;
diff --git a/packages/open-next/src/build/compileCache.ts b/packages/open-next/src/build/compileCache.ts
index fa30f2e3a..bf4af8412 100644
--- a/packages/open-next/src/build/compileCache.ts
+++ b/packages/open-next/src/build/compileCache.ts
@@ -15,7 +15,11 @@ export function compileCache(
) {
const { config } = options;
const ext = format === "cjs" ? "cjs" : "mjs";
- const outFile = path.join(options.buildDir, `cache.${ext}`);
+ const compiledCacheFile = path.join(options.buildDir, `cache.${ext}`);
+ const compiledComposableCacheFile = path.join(
+ options.buildDir,
+ `composable-cache.${ext}`,
+ );
const isAfter15 = buildHelper.compareSemver(
options.nextVersion,
@@ -23,11 +27,37 @@ export function compileCache(
"15.0.0",
);
+ // Normal cache
buildHelper.esbuildSync(
{
external: ["next", "styled-jsx", "react", "@aws-sdk/*"],
entryPoints: [path.join(options.openNextDistDir, "adapters", "cache.js")],
- outfile: outFile,
+ outfile: compiledCacheFile,
+ target: ["node18"],
+ format,
+ banner: {
+ js: [
+ `globalThis.disableIncrementalCache = ${
+ config.dangerous?.disableIncrementalCache ?? false
+ };`,
+ `globalThis.disableDynamoDBCache = ${
+ config.dangerous?.disableTagCache ?? false
+ };`,
+ `globalThis.isNextAfter15 = ${isAfter15};`,
+ ].join(""),
+ },
+ },
+ options,
+ );
+
+ // Composable cache
+ buildHelper.esbuildSync(
+ {
+ external: ["next", "styled-jsx", "react", "@aws-sdk/*"],
+ entryPoints: [
+ path.join(options.openNextDistDir, "adapters", "composable-cache.js"),
+ ],
+ outfile: compiledComposableCacheFile,
target: ["node18"],
format,
banner: {
@@ -44,5 +74,9 @@ export function compileCache(
},
options,
);
- return outFile;
+
+ return {
+ cache: compiledCacheFile,
+ composableCache: compiledComposableCacheFile,
+ };
}
diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts
index 694ef50c9..3582f5549 100644
--- a/packages/open-next/src/build/createServerBundle.ts
+++ b/packages/open-next/src/build/createServerBundle.ts
@@ -28,6 +28,7 @@ import {
patchFetchCacheSetMissingWaitUntil,
patchNextServer,
patchUnstableCacheForISR,
+ patchUseCacheForISR,
} from "./patch/patches/index.js";
interface CodeCustomization {
@@ -147,10 +148,16 @@ async function generateBundle(
fs.mkdirSync(outPackagePath, { recursive: true });
const ext = fnOptions.runtime === "deno" ? "mjs" : "cjs";
+ // Normal cache
fs.copyFileSync(
path.join(options.buildDir, `cache.${ext}`),
path.join(outPackagePath, "cache.cjs"),
);
+ // Composable cache
+ fs.copyFileSync(
+ path.join(options.buildDir, `composable-cache.${ext}`),
+ path.join(outPackagePath, "composable-cache.cjs"),
+ );
if (fnOptions.runtime === "deno") {
addDenoJson(outputPath, packagePath);
@@ -206,6 +213,7 @@ async function generateBundle(
patchNextServer,
patchEnvVars,
patchBackgroundRevalidation,
+ patchUseCacheForISR,
...additionalCodePatches,
]);
@@ -237,6 +245,12 @@ async function generateBundle(
"14.2",
);
+ const isAfter152 = buildHelper.compareSemver(
+ options.nextVersion,
+ ">=",
+ "15.2.0",
+ );
+
const disableRouting = isBefore13413 || config.middleware?.external;
const updater = new ContentUpdater(options);
@@ -265,6 +279,7 @@ async function generateBundle(
...(isAfter141
? ["experimentalIncrementalCacheHandler"]
: ["stableIncrementalCache"]),
+ ...(isAfter152 ? [] : ["composableCache"]),
],
}),
diff --git a/packages/open-next/src/build/patch/patches/index.ts b/packages/open-next/src/build/patch/patches/index.ts
index 22ef817ba..bd46d6532 100644
--- a/packages/open-next/src/build/patch/patches/index.ts
+++ b/packages/open-next/src/build/patch/patches/index.ts
@@ -3,6 +3,7 @@ export { patchNextServer } from "./patchNextServer.js";
export {
patchFetchCacheForISR,
patchUnstableCacheForISR,
+ patchUseCacheForISR,
} from "./patchFetchCacheISR.js";
export { patchFetchCacheSetMissingWaitUntil } from "./patchFetchCacheWaitUntil.js";
export { patchBackgroundRevalidation } from "./patchBackgroundRevalidation.js";
diff --git a/packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts b/packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts
index 17ccea628..ca858c0fd 100644
--- a/packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts
+++ b/packages/open-next/src/build/patch/patches/patchFetchCacheISR.ts
@@ -78,6 +78,29 @@ fix:
($STORE_OR_CACHE.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation)
`;
+export const useCacheRule = `
+rule:
+ kind: member_expression
+ pattern: $STORE_OR_CACHE.isOnDemandRevalidate
+ inside:
+ kind: binary_expression
+ has:
+ kind: member_expression
+ pattern: $STORE_OR_CACHE.isDraftMode
+ inside:
+ kind: if_statement
+ stopBy: end
+ has:
+ kind: return_statement
+ any:
+ - has:
+ kind: 'true'
+ - has:
+ regex: '!0'
+ stopBy: end
+fix:
+ '($STORE_OR_CACHE.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation)'`;
+
export const patchFetchCacheForISR: CodePatcher = {
name: "patch-fetch-cache-for-isr",
patches: [
@@ -111,3 +134,20 @@ export const patchUnstableCacheForISR: CodePatcher = {
},
],
};
+
+export const patchUseCacheForISR: CodePatcher = {
+ name: "patch-use-cache-for-isr",
+ patches: [
+ {
+ versions: ">=15.3.0",
+ field: {
+ pathFilter: getCrossPlatformPathRegex(
+ String.raw`(server/chunks/.*\.js|.*\.runtime\..*\.js|use-cache/use-cache-wrapper\.js)$`,
+ { escape: false },
+ ),
+ contentFilter: /\.isOnDemandRevalidate/,
+ patchCode: createPatchCode(useCacheRule, Lang.JavaScript),
+ },
+ },
+ ],
+};
diff --git a/packages/open-next/src/core/routing/cacheInterceptor.ts b/packages/open-next/src/core/routing/cacheInterceptor.ts
index bb8814f12..9a2aaf88c 100644
--- a/packages/open-next/src/core/routing/cacheInterceptor.ts
+++ b/packages/open-next/src/core/routing/cacheInterceptor.ts
@@ -89,7 +89,7 @@ async function computeCacheControl(
async function generateResult(
event: InternalEvent,
localizedPath: string,
- cachedValue: CacheValue,
+ cachedValue: CacheValue<"cache">,
lastModified?: number,
): Promise {
debug("Returning result from experimental cache");
diff --git a/packages/open-next/src/core/util.ts b/packages/open-next/src/core/util.ts
index 876710d2a..d6b89d38a 100644
--- a/packages/open-next/src/core/util.ts
+++ b/packages/open-next/src/core/util.ts
@@ -25,6 +25,7 @@ overrideNextjsRequireHooks(NextConfig);
applyNextjsRequireHooksOverride();
//#endOverride
const cacheHandlerPath = require.resolve("./cache.cjs");
+const composableCacheHandlerPath = require.resolve("./composable-cache.cjs");
// @ts-ignore
const nextServer = new NextServer.default({
//#override requestHandlerHost
@@ -52,6 +53,12 @@ const nextServer = new NextServer.default({
//#override experimentalIncrementalCacheHandler
incrementalCacheHandlerPath: cacheHandlerPath,
//#endOverride
+
+ //#override composableCache
+ cacheHandlers: {
+ default: composableCacheHandlerPath,
+ },
+ //#endOverride
},
},
customServer: false,
diff --git a/packages/open-next/src/overrides/incrementalCache/multi-tier-ddb-s3.ts b/packages/open-next/src/overrides/incrementalCache/multi-tier-ddb-s3.ts
index 49c254258..5ee8bcc32 100644
--- a/packages/open-next/src/overrides/incrementalCache/multi-tier-ddb-s3.ts
+++ b/packages/open-next/src/overrides/incrementalCache/multi-tier-ddb-s3.ts
@@ -1,4 +1,8 @@
-import type { CacheValue, IncrementalCache } from "types/overrides";
+import type {
+ CacheEntryType,
+ CacheValue,
+ IncrementalCache,
+} from "types/overrides";
import { customFetchClient } from "utils/fetch";
import { LRUCache } from "utils/lru";
import { debug } from "../../adapters/logger";
@@ -50,11 +54,14 @@ const buildDynamoKey = (key: string) => {
*/
const multiTierCache: IncrementalCache = {
name: "multi-tier-ddb-s3",
- async get(key: string, isFetch?: IsFetch) {
+ async get(
+ key: string,
+ isFetch?: CacheType,
+ ) {
// First we check the local cache
const localCacheEntry = localCache.get(key) as
| {
- value: CacheValue;
+ value: CacheValue;
lastModified: number;
}
| undefined;
diff --git a/packages/open-next/src/overrides/incrementalCache/s3-lite.ts b/packages/open-next/src/overrides/incrementalCache/s3-lite.ts
index e24db620a..e2355be34 100644
--- a/packages/open-next/src/overrides/incrementalCache/s3-lite.ts
+++ b/packages/open-next/src/overrides/incrementalCache/s3-lite.ts
@@ -44,13 +44,10 @@ function buildS3Key(key: string, extension: Extension) {
}
const incrementalCache: IncrementalCache = {
- async get(key, isFetch) {
- const result = await awsFetch(
- buildS3Key(key, isFetch ? "fetch" : "cache"),
- {
- method: "GET",
- },
- );
+ async get(key, cacheType) {
+ const result = await awsFetch(buildS3Key(key, cacheType ?? "cache"), {
+ method: "GET",
+ });
if (result.status === 404) {
throw new IgnorableError("Not found");
@@ -66,14 +63,11 @@ const incrementalCache: IncrementalCache = {
).getTime(),
};
},
- async set(key, value, isFetch): Promise {
- const response = await awsFetch(
- buildS3Key(key, isFetch ? "fetch" : "cache"),
- {
- method: "PUT",
- body: JSON.stringify(value),
- },
- );
+ async set(key, value, cacheType): Promise {
+ const response = await awsFetch(buildS3Key(key, cacheType ?? "cache"), {
+ method: "PUT",
+ body: JSON.stringify(value),
+ });
if (response.status !== 200) {
throw new RecoverableError(`Failed to set cache: ${response.status}`);
}
diff --git a/packages/open-next/src/overrides/incrementalCache/s3.ts b/packages/open-next/src/overrides/incrementalCache/s3.ts
index 0ee7f6b51..371499209 100644
--- a/packages/open-next/src/overrides/incrementalCache/s3.ts
+++ b/packages/open-next/src/overrides/incrementalCache/s3.ts
@@ -40,11 +40,11 @@ function buildS3Key(key: string, extension: Extension) {
}
const incrementalCache: IncrementalCache = {
- async get(key, isFetch) {
+ async get(key, cacheType) {
const result = await s3Client.send(
new GetObjectCommand({
Bucket: CACHE_BUCKET_NAME,
- Key: buildS3Key(key, isFetch ? "fetch" : "cache"),
+ Key: buildS3Key(key, cacheType ?? "cache"),
}),
);
@@ -56,11 +56,11 @@ const incrementalCache: IncrementalCache = {
lastModified: result.LastModified?.getTime(),
};
},
- async set(key, value, isFetch): Promise {
+ async set(key, value, cacheType): Promise {
await s3Client.send(
new PutObjectCommand({
Bucket: CACHE_BUCKET_NAME,
- Key: buildS3Key(key, isFetch ? "fetch" : "cache"),
+ Key: buildS3Key(key, cacheType ?? "cache"),
Body: JSON.stringify(value),
}),
);
diff --git a/packages/open-next/src/overrides/tagCache/dynamodb-nextMode.ts b/packages/open-next/src/overrides/tagCache/dynamodb-nextMode.ts
index 7d16bebfc..1d4a5da38 100644
--- a/packages/open-next/src/overrides/tagCache/dynamodb-nextMode.ts
+++ b/packages/open-next/src/overrides/tagCache/dynamodb-nextMode.ts
@@ -71,6 +71,10 @@ function buildDynamoObject(tag: string, revalidatedAt?: number) {
export default {
name: "ddb-nextMode",
mode: "nextMode",
+ getLastRevalidated: async (tags: string[]) => {
+ // Not supported for now
+ return 0;
+ },
hasBeenRevalidated: async (tags: string[], lastModified?: number) => {
if (globalThis.openNextConfig.dangerous?.disableTagCache) {
return false;
diff --git a/packages/open-next/src/overrides/tagCache/fs-dev.ts b/packages/open-next/src/overrides/tagCache/fs-dev.ts
index 4de4d0a75..f6f61e992 100644
--- a/packages/open-next/src/overrides/tagCache/fs-dev.ts
+++ b/packages/open-next/src/overrides/tagCache/fs-dev.ts
@@ -2,6 +2,7 @@ import type { TagCache } from "types/overrides";
import fs from "node:fs";
+// TODO: fix this for monorepo
const tagFile = "../../dynamodb-provider/dynamodb-cache.json";
const tagContent = fs.readFileSync(tagFile, "utf-8");
@@ -44,7 +45,7 @@ const tagCache: TagCache = {
newTags.map((tag) => ({
tag: { S: tag.tag },
path: { S: tag.path },
- revalidatedAt: { N: String(tag.revalidatedAt) },
+ revalidatedAt: { N: String(tag.revalidatedAt ?? 1) },
})),
);
},
diff --git a/packages/open-next/src/types/cache.ts b/packages/open-next/src/types/cache.ts
index b37997f38..a2de4d2c3 100644
--- a/packages/open-next/src/types/cache.ts
+++ b/packages/open-next/src/types/cache.ts
@@ -1,3 +1,5 @@
+import type { ReadableStream } from "node:stream/web";
+
interface CachedFetchValue {
kind: "FETCH";
data: {
@@ -79,7 +81,7 @@ export interface CacheHandlerValue {
value: IncrementalCacheValue | null;
}
-export type Extension = "cache" | "fetch";
+export type Extension = "cache" | "fetch" | "composable";
type MetaHeaders = {
"x-next-cache-tags"?: string;
@@ -98,10 +100,72 @@ export type TagCacheMetaFile = {
revalidatedAt: { N: string };
};
-export type IncrementalCacheContext = {
- revalidate?: number | false | undefined;
- fetchCache?: boolean | undefined;
- fetchUrl?: string | undefined;
- fetchIdx?: number | undefined;
- tags?: string[] | undefined;
+// Cache context since https://github.com/vercel/next.js/pull/76207
+interface SetIncrementalFetchCacheContext {
+ fetchCache: true;
+ fetchUrl?: string;
+ fetchIdx?: number;
+ tags?: string[];
+}
+
+interface SetIncrementalResponseCacheContext {
+ fetchCache?: false;
+ cacheControl?: {
+ revalidate: number | false;
+ expire?: number;
+ };
+
+ /**
+ * True if the route is enabled for PPR.
+ */
+ isRoutePPREnabled?: boolean;
+
+ /**
+ * True if this is a fallback request.
+ */
+ isFallback?: boolean;
+}
+
+// Before #76207 revalidate was passed this way
+interface SetIncrementalCacheContext {
+ revalidate?: number | false;
+ isRoutePPREnabled?: boolean;
+ isFallback?: boolean;
+}
+
+// Before https://github.com/vercel/next.js/pull/53321 context on set was just the revalidate
+type OldSetIncrementalCacheContext = number | false | undefined;
+
+export type IncrementalCacheContext =
+ | OldSetIncrementalCacheContext
+ | SetIncrementalCacheContext
+ | SetIncrementalFetchCacheContext
+ | SetIncrementalResponseCacheContext;
+
+export interface ComposableCacheEntry {
+ value: ReadableStream;
+ tags: string[];
+ stale: number;
+ timestamp: number;
+ expire: number;
+ revalidate: number;
+}
+
+export type StoredComposableCacheEntry = Omit & {
+ value: string;
};
+
+export interface ComposableCacheHandler {
+ get(cacheKey: string): Promise;
+ set(
+ cacheKey: string,
+ pendingEntry: Promise,
+ ): Promise;
+ refreshTags(): Promise;
+ getExpiration(...tags: string[]): Promise;
+ expireTags(...tags: string[]): Promise;
+ /**
+ * This function is only there for older versions and do nothing
+ */
+ receiveExpiredTags(...tags: string[]): Promise;
+}
diff --git a/packages/open-next/src/types/overrides.ts b/packages/open-next/src/types/overrides.ts
index 81325c8b8..4d8eb02d9 100644
--- a/packages/open-next/src/types/overrides.ts
+++ b/packages/open-next/src/types/overrides.ts
@@ -1,6 +1,7 @@
import type { Readable } from "node:stream";
-import type { Meta } from "types/cache";
+import type { Extension, Meta, StoredComposableCacheEntry } from "types/cache";
+
import type {
BaseEventOrResult,
BaseOverride,
@@ -77,19 +78,29 @@ export type WithLastModified = {
value?: T;
};
-export type CacheValue = (IsFetch extends true
- ? CachedFetchValue
- : CachedFile) & { revalidate?: number | false };
+export type CacheEntryType = Extension;
+
+export type CacheValue =
+ (CacheType extends "fetch"
+ ? CachedFetchValue
+ : CacheType extends "cache"
+ ? CachedFile
+ : StoredComposableCacheEntry) & {
+ /**
+ * This is available for page cache entry, but only at runtime.
+ */
+ revalidate?: number | false;
+ };
export type IncrementalCache = {
- get(
+ get(
key: string,
- isFetch?: IsFetch,
- ): Promise> | null>;
- set(
+ cacheType?: CacheType,
+ ): Promise> | null>;
+ set(
key: string,
- value: CacheValue,
- isFetch?: IsFetch,
+ value: CacheValue,
+ isFetch?: CacheType,
): Promise;
delete(key: string): Promise;
name: string;
@@ -124,6 +135,8 @@ Cons :
*/
export type NextModeTagCache = BaseTagCache & {
mode: "nextMode";
+ // Necessary for the composable cache
+ getLastRevalidated(tags: string[]): Promise;
hasBeenRevalidated(tags: string[], lastModified?: number): Promise;
writeTags(tags: string[]): Promise;
// Optional method to get paths by tags
diff --git a/packages/open-next/src/utils/cache.ts b/packages/open-next/src/utils/cache.ts
index 98c028004..5072e4bef 100644
--- a/packages/open-next/src/utils/cache.ts
+++ b/packages/open-next/src/utils/cache.ts
@@ -28,7 +28,7 @@ export async function hasBeenRevalidated(
return _lastModified === -1;
}
-export function getTagsFromValue(value?: CacheValue) {
+export function getTagsFromValue(value?: CacheValue<"cache">) {
if (!value) {
return [];
}
diff --git a/packages/tests-e2e/playwright.config.js b/packages/tests-e2e/playwright.config.js
index fe7e08be2..097102242 100644
--- a/packages/tests-e2e/playwright.config.js
+++ b/packages/tests-e2e/playwright.config.js
@@ -23,5 +23,12 @@ export default defineConfig({
baseURL: process.env.APP_PAGES_ROUTER_URL || "http://localhost:3003",
},
},
+ {
+ name: "experimental",
+ testMatch: ["tests/experimental/*.test.ts"],
+ use: {
+ baseURL: process.env.EXPERIMENTAL_APP_URL || "http://localhost:3004",
+ },
+ },
],
});
diff --git a/packages/tests-e2e/tests/experimental/nodeMiddleware.test.ts b/packages/tests-e2e/tests/experimental/nodeMiddleware.test.ts
new file mode 100644
index 000000000..b679776a6
--- /dev/null
+++ b/packages/tests-e2e/tests/experimental/nodeMiddleware.test.ts
@@ -0,0 +1,29 @@
+import { expect, test } from "@playwright/test";
+
+test("Node middleware should add headers", async ({ request }) => {
+ const resp = await request.get("/");
+ expect(resp.status()).toEqual(200);
+ const headers = resp.headers();
+ expect(headers["x-middleware-test"]).toEqual("1");
+ expect(headers["x-random-node"]).toBeDefined();
+});
+
+test("Node middleware should return json", async ({ request }) => {
+ const resp = await request.get("/api/hello");
+ expect(resp.status()).toEqual(200);
+ const json = await resp.json();
+ expect(json).toEqual({ name: "World" });
+});
+
+test("Node middleware should redirect", async ({ page }) => {
+ await page.goto("/redirect");
+ await page.waitForURL("/");
+ const el = page.getByText("Incremental PPR");
+ await expect(el).toBeVisible();
+});
+
+test("Node middleware should rewrite", async ({ page }) => {
+ await page.goto("/rewrite");
+ const el = page.getByText("Incremental PPR");
+ await expect(el).toBeVisible();
+});
diff --git a/packages/tests-e2e/tests/experimental/ppr.test.ts b/packages/tests-e2e/tests/experimental/ppr.test.ts
new file mode 100644
index 000000000..896571186
--- /dev/null
+++ b/packages/tests-e2e/tests/experimental/ppr.test.ts
@@ -0,0 +1,22 @@
+import { expect, test } from "@playwright/test";
+
+test("PPR should show loading first", async ({ page }) => {
+ await page.goto("/");
+ await page.getByRole("link", { name: "Incremental PPR" }).click();
+ await page.waitForURL("/ppr");
+ const loading = page.getByText("Loading...");
+ await expect(loading).toBeVisible();
+ const el = page.getByText("Dynamic Component");
+ await expect(el).toBeVisible();
+});
+
+test("PPR rsc prefetch request should be cached", async ({ request }) => {
+ const resp = await request.get("/ppr", {
+ headers: { rsc: "1", "next-router-prefetch": "1" },
+ });
+ expect(resp.status()).toEqual(200);
+ const headers = resp.headers();
+ expect(headers["x-nextjs-postponed"]).toEqual("1");
+ expect(headers["x-nextjs-cache"]).toEqual("HIT");
+ expect(headers["cache-control"]).toEqual("s-maxage=31536000");
+});
diff --git a/packages/tests-e2e/tests/experimental/use-cache.test.ts b/packages/tests-e2e/tests/experimental/use-cache.test.ts
new file mode 100644
index 000000000..8f4111717
--- /dev/null
+++ b/packages/tests-e2e/tests/experimental/use-cache.test.ts
@@ -0,0 +1,87 @@
+import { expect, test } from "@playwright/test";
+
+test("cached component should work in ssr", async ({ page }) => {
+ await page.goto("/use-cache/ssr");
+ let fullyCachedElt = page.getByTestId("fullyCached");
+ let isrElt = page.getByTestId("isr");
+ await expect(fullyCachedElt).toBeVisible();
+ await expect(isrElt).toBeVisible();
+
+ const initialFullyCachedText = await fullyCachedElt.textContent();
+ const initialIsrText = await isrElt.textContent();
+
+ let isrText = initialIsrText;
+
+ do {
+ await page.reload();
+ fullyCachedElt = page.getByTestId("fullyCached");
+ isrElt = page.getByTestId("isr");
+ await expect(fullyCachedElt).toBeVisible();
+ await expect(isrElt).toBeVisible();
+ isrText = await isrElt.textContent();
+ await page.waitForTimeout(1000);
+ } while (isrText === initialIsrText);
+ const fullyCachedText = await fullyCachedElt.textContent();
+ expect(fullyCachedText).toEqual(initialFullyCachedText);
+});
+
+test("revalidateTag should work for fullyCached component", async ({
+ page,
+ request,
+}) => {
+ await page.goto("/use-cache/ssr");
+ const fullyCachedElt = page.getByTestId("fullyCached");
+ await expect(fullyCachedElt).toBeVisible();
+
+ const initialFullyCachedText = await fullyCachedElt.textContent();
+
+ const resp = await request.get("/api/revalidate");
+ expect(resp.status()).toEqual(200);
+ expect(await resp.text()).toEqual("DONE");
+
+ await page.reload();
+ await expect(fullyCachedElt).toBeVisible();
+ const newFullyCachedText = await fullyCachedElt.textContent();
+ expect(newFullyCachedText).not.toEqual(initialFullyCachedText);
+});
+
+test("cached component should work in isr", async ({ page }) => {
+ await page.goto("/use-cache/isr");
+
+ let fullyCachedElt = page.getByTestId("fullyCached");
+ let isrElt = page.getByTestId("isr");
+
+ await expect(fullyCachedElt).toBeVisible();
+ await expect(isrElt).toBeVisible();
+
+ let initialFullyCachedText = await fullyCachedElt.textContent();
+ let initialIsrText = await isrElt.textContent();
+
+ // We have to force reload until ISR has triggered at least once, otherwise the test will be flakey
+
+ let isrText = initialIsrText;
+
+ while (isrText === initialIsrText) {
+ await page.reload();
+ isrElt = page.getByTestId("isr");
+ fullyCachedElt = page.getByTestId("fullyCached");
+ await expect(isrElt).toBeVisible();
+ isrText = await isrElt.textContent();
+ await expect(fullyCachedElt).toBeVisible();
+ initialFullyCachedText = await fullyCachedElt.textContent();
+ await page.waitForTimeout(1000);
+ }
+ initialIsrText = isrText;
+
+ do {
+ await page.reload();
+ fullyCachedElt = page.getByTestId("fullyCached");
+ isrElt = page.getByTestId("isr");
+ await expect(fullyCachedElt).toBeVisible();
+ await expect(isrElt).toBeVisible();
+ isrText = await isrElt.textContent();
+ await page.waitForTimeout(1000);
+ } while (isrText === initialIsrText);
+ const fullyCachedText = await fullyCachedElt.textContent();
+ expect(fullyCachedText).toEqual(initialFullyCachedText);
+});
diff --git a/packages/tests-unit/tests/adapters/cache.test.ts b/packages/tests-unit/tests/adapters/cache.test.ts
index 4dff3166c..e4e3977fd 100644
--- a/packages/tests-unit/tests/adapters/cache.test.ts
+++ b/packages/tests-unit/tests/adapters/cache.test.ts
@@ -361,7 +361,7 @@ describe("CacheHandler", () => {
expect(incrementalCache.set).toHaveBeenCalledWith(
"key",
{ type: "route", body: "{}", meta: { status: 200, headers: {} } },
- false,
+ "cache",
);
});
@@ -382,7 +382,7 @@ describe("CacheHandler", () => {
body: Buffer.from("{}").toString("base64"),
meta: { status: 200, headers: { "content-type": "image/png" } },
},
- false,
+ "cache",
);
});
@@ -402,7 +402,7 @@ describe("CacheHandler", () => {
html: "",
json: {},
},
- false,
+ "cache",
);
});
@@ -423,7 +423,7 @@ describe("CacheHandler", () => {
rsc: "rsc",
meta: { status: 200, headers: {} },
},
- false,
+ "cache",
);
});
@@ -444,7 +444,7 @@ describe("CacheHandler", () => {
rsc: "rsc",
meta: { status: 200, headers: {} },
},
- false,
+ "cache",
);
});
@@ -474,7 +474,7 @@ describe("CacheHandler", () => {
},
revalidate: 60,
},
- true,
+ "fetch",
);
});
@@ -487,7 +487,7 @@ describe("CacheHandler", () => {
type: "redirect",
props: {},
},
- false,
+ "cache",
);
});
diff --git a/packages/tests-unit/tests/build/patch/patches/patchFetchCacheISR.test.ts b/packages/tests-unit/tests/build/patch/patches/patchFetchCacheISR.test.ts
index 649c5cc13..e6d421aee 100644
--- a/packages/tests-unit/tests/build/patch/patches/patchFetchCacheISR.test.ts
+++ b/packages/tests-unit/tests/build/patch/patches/patchFetchCacheISR.test.ts
@@ -2,6 +2,7 @@ import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
import {
fetchRule,
unstable_cacheRule,
+ useCacheRule,
} from "@opennextjs/aws/build/patch/patches/patchFetchCacheISR.js";
import { describe } from "vitest";
@@ -54,6 +55,24 @@ const patchFetchCacheCodeMinifiedNext15 = `
let t=P.isOnDemandRevalidate?null:await V.get(n,{kind:l.IncrementalCacheKind.FETCH,revalidate:_,fetchUrl:y,fetchIdx:X,tags:N,softTags:C});
`;
+const patchUseCacheUnminified = `
+function shouldForceRevalidate(workStore, workUnitStore) {
+ if (workStore.isOnDemandRevalidate || workStore.isDraftMode) {
+ return true;
+ }
+ if (workStore.dev && workUnitStore) {
+ if (workUnitStore.type === 'request') {
+ return workUnitStore.headers.get('cache-control') === 'no-cache';
+ }
+ if (workUnitStore.type === 'cache') {
+ return workUnitStore.forceRevalidate;
+ }
+ }
+ return false;
+}`;
+const patchUseCacheMinified = `
+function D(e,t){if(e.isOnDemandRevalidate||e.isDraftMode)return!0;if(e.dev&&t){if("request"===t.type)return"no-cache"===t.headers.get("cache-control");if("cache"===t.type)return t.forceRevalidate}return!1}`;
+
describe("patchUnstableCacheForISR", () => {
test("on unminified code", async () => {
expect(
@@ -124,3 +143,32 @@ describe("patchFetchCacheISR", () => {
});
//TODO: Add test for Next 14.2.24
});
+
+describe("patchUseCache", () => {
+ test("on unminified code", async () => {
+ expect(
+ patchCode(patchUseCacheUnminified, useCacheRule),
+ ).toMatchInlineSnapshot(`
+"function shouldForceRevalidate(workStore, workUnitStore) {
+ if ((workStore.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation) || workStore.isDraftMode) {
+ return true;
+ }
+ if (workStore.dev && workUnitStore) {
+ if (workUnitStore.type === 'request') {
+ return workUnitStore.headers.get('cache-control') === 'no-cache';
+ }
+ if (workUnitStore.type === 'cache') {
+ return workUnitStore.forceRevalidate;
+ }
+ }
+ return false;
+}"`);
+ });
+ test("on minified code", async () => {
+ expect(
+ patchCode(patchUseCacheMinified, useCacheRule),
+ ).toMatchInlineSnapshot(`
+"function D(e,t){if((e.isOnDemandRevalidate && !globalThis.__openNextAls?.getStore()?.isISRRevalidation)||e.isDraftMode)return!0;if(e.dev&&t){if("request"===t.type)return"no-cache"===t.headers.get("cache-control");if("cache"===t.type)return t.forceRevalidate}return!1}"
+`);
+ });
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4327bfa4f..9f216fbce 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -137,6 +137,31 @@ importers:
specifier: 'catalog:'
version: 5.6.3
+ examples/experimental:
+ dependencies:
+ next:
+ specifier: 15.3.1-canary.13
+ version: 15.3.1-canary.13(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ react:
+ specifier: 'catalog:'
+ version: 19.0.0
+ react-dom:
+ specifier: 'catalog:'
+ version: 19.0.0(react@19.0.0)
+ devDependencies:
+ '@types/node':
+ specifier: 'catalog:'
+ version: 20.17.6
+ '@types/react':
+ specifier: 'catalog:'
+ version: 19.0.0
+ '@types/react-dom':
+ specifier: 'catalog:'
+ version: 19.0.0
+ typescript:
+ specifier: 'catalog:'
+ version: 5.6.3
+
examples/pages-router:
dependencies:
'@example/shared':
@@ -1009,6 +1034,9 @@ packages:
'@emnapi/runtime@1.3.1':
resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
+ '@emnapi/runtime@1.4.1':
+ resolution: {integrity: sha512-LMshMVP0ZhACNjQNYXiU1iZJ6QCcv0lUdPDPugqGvCGXt5xtRVBPdtA0qU12pEXZzpWAhWlZYptfdAFq10DOVQ==}
+
'@envelop/core@3.0.6':
resolution: {integrity: sha512-06t1xCPXq6QFN7W1JUEf68aCwYN0OUDNAIoJe7bAqhaoa2vn7NCcuX1VHkJ/OWpmElUgCsRO6RiBbIru1in0Ig==}
@@ -1608,105 +1636,215 @@ packages:
cpu: [arm64]
os: [darwin]
+ '@img/sharp-darwin-arm64@0.34.1':
+ resolution: {integrity: sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [darwin]
+
'@img/sharp-darwin-x64@0.33.5':
resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
+ '@img/sharp-darwin-x64@0.34.1':
+ resolution: {integrity: sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [darwin]
+
'@img/sharp-libvips-darwin-arm64@1.0.4':
resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
cpu: [arm64]
os: [darwin]
+ '@img/sharp-libvips-darwin-arm64@1.1.0':
+ resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==}
+ cpu: [arm64]
+ os: [darwin]
+
'@img/sharp-libvips-darwin-x64@1.0.4':
resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
cpu: [x64]
os: [darwin]
+ '@img/sharp-libvips-darwin-x64@1.1.0':
+ resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==}
+ cpu: [x64]
+ os: [darwin]
+
'@img/sharp-libvips-linux-arm64@1.0.4':
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
cpu: [arm64]
os: [linux]
+ '@img/sharp-libvips-linux-arm64@1.1.0':
+ resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==}
+ cpu: [arm64]
+ os: [linux]
+
'@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
cpu: [arm]
os: [linux]
+ '@img/sharp-libvips-linux-arm@1.1.0':
+ resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==}
+ cpu: [arm]
+ os: [linux]
+
+ '@img/sharp-libvips-linux-ppc64@1.1.0':
+ resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==}
+ cpu: [ppc64]
+ os: [linux]
+
'@img/sharp-libvips-linux-s390x@1.0.4':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
cpu: [s390x]
os: [linux]
+ '@img/sharp-libvips-linux-s390x@1.1.0':
+ resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==}
+ cpu: [s390x]
+ os: [linux]
+
'@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
cpu: [x64]
os: [linux]
+ '@img/sharp-libvips-linux-x64@1.1.0':
+ resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==}
+ cpu: [x64]
+ os: [linux]
+
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
cpu: [arm64]
os: [linux]
+ '@img/sharp-libvips-linuxmusl-arm64@1.1.0':
+ resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==}
+ cpu: [arm64]
+ os: [linux]
+
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
cpu: [x64]
os: [linux]
+ '@img/sharp-libvips-linuxmusl-x64@1.1.0':
+ resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==}
+ cpu: [x64]
+ os: [linux]
+
'@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ '@img/sharp-linux-arm64@0.34.1':
+ resolution: {integrity: sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [linux]
+
'@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
+ '@img/sharp-linux-arm@0.34.1':
+ resolution: {integrity: sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm]
+ os: [linux]
+
'@img/sharp-linux-s390x@0.33.5':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
+ '@img/sharp-linux-s390x@0.34.1':
+ resolution: {integrity: sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [s390x]
+ os: [linux]
+
'@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ '@img/sharp-linux-x64@0.34.1':
+ resolution: {integrity: sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [linux]
+
'@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ '@img/sharp-linuxmusl-arm64@0.34.1':
+ resolution: {integrity: sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [arm64]
+ os: [linux]
+
'@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ '@img/sharp-linuxmusl-x64@0.34.1':
+ resolution: {integrity: sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [linux]
+
'@img/sharp-wasm32@0.33.5':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
+ '@img/sharp-wasm32@0.34.1':
+ resolution: {integrity: sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [wasm32]
+
'@img/sharp-win32-ia32@0.33.5':
resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
+ '@img/sharp-win32-ia32@0.34.1':
+ resolution: {integrity: sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [ia32]
+ os: [win32]
+
'@img/sharp-win32-x64@0.33.5':
resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
+ '@img/sharp-win32-x64@0.34.1':
+ resolution: {integrity: sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ cpu: [x64]
+ os: [win32]
+
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@@ -1768,54 +1906,105 @@ packages:
'@next/env@15.2.0':
resolution: {integrity: sha512-eMgJu1RBXxxqqnuRJQh5RozhskoNUDHBFybvi+Z+yK9qzKeG7dadhv/Vp1YooSZmCnegf7JxWuapV77necLZNA==}
+ '@next/env@15.3.1-canary.13':
+ resolution: {integrity: sha512-BgYTh0j+eFPpYUI8u8RUiJiNpyGXZVVpC3t+/JlbBvD2ZN68cIHiyR+5+vX9koc0JbxYn5nR65s0AgXzOKod1g==}
+
'@next/swc-darwin-arm64@15.2.0':
resolution: {integrity: sha512-rlp22GZwNJjFCyL7h5wz9vtpBVuCt3ZYjFWpEPBGzG712/uL1bbSkS675rVAUCRZ4hjoTJ26Q7IKhr5DfJrHDA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
+ '@next/swc-darwin-arm64@15.3.1-canary.13':
+ resolution: {integrity: sha512-UbNA7Ox/5k6Tev0UKJNCRp1Gd+b15tL/ATW7WGvmBe8zeszQEn/U9ItML4hj2KkAmEKj9PJdh0uytQgXeAoBcQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
'@next/swc-darwin-x64@15.2.0':
resolution: {integrity: sha512-DiU85EqSHogCz80+sgsx90/ecygfCSGl5P3b4XDRVZpgujBm5lp4ts7YaHru7eVTyZMjHInzKr+w0/7+qDrvMA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
+ '@next/swc-darwin-x64@15.3.1-canary.13':
+ resolution: {integrity: sha512-WBzNHBKbffGQbV4EBt5hwSJsmJblBEvHWydqZ2DeabwxOFpw4DZrEyRNsP3Vhiu40wSV/vpwjTbNtLoIs8Ocbg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
'@next/swc-linux-arm64-gnu@15.2.0':
resolution: {integrity: sha512-VnpoMaGukiNWVxeqKHwi8MN47yKGyki5q+7ql/7p/3ifuU2341i/gDwGK1rivk0pVYbdv5D8z63uu9yMw0QhpQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ '@next/swc-linux-arm64-gnu@15.3.1-canary.13':
+ resolution: {integrity: sha512-vDZGVhN2KVmxBfuK/xvFTPj3XpylpPKKzP8XFM2m0GI4NUqtLRIjKvd4OyrsuB1NRaIRfchl5iIGh23v2om+Pw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
'@next/swc-linux-arm64-musl@15.2.0':
resolution: {integrity: sha512-ka97/ssYE5nPH4Qs+8bd8RlYeNeUVBhcnsNUmFM6VWEob4jfN9FTr0NBhXVi1XEJpj3cMfgSRW+LdE3SUZbPrw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ '@next/swc-linux-arm64-musl@15.3.1-canary.13':
+ resolution: {integrity: sha512-dNcky0UM/6/M2xK0J4ecmb/wo1t4h0o7L5vP6dSjPZk2tCXZf98+++XqYZiBIv6DVvJck3oS85x7CcmCRZfRrw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
'@next/swc-linux-x64-gnu@15.2.0':
resolution: {integrity: sha512-zY1JduE4B3q0k2ZCE+DAF/1efjTXUsKP+VXRtrt/rJCTgDlUyyryx7aOgYXNc1d8gobys/Lof9P9ze8IyRDn7Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ '@next/swc-linux-x64-gnu@15.3.1-canary.13':
+ resolution: {integrity: sha512-yTvXBe0AbRwq3jj6QLsMej9dRrXTtlPzS78IKU3Pr+uqY4DwXqeuuvYjEN8VkmD/R260HaFzAecQIdxDN+k9Mw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
'@next/swc-linux-x64-musl@15.2.0':
resolution: {integrity: sha512-QqvLZpurBD46RhaVaVBepkVQzh8xtlUN00RlG4Iq1sBheNugamUNPuZEH1r9X1YGQo1KqAe1iiShF0acva3jHQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ '@next/swc-linux-x64-musl@15.3.1-canary.13':
+ resolution: {integrity: sha512-Qvw7CEqlfXLFjoAX2Q3jaM6RG/j3r4Mrp3/ZOo3OWS5MztjDBpxMst/y0EP1bgJCOyw/gxdzSArdQVVjLSKeeQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
'@next/swc-win32-arm64-msvc@15.2.0':
resolution: {integrity: sha512-ODZ0r9WMyylTHAN6pLtvUtQlGXBL9voljv6ujSlcsjOxhtXPI1Ag6AhZK0SE8hEpR1374WZZ5w33ChpJd5fsjw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
+ '@next/swc-win32-arm64-msvc@15.3.1-canary.13':
+ resolution: {integrity: sha512-0lzIvfUh5yMwEARGxscgdDf1wtkg4kkWQtAbjKvlcwYGDVkkunL6tsq8fQ6NmxTNxwFATxCIeUysuvBOeWi0LQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
'@next/swc-win32-x64-msvc@15.2.0':
resolution: {integrity: sha512-8+4Z3Z7xa13NdUuUAcpVNA6o76lNPniBd9Xbo02bwXQXnZgFvEopwY2at5+z7yHl47X9qbZpvwatZ2BRo3EdZw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
+ '@next/swc-win32-x64-msvc@15.3.1-canary.13':
+ resolution: {integrity: sha512-Z1dAHpyg8OdLm3v8SHhkuqnTqT5x71eBCMNsr3kntdzzvT29HbtVMovQpG6oLmxtVqWFyR0SSs4+ukbi17cXjw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
'@node-minify/core@8.0.6':
resolution: {integrity: sha512-/vxN46ieWDLU67CmgbArEvOb41zlYFOkOtr9QW9CnTrBLuTyGgkyNWC2y5+khvRw3Br58p2B5ZVSx/PxCTru6g==}
engines: {node: '>=16.0.0'}
@@ -4254,6 +4443,27 @@ packages:
sass:
optional: true
+ next@15.3.1-canary.13:
+ resolution: {integrity: sha512-LC8RQrWYsgD7639DRruitJ7x2HAGLFxAFXY6B6xwYQEkJNK3aeWxZyVYs8fIfXYscRqZtJXPc1EJy03xDuseyA==}
+ engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@opentelemetry/api': ^1.1.0
+ '@playwright/test': ^1.41.2
+ babel-plugin-react-compiler: '*'
+ react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ sass: ^1.3.0
+ peerDependenciesMeta:
+ '@opentelemetry/api':
+ optional: true
+ '@playwright/test':
+ optional: true
+ babel-plugin-react-compiler:
+ optional: true
+ sass:
+ optional: true
+
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
@@ -4771,6 +4981,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ semver@7.7.1:
+ resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
send@0.19.0:
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
engines: {node: '>= 0.8.0'}
@@ -4798,6 +5013,10 @@ packages:
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+ sharp@0.34.1:
+ resolution: {integrity: sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==}
+ engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+
shebang-command@1.2.0:
resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
engines: {node: '>=0.10.0'}
@@ -7502,6 +7721,11 @@ snapshots:
tslib: 2.8.0
optional: true
+ '@emnapi/runtime@1.4.1':
+ dependencies:
+ tslib: 2.8.0
+ optional: true
+
'@envelop/core@3.0.6':
dependencies:
'@envelop/types': 3.0.2
@@ -7862,76 +8086,154 @@ snapshots:
'@img/sharp-libvips-darwin-arm64': 1.0.4
optional: true
+ '@img/sharp-darwin-arm64@0.34.1':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-arm64': 1.1.0
+ optional: true
+
'@img/sharp-darwin-x64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.0.4
optional: true
+ '@img/sharp-darwin-x64@0.34.1':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-x64': 1.1.0
+ optional: true
+
'@img/sharp-libvips-darwin-arm64@1.0.4':
optional: true
+ '@img/sharp-libvips-darwin-arm64@1.1.0':
+ optional: true
+
'@img/sharp-libvips-darwin-x64@1.0.4':
optional: true
+ '@img/sharp-libvips-darwin-x64@1.1.0':
+ optional: true
+
'@img/sharp-libvips-linux-arm64@1.0.4':
optional: true
+ '@img/sharp-libvips-linux-arm64@1.1.0':
+ optional: true
+
'@img/sharp-libvips-linux-arm@1.0.5':
optional: true
+ '@img/sharp-libvips-linux-arm@1.1.0':
+ optional: true
+
+ '@img/sharp-libvips-linux-ppc64@1.1.0':
+ optional: true
+
'@img/sharp-libvips-linux-s390x@1.0.4':
optional: true
+ '@img/sharp-libvips-linux-s390x@1.1.0':
+ optional: true
+
'@img/sharp-libvips-linux-x64@1.0.4':
optional: true
+ '@img/sharp-libvips-linux-x64@1.1.0':
+ optional: true
+
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
optional: true
+ '@img/sharp-libvips-linuxmusl-arm64@1.1.0':
+ optional: true
+
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
optional: true
+ '@img/sharp-libvips-linuxmusl-x64@1.1.0':
+ optional: true
+
'@img/sharp-linux-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.0.4
optional: true
+ '@img/sharp-linux-arm64@0.34.1':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm64': 1.1.0
+ optional: true
+
'@img/sharp-linux-arm@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.0.5
optional: true
+ '@img/sharp-linux-arm@0.34.1':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm': 1.1.0
+ optional: true
+
'@img/sharp-linux-s390x@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.0.4
optional: true
+ '@img/sharp-linux-s390x@0.34.1':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-s390x': 1.1.0
+ optional: true
+
'@img/sharp-linux-x64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.0.4
optional: true
+ '@img/sharp-linux-x64@0.34.1':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-x64': 1.1.0
+ optional: true
+
'@img/sharp-linuxmusl-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
optional: true
+ '@img/sharp-linuxmusl-arm64@0.34.1':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-arm64': 1.1.0
+ optional: true
+
'@img/sharp-linuxmusl-x64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
optional: true
+ '@img/sharp-linuxmusl-x64@0.34.1':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-x64': 1.1.0
+ optional: true
+
'@img/sharp-wasm32@0.33.5':
dependencies:
'@emnapi/runtime': 1.3.1
optional: true
+ '@img/sharp-wasm32@0.34.1':
+ dependencies:
+ '@emnapi/runtime': 1.4.1
+ optional: true
+
'@img/sharp-win32-ia32@0.33.5':
optional: true
+ '@img/sharp-win32-ia32@0.34.1':
+ optional: true
+
'@img/sharp-win32-x64@0.33.5':
optional: true
+ '@img/sharp-win32-x64@0.34.1':
+ optional: true
+
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@@ -8014,30 +8316,56 @@ snapshots:
'@next/env@15.2.0': {}
+ '@next/env@15.3.1-canary.13': {}
+
'@next/swc-darwin-arm64@15.2.0':
optional: true
+ '@next/swc-darwin-arm64@15.3.1-canary.13':
+ optional: true
+
'@next/swc-darwin-x64@15.2.0':
optional: true
+ '@next/swc-darwin-x64@15.3.1-canary.13':
+ optional: true
+
'@next/swc-linux-arm64-gnu@15.2.0':
optional: true
+ '@next/swc-linux-arm64-gnu@15.3.1-canary.13':
+ optional: true
+
'@next/swc-linux-arm64-musl@15.2.0':
optional: true
+ '@next/swc-linux-arm64-musl@15.3.1-canary.13':
+ optional: true
+
'@next/swc-linux-x64-gnu@15.2.0':
optional: true
+ '@next/swc-linux-x64-gnu@15.3.1-canary.13':
+ optional: true
+
'@next/swc-linux-x64-musl@15.2.0':
optional: true
+ '@next/swc-linux-x64-musl@15.3.1-canary.13':
+ optional: true
+
'@next/swc-win32-arm64-msvc@15.2.0':
optional: true
+ '@next/swc-win32-arm64-msvc@15.3.1-canary.13':
+ optional: true
+
'@next/swc-win32-x64-msvc@15.2.0':
optional: true
+ '@next/swc-win32-x64-msvc@15.3.1-canary.13':
+ optional: true
+
'@node-minify/core@8.0.6':
dependencies:
'@node-minify/utils': 8.0.6
@@ -10954,6 +11282,32 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
+ next@15.3.1-canary.13(@playwright/test@1.49.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ dependencies:
+ '@next/env': 15.3.1-canary.13
+ '@swc/counter': 0.1.3
+ '@swc/helpers': 0.5.15
+ busboy: 1.6.0
+ caniuse-lite: 1.0.30001669
+ postcss: 8.4.31
+ react: 19.0.0
+ react-dom: 19.0.0(react@19.0.0)
+ styled-jsx: 5.1.6(react@19.0.0)
+ optionalDependencies:
+ '@next/swc-darwin-arm64': 15.3.1-canary.13
+ '@next/swc-darwin-x64': 15.3.1-canary.13
+ '@next/swc-linux-arm64-gnu': 15.3.1-canary.13
+ '@next/swc-linux-arm64-musl': 15.3.1-canary.13
+ '@next/swc-linux-x64-gnu': 15.3.1-canary.13
+ '@next/swc-linux-x64-musl': 15.3.1-canary.13
+ '@next/swc-win32-arm64-msvc': 15.3.1-canary.13
+ '@next/swc-win32-x64-msvc': 15.3.1-canary.13
+ '@playwright/test': 1.49.1
+ sharp: 0.34.1
+ transitivePeerDependencies:
+ - '@babel/core'
+ - babel-plugin-macros
+
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
@@ -11444,6 +11798,9 @@ snapshots:
semver@7.6.3: {}
+ semver@7.7.1:
+ optional: true
+
send@0.19.0:
dependencies:
debug: 2.6.9
@@ -11535,6 +11892,34 @@ snapshots:
'@img/sharp-win32-x64': 0.33.5
optional: true
+ sharp@0.34.1:
+ dependencies:
+ color: 4.2.3
+ detect-libc: 2.0.3
+ semver: 7.7.1
+ optionalDependencies:
+ '@img/sharp-darwin-arm64': 0.34.1
+ '@img/sharp-darwin-x64': 0.34.1
+ '@img/sharp-libvips-darwin-arm64': 1.1.0
+ '@img/sharp-libvips-darwin-x64': 1.1.0
+ '@img/sharp-libvips-linux-arm': 1.1.0
+ '@img/sharp-libvips-linux-arm64': 1.1.0
+ '@img/sharp-libvips-linux-ppc64': 1.1.0
+ '@img/sharp-libvips-linux-s390x': 1.1.0
+ '@img/sharp-libvips-linux-x64': 1.1.0
+ '@img/sharp-libvips-linuxmusl-arm64': 1.1.0
+ '@img/sharp-libvips-linuxmusl-x64': 1.1.0
+ '@img/sharp-linux-arm': 0.34.1
+ '@img/sharp-linux-arm64': 0.34.1
+ '@img/sharp-linux-s390x': 0.34.1
+ '@img/sharp-linux-x64': 0.34.1
+ '@img/sharp-linuxmusl-arm64': 0.34.1
+ '@img/sharp-linuxmusl-x64': 0.34.1
+ '@img/sharp-wasm32': 0.34.1
+ '@img/sharp-win32-ia32': 0.34.1
+ '@img/sharp-win32-x64': 0.34.1
+ optional: true
+
shebang-command@1.2.0:
dependencies:
shebang-regex: 1.0.0