diff --git a/examples/common/apps.ts b/examples/common/apps.ts
index 919b45e5..9dcedaf1 100644
--- a/examples/common/apps.ts
+++ b/examples/common/apps.ts
@@ -11,6 +11,7 @@ const apps = [
"app-pages-router",
"app-router",
"pages-router",
+ "experimental",
// overrides
"d1-tag-next",
"memory-queue",
diff --git a/examples/e2e/experimental/.gitignore b/examples/e2e/experimental/.gitignore
new file mode 100644
index 00000000..5ef6a520
--- /dev/null
+++ b/examples/e2e/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/e2e/experimental/README.md b/examples/e2e/experimental/README.md
new file mode 100644
index 00000000..9fbf1369
--- /dev/null
+++ b/examples/e2e/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.
diff --git a/examples/e2e/experimental/e2e/nodeMiddleware.test.ts b/examples/e2e/experimental/e2e/nodeMiddleware.test.ts
new file mode 100644
index 00000000..a3528709
--- /dev/null
+++ b/examples/e2e/experimental/e2e/nodeMiddleware.test.ts
@@ -0,0 +1,32 @@
+import { expect, test } from "@playwright/test";
+
+// See https://github.com/opennextjs/opennextjs-cloudflare/issues/617
+test.describe("Node Middleware", () => {
+ test.skip("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.skip("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.skip("Node middleware should redirect", async ({ page }) => {
+ await page.goto("/redirect");
+ await page.waitForURL("/");
+ const el = page.getByText("Incremental PPR");
+ await expect(el).toBeVisible();
+ });
+
+ test.skip("Node middleware should rewrite", async ({ page }) => {
+ await page.goto("/rewrite");
+ const el = page.getByText("Incremental PPR");
+ await expect(el).toBeVisible();
+ });
+});
diff --git a/examples/e2e/experimental/e2e/playwright.config.ts b/examples/e2e/experimental/e2e/playwright.config.ts
new file mode 100644
index 00000000..23ffcbba
--- /dev/null
+++ b/examples/e2e/experimental/e2e/playwright.config.ts
@@ -0,0 +1,4 @@
+import { configurePlaywright } from "../../../common/config-e2e";
+
+// We need to disable parallel execution for the experimental app, otherwise it breaks the SSR test
+export default configurePlaywright("experimental", { parallel: false });
diff --git a/examples/e2e/experimental/e2e/ppr.test.ts b/examples/e2e/experimental/e2e/ppr.test.ts
new file mode 100644
index 00000000..a9e94694
--- /dev/null
+++ b/examples/e2e/experimental/e2e/ppr.test.ts
@@ -0,0 +1,24 @@
+import { expect, test } from "@playwright/test";
+
+test.describe("PPR", () => {
+ 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/examples/e2e/experimental/e2e/use-cache.test.ts b/examples/e2e/experimental/e2e/use-cache.test.ts
new file mode 100644
index 00000000..bb72ffc3
--- /dev/null
+++ b/examples/e2e/experimental/e2e/use-cache.test.ts
@@ -0,0 +1,85 @@
+import { expect, test } from "@playwright/test";
+
+test.describe("Composable Cache", () => {
+ 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/examples/e2e/experimental/next.config.ts b/examples/e2e/experimental/next.config.ts
new file mode 100644
index 00000000..3440439a
--- /dev/null
+++ b/examples/e2e/experimental/next.config.ts
@@ -0,0 +1,23 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+ /* config options here */
+ cleanDistDir: true,
+ output: "standalone",
+ eslint: {
+ ignoreDuringBuilds: true,
+ },
+ typescript: {
+ // Ignore type errors during build for now, we'll need to figure this out later
+ ignoreBuildErrors: true,
+ },
+ experimental: {
+ ppr: "incremental",
+ // Node middleware is not supported yet in cloudflare
+ // See https://github.com/opennextjs/opennextjs-cloudflare/issues/617
+ // nodeMiddleware: true,
+ dynamicIO: true,
+ },
+};
+
+export default nextConfig;
diff --git a/examples/e2e/experimental/open-next.config.ts b/examples/e2e/experimental/open-next.config.ts
new file mode 100644
index 00000000..b202bbb1
--- /dev/null
+++ b/examples/e2e/experimental/open-next.config.ts
@@ -0,0 +1,17 @@
+import { defineCloudflareConfig } from "@opennextjs/cloudflare";
+import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
+import shardedTagCache from "@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache";
+import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";
+
+export default defineCloudflareConfig({
+ incrementalCache: r2IncrementalCache,
+ // With such a configuration, we could have up to 12 * (8 + 2) = 120 Durable Objects instances
+ tagCache: shardedTagCache({
+ baseShardSize: 12,
+ shardReplication: {
+ numberOfSoftReplicas: 8,
+ numberOfHardReplicas: 2,
+ },
+ }),
+ queue: doQueue,
+});
diff --git a/examples/e2e/experimental/package.json b/examples/e2e/experimental/package.json
new file mode 100644
index 00000000..53e1f420
--- /dev/null
+++ b/examples/e2e/experimental/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "experimental",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "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",
+ "build:worker": "pnpm opennextjs-cloudflare build",
+ "preview:worker": "pnpm opennextjs-cloudflare preview",
+ "preview": "pnpm build:worker && pnpm preview:worker",
+ "e2e": "playwright test -c e2e/playwright.config.ts"
+ },
+ "dependencies": {
+ "@opennextjs/cloudflare": "workspace:*",
+ "next": "15.4.0-canary.14",
+ "react": "catalog:e2e",
+ "react-dom": "catalog:e2e"
+ },
+ "devDependencies": {
+ "@playwright/test": "catalog:",
+ "@types/node": "catalog:e2e",
+ "@types/react": "catalog:e2e",
+ "@types/react-dom": "catalog:e2e",
+ "typescript": "catalog:default",
+ "wrangler": "catalog:"
+ }
+}
diff --git a/examples/e2e/experimental/src/app/api/revalidate/route.ts b/examples/e2e/experimental/src/app/api/revalidate/route.ts
new file mode 100644
index 00000000..da6b1e02
--- /dev/null
+++ b/examples/e2e/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/e2e/experimental/src/app/favicon.ico b/examples/e2e/experimental/src/app/favicon.ico
new file mode 100644
index 00000000..718d6fea
Binary files /dev/null and b/examples/e2e/experimental/src/app/favicon.ico differ
diff --git a/examples/e2e/experimental/src/app/globals.css b/examples/e2e/experimental/src/app/globals.css
new file mode 100644
index 00000000..e3734be1
--- /dev/null
+++ b/examples/e2e/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/e2e/experimental/src/app/layout.tsx b/examples/e2e/experimental/src/app/layout.tsx
new file mode 100644
index 00000000..ec006ee8
--- /dev/null
+++ b/examples/e2e/experimental/src/app/layout.tsx
@@ -0,0 +1,31 @@
+import "./globals.css";
+import type { Metadata } from "next";
+
+import { Geist, Geist_Mono } from "next/font/google";
+
+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/e2e/experimental/src/app/page.module.css b/examples/e2e/experimental/src/app/page.module.css
new file mode 100644
index 00000000..a11c8f31
--- /dev/null
+++ b/examples/e2e/experimental/src/app/page.module.css
@@ -0,0 +1,168 @@
+.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/e2e/experimental/src/app/page.tsx b/examples/e2e/experimental/src/app/page.tsx
new file mode 100644
index 00000000..f14e5dad
--- /dev/null
+++ b/examples/e2e/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/e2e/experimental/src/app/ppr/page.tsx b/examples/e2e/experimental/src/app/ppr/page.tsx
new file mode 100644
index 00000000..11d017a7
--- /dev/null
+++ b/examples/e2e/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/e2e/experimental/src/app/use-cache/isr/page.tsx b/examples/e2e/experimental/src/app/use-cache/isr/page.tsx
new file mode 100644
index 00000000..dd02f8a6
--- /dev/null
+++ b/examples/e2e/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/e2e/experimental/src/app/use-cache/layout.tsx b/examples/e2e/experimental/src/app/use-cache/layout.tsx
new file mode 100644
index 00000000..26b3ea7c
--- /dev/null
+++ b/examples/e2e/experimental/src/app/use-cache/layout.tsx
@@ -0,0 +1,14 @@
+import { Suspense } from "react";
+
+export default function Layout({
+ children,
+}: Readonly<{
+ // For some reason using ReactNode here causes a type error
+ children: any;
+}>) {
+ return (
+
+ Loading...}>{children}
+
+ );
+}
diff --git a/examples/e2e/experimental/src/app/use-cache/ssr/page.tsx b/examples/e2e/experimental/src/app/use-cache/ssr/page.tsx
new file mode 100644
index 00000000..41a413d1
--- /dev/null
+++ b/examples/e2e/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/e2e/experimental/src/components/cached.tsx b/examples/e2e/experimental/src/components/cached.tsx
new file mode 100644
index 00000000..7abaa010
--- /dev/null
+++ b/examples/e2e/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/e2e/experimental/src/components/dynamic.tsx b/examples/e2e/experimental/src/components/dynamic.tsx
new file mode 100644
index 00000000..bca758e4
--- /dev/null
+++ b/examples/e2e/experimental/src/components/dynamic.tsx
@@ -0,0 +1,15 @@
+import { setTimeout } from "node:timers/promises";
+import { headers } from "next/headers";
+
+export async function DynamicComponent() {
+ const _headers = await headers();
+ // Simulate a delay to mimic server-side calls
+ await setTimeout(1000, new Date().toString());
+ return (
+
+
Dynamic Component
+
This component should be SSR
+
{_headers.get("referer")}
+
+ );
+}
diff --git a/examples/e2e/experimental/src/components/static.tsx b/examples/e2e/experimental/src/components/static.tsx
new file mode 100644
index 00000000..8af73f9f
--- /dev/null
+++ b/examples/e2e/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/e2e/experimental/src/middleware.ts b/examples/e2e/experimental/src/middleware.ts
new file mode 100644
index 00000000..ba626941
--- /dev/null
+++ b/examples/e2e/experimental/src/middleware.ts
@@ -0,0 +1,30 @@
+// Node middleware is not supported yet in cloudflare
+// See https://github.com/opennextjs/opennextjs-cloudflare/issues/617
+
+// 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/e2e/experimental/tsconfig.json b/examples/e2e/experimental/tsconfig.json
new file mode 100644
index 00000000..c1334095
--- /dev/null
+++ b/examples/e2e/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/e2e/experimental/wrangler.jsonc b/examples/e2e/experimental/wrangler.jsonc
new file mode 100644
index 00000000..9b0043a4
--- /dev/null
+++ b/examples/e2e/experimental/wrangler.jsonc
@@ -0,0 +1,44 @@
+{
+ "$schema": "node_modules/wrangler/config-schema.json",
+ "main": ".open-next/worker.js",
+ "name": "experimental",
+ "compatibility_date": "2024-12-30",
+ "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
+ "assets": {
+ "directory": ".open-next/assets",
+ "binding": "ASSETS"
+ },
+ "vars": {
+ "NEXT_PRIVATE_DEBUG_CACHE": true
+ },
+ "durable_objects": {
+ "bindings": [
+ {
+ "name": "NEXT_CACHE_DO_QUEUE",
+ "class_name": "DOQueueHandler"
+ },
+ {
+ "name": "NEXT_TAG_CACHE_DO_SHARDED",
+ "class_name": "DOShardedTagCache"
+ }
+ ]
+ },
+ "migrations": [
+ {
+ "tag": "v1",
+ "new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
+ }
+ ],
+ "r2_buckets": [
+ {
+ "binding": "NEXT_INC_CACHE_R2_BUCKET",
+ "bucket_name": ""
+ }
+ ],
+ "services": [
+ {
+ "binding": "WORKER_SELF_REFERENCE",
+ "service": "experimental"
+ }
+ ]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 76f1a5f5..555d9ba8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -89,11 +89,11 @@ catalogs:
specifier: 20.17.6
version: 20.17.6
'@types/react':
- specifier: 19.0.8
- version: 19.0.8
+ specifier: 19.0.0
+ version: 19.0.0
'@types/react-dom':
- specifier: 19.0.3
- version: 19.0.3
+ specifier: 19.0.0
+ version: 19.0.0
autoprefixer:
specifier: 10.4.15
version: 10.4.15
@@ -104,10 +104,10 @@ catalogs:
specifier: 8.4.27
version: 8.4.27
react:
- specifier: ^19.0.0
+ specifier: 19.0.0
version: 19.0.0
react-dom:
- specifier: ^19.0.0
+ specifier: 19.0.0
version: 19.0.0
tailwindcss:
specifier: 3.3.3
@@ -448,10 +448,10 @@ importers:
version: 20.17.6
'@types/react':
specifier: catalog:e2e
- version: 19.0.8
+ version: 19.0.0
'@types/react-dom':
specifier: catalog:e2e
- version: 19.0.3(@types/react@19.0.8)
+ version: 19.0.0
autoprefixer:
specifier: catalog:e2e
version: 10.4.15(postcss@8.4.27)
@@ -494,10 +494,10 @@ importers:
version: 20.17.6
'@types/react':
specifier: catalog:e2e
- version: 19.0.8
+ version: 19.0.0
'@types/react-dom':
specifier: catalog:e2e
- version: 19.0.3(@types/react@19.0.8)
+ version: 19.0.0
autoprefixer:
specifier: catalog:e2e
version: 10.4.15(postcss@8.4.27)
@@ -514,6 +514,40 @@ importers:
specifier: 'catalog:'
version: 4.7.0(@cloudflare/workers-types@4.20250321.0)
+ examples/e2e/experimental:
+ dependencies:
+ '@opennextjs/cloudflare':
+ specifier: workspace:*
+ version: link:../../../packages/cloudflare
+ next:
+ specifier: 15.4.0-canary.14
+ version: 15.4.0-canary.14(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ react:
+ specifier: catalog:e2e
+ version: 19.0.0
+ react-dom:
+ specifier: catalog:e2e
+ version: 19.0.0(react@19.0.0)
+ devDependencies:
+ '@playwright/test':
+ specifier: 'catalog:'
+ version: 1.51.1
+ '@types/node':
+ specifier: catalog:e2e
+ version: 20.17.6
+ '@types/react':
+ specifier: catalog:e2e
+ version: 19.0.0
+ '@types/react-dom':
+ specifier: catalog:e2e
+ version: 19.0.0
+ typescript:
+ specifier: catalog:default
+ version: 5.7.3
+ wrangler:
+ specifier: 'catalog:'
+ version: 4.7.0(@cloudflare/workers-types@4.20250321.0)
+
examples/e2e/pages-router:
dependencies:
'@example/shared':
@@ -540,10 +574,10 @@ importers:
version: 20.17.6
'@types/react':
specifier: catalog:e2e
- version: 19.0.8
+ version: 19.0.0
'@types/react-dom':
specifier: catalog:e2e
- version: 19.0.3(@types/react@19.0.8)
+ version: 19.0.0
autoprefixer:
specifier: catalog:e2e
version: 10.4.15(postcss@8.4.27)
@@ -574,10 +608,10 @@ importers:
devDependencies:
'@types/react':
specifier: catalog:e2e
- version: 19.0.8
+ version: 19.0.0
'@types/react-dom':
specifier: catalog:e2e
- version: 19.0.3(@types/react@19.0.8)
+ version: 19.0.0
examples/middleware:
dependencies:
@@ -703,10 +737,10 @@ importers:
version: 22.2.0
'@types/react':
specifier: catalog:e2e
- version: 19.0.8
+ version: 19.0.0
'@types/react-dom':
specifier: catalog:e2e
- version: 19.0.3(@types/react@19.0.8)
+ version: 19.0.0
typescript:
specifier: 'catalog:'
version: 5.7.3
@@ -737,10 +771,10 @@ importers:
version: 22.2.0
'@types/react':
specifier: catalog:e2e
- version: 19.0.8
+ version: 19.0.0
'@types/react-dom':
specifier: catalog:e2e
- version: 19.0.3(@types/react@19.0.8)
+ version: 19.0.0
typescript:
specifier: 'catalog:'
version: 5.7.3
@@ -771,10 +805,10 @@ importers:
version: 22.2.0
'@types/react':
specifier: catalog:e2e
- version: 19.0.8
+ version: 19.0.0
'@types/react-dom':
specifier: catalog:e2e
- version: 19.0.3(@types/react@19.0.8)
+ version: 19.0.0
typescript:
specifier: 'catalog:'
version: 5.7.3
@@ -805,10 +839,10 @@ importers:
version: 22.2.0
'@types/react':
specifier: catalog:e2e
- version: 19.0.8
+ version: 19.0.0
'@types/react-dom':
specifier: catalog:e2e
- version: 19.0.3(@types/react@19.0.8)
+ version: 19.0.0
typescript:
specifier: 'catalog:'
version: 5.7.3
@@ -3250,6 +3284,9 @@ packages:
'@next/env@15.3.0':
resolution: {integrity: sha512-6mDmHX24nWlHOlbwUiAOmMyY7KELimmi+ed8qWcJYjqXeC+G6JzPZ3QosOAfjNwgMIzwhXBiRiCgdh8axTTdTA==}
+ '@next/env@15.4.0-canary.14':
+ resolution: {integrity: sha512-ynXM3n0AEcB1mwoOLgar27s/WoFyX0C8kpbfpc6bylq2rfS+q+KNla1WAVX3QdHyV82KyrqdMQAFOIyTZg4K9A==}
+
'@next/eslint-plugin-next@14.2.14':
resolution: {integrity: sha512-kV+OsZ56xhj0rnTn6HegyTGkoa16Mxjrpk7pjWumyB2P8JVQb8S9qtkjy/ye0GnTr4JWtWG4x/2qN40lKZ3iVQ==}
@@ -3304,6 +3341,12 @@ packages:
cpu: [arm64]
os: [darwin]
+ '@next/swc-darwin-arm64@15.4.0-canary.14':
+ resolution: {integrity: sha512-p62YaNcigaJlZ6IIubZPT+S4N0CXXkjqdIbC2Otr6LLxWsvdkHRgWaPLHauCxWw0zS7jczKY1w4ZfyX9l26sIQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
'@next/swc-darwin-x64@14.2.24':
resolution: {integrity: sha512-lXR2WQqUtu69l5JMdTwSvQUkdqAhEWOqJEYUQ21QczQsAlNOW2kWZCucA6b3EXmPbcvmHB1kSZDua/713d52xg==}
engines: {node: '>= 10'}
@@ -3346,6 +3389,12 @@ packages:
cpu: [x64]
os: [darwin]
+ '@next/swc-darwin-x64@15.4.0-canary.14':
+ resolution: {integrity: sha512-PQ4z01gYCeYzP4NpFKBvg0slDu/CZ+vrpin6+O5XfzGOOdBCUqlJWK78ZTlfs8eTjVWnvVEi2FsTnbW5BZ0yiA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
'@next/swc-linux-arm64-gnu@14.2.24':
resolution: {integrity: sha512-nxvJgWOpSNmzidYvvGDfXwxkijb6hL9+cjZx1PVG6urr2h2jUqBALkKjT7kpfurRWicK6hFOvarmaWsINT1hnA==}
engines: {node: '>= 10'}
@@ -3388,6 +3437,12 @@ packages:
cpu: [arm64]
os: [linux]
+ '@next/swc-linux-arm64-gnu@15.4.0-canary.14':
+ resolution: {integrity: sha512-u/eeGK9okYiJ24aLcrq2jOCyOnjhzOM/MkcOOMkzE4/Rp7EKIepnGUhnIcLeLmcQw4RCDAjh3QZBqt5rQEm4fA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
'@next/swc-linux-arm64-musl@14.2.24':
resolution: {integrity: sha512-PaBgOPhqa4Abxa3y/P92F3kklNPsiFjcjldQGT7kFmiY5nuFn8ClBEoX8GIpqU1ODP2y8P6hio6vTomx2Vy0UQ==}
engines: {node: '>= 10'}
@@ -3430,6 +3485,12 @@ packages:
cpu: [arm64]
os: [linux]
+ '@next/swc-linux-arm64-musl@15.4.0-canary.14':
+ resolution: {integrity: sha512-6eODbSA592cYMYtBU9Vm2D8ApXn6dBh/cN7GQlsTiDBIlCId9Z8DlkGCDj/9thr0JEluUlkt379+B19BGxsCEg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+
'@next/swc-linux-x64-gnu@14.2.24':
resolution: {integrity: sha512-vEbyadiRI7GOr94hd2AB15LFVgcJZQWu7Cdi9cWjCMeCiUsHWA0U5BkGPuoYRnTxTn0HacuMb9NeAmStfBCLoQ==}
engines: {node: '>= 10'}
@@ -3472,6 +3533,12 @@ packages:
cpu: [x64]
os: [linux]
+ '@next/swc-linux-x64-gnu@15.4.0-canary.14':
+ resolution: {integrity: sha512-FwOtQDbMLJmGPCg8p1ZilCBjfjBZGBRwXnWmxLmpO4lcWTWMFTCfAxkqCUi62zXBZUJztqT8TgXQ9VBk4BKukQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
'@next/swc-linux-x64-musl@14.2.24':
resolution: {integrity: sha512-df0FC9ptaYsd8nQCINCzFtDWtko8PNRTAU0/+d7hy47E0oC17tI54U/0NdGk7l/76jz1J377dvRjmt6IUdkpzQ==}
engines: {node: '>= 10'}
@@ -3514,6 +3581,12 @@ packages:
cpu: [x64]
os: [linux]
+ '@next/swc-linux-x64-musl@15.4.0-canary.14':
+ resolution: {integrity: sha512-0k8lkaryoYsB4wksRm/5SlWWtJjuq6vOzQ/zqKRlNdpNvsvzZ61sEaCLZn1zdcFcUVH6wSzK/GMclcpn2w0VAg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+
'@next/swc-win32-arm64-msvc@14.2.24':
resolution: {integrity: sha512-ZEntbLjeYAJ286eAqbxpZHhDFYpYjArotQ+/TW9j7UROh0DUmX7wYDGtsTPpfCV8V+UoqHBPU7q9D4nDNH014Q==}
engines: {node: '>= 10'}
@@ -3556,6 +3629,12 @@ packages:
cpu: [arm64]
os: [win32]
+ '@next/swc-win32-arm64-msvc@15.4.0-canary.14':
+ resolution: {integrity: sha512-Kih/2CNMpegubEJT8xoigF+hMihetcFEwWXINfPoO534GQax4o1HU56aai6YgFYCvcrb9fAmW2vVagCQx3GS2g==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
'@next/swc-win32-ia32-msvc@14.2.24':
resolution: {integrity: sha512-9KuS+XUXM3T6v7leeWU0erpJ6NsFIwiTFD5nzNg8J5uo/DMIPvCp3L1Ao5HjbHX0gkWPB1VrKoo/Il4F0cGK2Q==}
engines: {node: '>= 10'}
@@ -3610,6 +3689,12 @@ packages:
cpu: [x64]
os: [win32]
+ '@next/swc-win32-x64-msvc@15.4.0-canary.14':
+ resolution: {integrity: sha512-iOTIfyhrUDDIFH0BA0ZAek8XEK2Wgtbg1QOiqzTU7QPasn28lK/b2bHI+stFrGfz6u1NZw9V/B+/D+o9lzSWKQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
'@noble/ciphers@1.1.3':
resolution: {integrity: sha512-Ygv6WnWJHLLiW4fnNDC1z+i13bud+enXOFRBlpxI+NJliPWx5wdR+oWlTjLuBPTqjUjtHXtjkU6w3kuuH6upZA==}
engines: {node: ^14.21.3 || >=16}
@@ -7527,6 +7612,27 @@ packages:
sass:
optional: true
+ next@15.4.0-canary.14:
+ resolution: {integrity: sha512-4/WNK9Uw/Js1QruZhZfUJWTLrXtL7cvVWLDi1PoCcGdVY91b/1U5jNDOt/Vebr/aJ6Xr5aF+PNHUTtcvBFPInw==}
+ 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
+
no-case@3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
@@ -12210,7 +12316,7 @@ snapshots:
https-proxy-agent: 7.0.6
node-fetch: 2.7.0
nopt: 8.1.0
- semver: 7.6.3
+ semver: 7.7.1
tar: 7.4.3
transitivePeerDependencies:
- encoding
@@ -12232,6 +12338,8 @@ snapshots:
'@next/env@15.3.0': {}
+ '@next/env@15.4.0-canary.14': {}
+
'@next/eslint-plugin-next@14.2.14':
dependencies:
glob: 10.3.10
@@ -12269,6 +12377,9 @@ snapshots:
'@next/swc-darwin-arm64@15.3.0':
optional: true
+ '@next/swc-darwin-arm64@15.4.0-canary.14':
+ optional: true
+
'@next/swc-darwin-x64@14.2.24':
optional: true
@@ -12290,6 +12401,9 @@ snapshots:
'@next/swc-darwin-x64@15.3.0':
optional: true
+ '@next/swc-darwin-x64@15.4.0-canary.14':
+ optional: true
+
'@next/swc-linux-arm64-gnu@14.2.24':
optional: true
@@ -12311,6 +12425,9 @@ snapshots:
'@next/swc-linux-arm64-gnu@15.3.0':
optional: true
+ '@next/swc-linux-arm64-gnu@15.4.0-canary.14':
+ optional: true
+
'@next/swc-linux-arm64-musl@14.2.24':
optional: true
@@ -12332,6 +12449,9 @@ snapshots:
'@next/swc-linux-arm64-musl@15.3.0':
optional: true
+ '@next/swc-linux-arm64-musl@15.4.0-canary.14':
+ optional: true
+
'@next/swc-linux-x64-gnu@14.2.24':
optional: true
@@ -12353,6 +12473,9 @@ snapshots:
'@next/swc-linux-x64-gnu@15.3.0':
optional: true
+ '@next/swc-linux-x64-gnu@15.4.0-canary.14':
+ optional: true
+
'@next/swc-linux-x64-musl@14.2.24':
optional: true
@@ -12374,6 +12497,9 @@ snapshots:
'@next/swc-linux-x64-musl@15.3.0':
optional: true
+ '@next/swc-linux-x64-musl@15.4.0-canary.14':
+ optional: true
+
'@next/swc-win32-arm64-msvc@14.2.24':
optional: true
@@ -12395,6 +12521,9 @@ snapshots:
'@next/swc-win32-arm64-msvc@15.3.0':
optional: true
+ '@next/swc-win32-arm64-msvc@15.4.0-canary.14':
+ optional: true
+
'@next/swc-win32-ia32-msvc@14.2.24':
optional: true
@@ -12422,6 +12551,9 @@ snapshots:
'@next/swc-win32-x64-msvc@15.3.0':
optional: true
+ '@next/swc-win32-x64-msvc@15.4.0-canary.14':
+ optional: true
+
'@noble/ciphers@1.1.3': {}
'@noble/curves@1.7.0':
@@ -17872,6 +18004,32 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
+ next@15.4.0-canary.14(@opentelemetry/api@1.9.0)(@playwright/test@1.51.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+ dependencies:
+ '@next/env': 15.4.0-canary.14
+ '@swc/counter': 0.1.3
+ '@swc/helpers': 0.5.15
+ caniuse-lite: 1.0.30001664
+ 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.4.0-canary.14
+ '@next/swc-darwin-x64': 15.4.0-canary.14
+ '@next/swc-linux-arm64-gnu': 15.4.0-canary.14
+ '@next/swc-linux-arm64-musl': 15.4.0-canary.14
+ '@next/swc-linux-x64-gnu': 15.4.0-canary.14
+ '@next/swc-linux-x64-musl': 15.4.0-canary.14
+ '@next/swc-win32-arm64-msvc': 15.4.0-canary.14
+ '@next/swc-win32-x64-msvc': 15.4.0-canary.14
+ '@opentelemetry/api': 1.9.0
+ '@playwright/test': 1.51.1
+ sharp: 0.34.1
+ transitivePeerDependencies:
+ - '@babel/core'
+ - babel-plugin-macros
+
no-case@3.0.4:
dependencies:
lower-case: 2.0.2
@@ -18730,8 +18888,7 @@ snapshots:
semver@7.6.3: {}
- semver@7.7.1:
- optional: true
+ semver@7.7.1: {}
send@1.2.0:
dependencies:
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 20a09150..20e6c431 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -40,11 +40,11 @@ catalog:
catalogs:
e2e:
"@types/node": 20.17.6
- "@types/react-dom": 19.0.3
- "@types/react": 19.0.8
+ "@types/react-dom": 19.0.0
+ "@types/react": 19.0.0
autoprefixer: 10.4.15
next: ~15.3.0
postcss: 8.4.27
- react-dom: ^19.0.0
- react: ^19.0.0
+ react-dom: 19.0.0
+ react: 19.0.0
tailwindcss: 3.3.3