diff --git a/examples/e2e/app-pages-router/.env b/examples/e2e/app-pages-router/.env
new file mode 100644
index 00000000..5d8e1786
--- /dev/null
+++ b/examples/e2e/app-pages-router/.env
@@ -0,0 +1 @@
+SOME_ENV_VAR=foo
\ No newline at end of file
diff --git a/examples/e2e/app-pages-router/.gitignore b/examples/e2e/app-pages-router/.gitignore
new file mode 100644
index 00000000..28379183
--- /dev/null
+++ b/examples/e2e/app-pages-router/.gitignore
@@ -0,0 +1,42 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+.open-next
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+# playwright
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
\ No newline at end of file
diff --git a/examples/e2e/app-pages-router/CHANGELOG.md b/examples/e2e/app-pages-router/CHANGELOG.md
new file mode 100644
index 00000000..bb60e202
--- /dev/null
+++ b/examples/e2e/app-pages-router/CHANGELOG.md
@@ -0,0 +1,43 @@
+# app-pages-router
+
+## 0.1.6
+
+### Patch Changes
+
+- Updated dependencies [[`7eda030388880d8ad25d3f4692e24bac31b7ec4f`](https://github.com/opennextjs/opennextjs-aws/commit/7eda030388880d8ad25d3f4692e24bac31b7ec4f), [`e5678b39e0f3c21d3e30d08a89f5cb0acdd3d050`](https://github.com/opennextjs/opennextjs-aws/commit/e5678b39e0f3c21d3e30d08a89f5cb0acdd3d050), [`1981a47dd3dbc77066d2bf5cad5d5d406fecb010`](https://github.com/opennextjs/opennextjs-aws/commit/1981a47dd3dbc77066d2bf5cad5d5d406fecb010), [`b4ad0f0e0f6069ca87f3b72c23d655cedebc86e5`](https://github.com/opennextjs/opennextjs-aws/commit/b4ad0f0e0f6069ca87f3b72c23d655cedebc86e5)]:
+ - @opennextjs/aws@3.4.1
+
+## 0.1.5
+
+### Patch Changes
+
+- Updated dependencies [[`e8f6dc8c7a421e316f5fbed03dcb82bb860c5249`](https://github.com/opennextjs/opennextjs-aws/commit/e8f6dc8c7a421e316f5fbed03dcb82bb860c5249), [`00ce837cb98e5902316f26163c9fb927058f956c`](https://github.com/opennextjs/opennextjs-aws/commit/00ce837cb98e5902316f26163c9fb927058f956c), [`d1cea5601943afaa197d56f931593234f351c441`](https://github.com/opennextjs/opennextjs-aws/commit/d1cea5601943afaa197d56f931593234f351c441), [`6884444cb929ab60c074c918954d24100f4e9668`](https://github.com/opennextjs/opennextjs-aws/commit/6884444cb929ab60c074c918954d24100f4e9668), [`86916bfd9246a63f321352bb11346eeb0ca3f6da`](https://github.com/opennextjs/opennextjs-aws/commit/86916bfd9246a63f321352bb11346eeb0ca3f6da), [`eaa9ef8daf2fc454139c77ce0e100cb48da15561`](https://github.com/opennextjs/opennextjs-aws/commit/eaa9ef8daf2fc454139c77ce0e100cb48da15561), [`ae7fb9c5d24ecf3eeb99682aa34bcbe0adb45675`](https://github.com/opennextjs/opennextjs-aws/commit/ae7fb9c5d24ecf3eeb99682aa34bcbe0adb45675), [`e708ec4d9f4c87d3249a01382482347d295ed28a`](https://github.com/opennextjs/opennextjs-aws/commit/e708ec4d9f4c87d3249a01382482347d295ed28a)]:
+ - @opennextjs/aws@3.4.0
+
+## 0.1.4
+
+### Patch Changes
+
+- Updated dependencies [[`9595714ac23e5f131b879d04d5cfb2a5d11bdbdd`](https://github.com/opennextjs/opennextjs-aws/commit/9595714ac23e5f131b879d04d5cfb2a5d11bdbdd), [`4e88b47935523de1d15da067b56105bd6be91e47`](https://github.com/opennextjs/opennextjs-aws/commit/4e88b47935523de1d15da067b56105bd6be91e47), [`7140ca56e1e88d7a7cae327eceb3ef8c2fde2a1e`](https://github.com/opennextjs/opennextjs-aws/commit/7140ca56e1e88d7a7cae327eceb3ef8c2fde2a1e)]:
+ - @opennextjs/aws@3.3.1
+
+## 0.1.3
+
+### Patch Changes
+
+- Updated dependencies [[`4d328e3fc306b878e9497986baa65bfd1d4de66a`](https://github.com/opennextjs/opennextjs-aws/commit/4d328e3fc306b878e9497986baa65bfd1d4de66a), [`2b2a48b70ae95b5e600ac2e4b7f2df8702c5c26e`](https://github.com/opennextjs/opennextjs-aws/commit/2b2a48b70ae95b5e600ac2e4b7f2df8702c5c26e), [`f685ddea8f8a5c82591dc02713aff7138f2d9896`](https://github.com/opennextjs/opennextjs-aws/commit/f685ddea8f8a5c82591dc02713aff7138f2d9896), [`ef1fe48d570863266c271e5dedaf02b943849ded`](https://github.com/opennextjs/opennextjs-aws/commit/ef1fe48d570863266c271e5dedaf02b943849ded), [`8ab921f8b5bd40c7ba109ccef3e59a6c24283fb2`](https://github.com/opennextjs/opennextjs-aws/commit/8ab921f8b5bd40c7ba109ccef3e59a6c24283fb2), [`2202f36ce0f87357b249bd127cdd5e84d6deffd3`](https://github.com/opennextjs/opennextjs-aws/commit/2202f36ce0f87357b249bd127cdd5e84d6deffd3), [`44392ba82990d43e16a614113d9e7d8e257e5bdd`](https://github.com/opennextjs/opennextjs-aws/commit/44392ba82990d43e16a614113d9e7d8e257e5bdd), [`4dea7ea2f5ffd1848e51502c88d2efcc1896bb8c`](https://github.com/opennextjs/opennextjs-aws/commit/4dea7ea2f5ffd1848e51502c88d2efcc1896bb8c), [`0ac604e5867497cc93fb677b5ebc28ef87e057f8`](https://github.com/opennextjs/opennextjs-aws/commit/0ac604e5867497cc93fb677b5ebc28ef87e057f8), [`1ece6b479bb4e0309892ffbd1200870821a410c4`](https://github.com/opennextjs/opennextjs-aws/commit/1ece6b479bb4e0309892ffbd1200870821a410c4), [`697681bf9ce25212ce4e2e94d886ca425428280d`](https://github.com/opennextjs/opennextjs-aws/commit/697681bf9ce25212ce4e2e94d886ca425428280d)]:
+ - @opennextjs/aws@3.3.0
+
+## 0.1.2
+
+### Patch Changes
+
+- Updated dependencies [[`6f798debb575b157acb2f5068658f95ace0fae50`](https://github.com/opennextjs/opennextjs-aws/commit/6f798debb575b157acb2f5068658f95ace0fae50), [`fe600ac6f5e513376cf233a5d2ce68affaa3aa5a`](https://github.com/opennextjs/opennextjs-aws/commit/fe600ac6f5e513376cf233a5d2ce68affaa3aa5a), [`5f0cbc8feac9eec728c27bb3b7ff5c3f3bc26716`](https://github.com/opennextjs/opennextjs-aws/commit/5f0cbc8feac9eec728c27bb3b7ff5c3f3bc26716), [`8b51108d9aee7e5ed3027c1ceda99091b579951d`](https://github.com/opennextjs/opennextjs-aws/commit/8b51108d9aee7e5ed3027c1ceda99091b579951d), [`b999c4e9a38499680bed77ddeb94b62a3301c0fa`](https://github.com/opennextjs/opennextjs-aws/commit/b999c4e9a38499680bed77ddeb94b62a3301c0fa), [`ba84259d2e35e79a562a7e3f055e350a03c9d651`](https://github.com/opennextjs/opennextjs-aws/commit/ba84259d2e35e79a562a7e3f055e350a03c9d651)]:
+ - @opennextjs/aws@3.2.2
+
+## 0.1.1
+
+### Patch Changes
+
+- Updated dependencies [[`cf33973f3fbab73e77898fdd072a00a1f037257a`](https://github.com/opennextjs/opennextjs-aws/commit/cf33973f3fbab73e77898fdd072a00a1f037257a), [`77d87e7a870fad6afad022bf75aca18c8656c268`](https://github.com/opennextjs/opennextjs-aws/commit/77d87e7a870fad6afad022bf75aca18c8656c268), [`a43b82b4cb68889371ac8260aefef9e04eefb037`](https://github.com/opennextjs/opennextjs-aws/commit/a43b82b4cb68889371ac8260aefef9e04eefb037), [`bfa1a8c4056bd691fb57617dd6287693e51071b4`](https://github.com/opennextjs/opennextjs-aws/commit/bfa1a8c4056bd691fb57617dd6287693e51071b4), [`5839217411012d1df2874d299daa977ba3701c2c`](https://github.com/opennextjs/opennextjs-aws/commit/5839217411012d1df2874d299daa977ba3701c2c), [`dfc174d88b7bcc54eede09c98d9443dd84b93fd8`](https://github.com/opennextjs/opennextjs-aws/commit/dfc174d88b7bcc54eede09c98d9443dd84b93fd8)]:
+ - @opennextjs/aws@3.2.1
diff --git a/examples/e2e/app-pages-router/README.md b/examples/e2e/app-pages-router/README.md
new file mode 100644
index 00000000..dd87bc02
--- /dev/null
+++ b/examples/e2e/app-pages-router/README.md
@@ -0,0 +1,3 @@
+# App Pages Router
+
+This project uses both the App and Pages router.
diff --git a/examples/e2e/app-pages-router/app/albums/@modal/(.)[album]/[song]/page.tsx b/examples/e2e/app-pages-router/app/albums/@modal/(.)[album]/[song]/page.tsx
new file mode 100644
index 00000000..f83f84b4
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/albums/@modal/(.)[album]/[song]/page.tsx
@@ -0,0 +1,29 @@
+import { getSong } from "@example/shared/api";
+import Modal from "@example/shared/components/Modal";
+
+type Props = {
+ params: Promise<{
+ album: string;
+ song: string;
+ }>;
+};
+export default async function SongPage(props: Props) {
+ const params = await props.params;
+ const song = await getSong(params.album, params.song);
+ return (
+
+ Modal
+ Album: {decodeURIComponent(params.album)}
+
+ {/* */}
+
+
+
+ );
+}
diff --git a/examples/e2e/app-pages-router/app/albums/@modal/(.)[album]/page.tsx b/examples/e2e/app-pages-router/app/albums/@modal/(.)[album]/page.tsx
new file mode 100644
index 00000000..463c0c96
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/albums/@modal/(.)[album]/page.tsx
@@ -0,0 +1,11 @@
+import Modal from "@example/shared/components/Modal";
+
+type Props = {
+ params: Promise<{
+ artist: string;
+ }>;
+};
+export default async function ArtistPage(props: Props) {
+ const params = await props.params;
+ return Artists {params.artist};
+}
diff --git a/examples/e2e/app-pages-router/app/albums/@modal/default.tsx b/examples/e2e/app-pages-router/app/albums/@modal/default.tsx
new file mode 100644
index 00000000..6ddf1b76
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/albums/@modal/default.tsx
@@ -0,0 +1,3 @@
+export default function Default() {
+ return null;
+}
diff --git a/examples/e2e/app-pages-router/app/albums/[album]/[song]/page.tsx b/examples/e2e/app-pages-router/app/albums/[album]/[song]/page.tsx
new file mode 100644
index 00000000..87e15694
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/albums/[album]/[song]/page.tsx
@@ -0,0 +1,25 @@
+import { getSong } from "@example/shared/api";
+
+type Props = {
+ params: Promise<{
+ album: string;
+ song: string;
+ }>;
+};
+export default async function Song(props: Props) {
+ const params = await props.params;
+ const song = await getSong(params.album, params.song);
+
+ return (
+
+
Not Modal
+ {decodeURIComponent(params.album)}
+
+
+ );
+}
diff --git a/examples/e2e/app-pages-router/app/albums/[album]/page.tsx b/examples/e2e/app-pages-router/app/albums/[album]/page.tsx
new file mode 100644
index 00000000..8d3a5537
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/albums/[album]/page.tsx
@@ -0,0 +1,3 @@
+export default function ArtistPage() {
+ return Artist
;
+}
diff --git a/examples/e2e/app-pages-router/app/albums/layout.tsx b/examples/e2e/app-pages-router/app/albums/layout.tsx
new file mode 100644
index 00000000..0de9cfdc
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/albums/layout.tsx
@@ -0,0 +1,10 @@
+import type { ReactNode } from "react";
+
+export default function Layout({ children, modal }: { children: ReactNode; modal: ReactNode }) {
+ return (
+
+ {children}
+ {modal}
+
+ );
+}
diff --git a/examples/e2e/app-pages-router/app/albums/page.tsx b/examples/e2e/app-pages-router/app/albums/page.tsx
new file mode 100644
index 00000000..6471009b
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/albums/page.tsx
@@ -0,0 +1,13 @@
+import { getAlbums } from "@example/shared/api";
+import Album from "@example/shared/components/Album";
+
+export default async function AlbumPage() {
+ const albums = await getAlbums();
+ return (
+
+ {albums.map((album) => (
+
+ ))}
+
+ );
+}
diff --git a/examples/e2e/app-pages-router/app/api/client/route.ts b/examples/e2e/app-pages-router/app/api/client/route.ts
new file mode 100644
index 00000000..e9c204ba
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/api/client/route.ts
@@ -0,0 +1,7 @@
+import { NextResponse } from "next/server";
+
+export async function GET(request: Request) {
+ return NextResponse.json({
+ hello: "client",
+ });
+}
diff --git a/examples/e2e/app-pages-router/app/api/host/route.ts b/examples/e2e/app-pages-router/app/api/host/route.ts
new file mode 100644
index 00000000..16630b34
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/api/host/route.ts
@@ -0,0 +1,7 @@
+import { NextResponse } from "next/server";
+
+export async function GET(request: Request) {
+ return NextResponse.json({
+ url: request.url,
+ });
+}
diff --git a/examples/e2e/app-pages-router/app/api/page.tsx b/examples/e2e/app-pages-router/app/api/page.tsx
new file mode 100644
index 00000000..0771ac5a
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/api/page.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import { useCallback, useState } from "react";
+
+/**
+ * Make /api/hello call exclusively on the client
+ * - we already know SSR can fetch itself w/o issues
+ */
+export default function Page() {
+ const [data, setData] = useState();
+
+ const onClientClick = useCallback(async () => {
+ const { protocol, host } = window.location;
+ const url = `${protocol}//${host}`;
+ const r = await fetch(`${url}/api/client`);
+ const d = await r.json();
+ setData(d);
+ }, []);
+
+ const onMiddlewareClick = useCallback(async () => {
+ const { protocol, host } = window.location;
+ const url = `${protocol}//${host}`;
+ const r = await fetch(`${url}/api/middleware`);
+ const d = await r.json();
+ setData(d);
+ }, []);
+
+ return (
+
+
API: {data ? JSON.stringify(data, null, 2) : "N/A"}
+
+
+
+
+ );
+}
diff --git a/examples/e2e/app-pages-router/app/globals.css b/examples/e2e/app-pages-router/app/globals.css
new file mode 100644
index 00000000..e0936eda
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/globals.css
@@ -0,0 +1,23 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ --foreground-rgb: 0, 0, 0;
+ --background-start-rgb: 214, 219, 220;
+ --background-end-rgb: 255, 255, 255;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --foreground-rgb: 255, 255, 255;
+ --background-start-rgb: 0, 0, 0;
+ --background-end-rgb: 0, 0, 0;
+ }
+}
+
+body {
+ color: rgb(var(--foreground-rgb));
+ background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb)))
+ rgb(var(--background-start-rgb));
+}
diff --git a/examples/e2e/app-pages-router/app/image-optimization/page.tsx b/examples/e2e/app-pages-router/app/image-optimization/page.tsx
new file mode 100644
index 00000000..3bc0ee04
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/image-optimization/page.tsx
@@ -0,0 +1,9 @@
+import Image from "next/image";
+
+export default function ImageOptimization() {
+ return (
+
+
+
+ );
+}
diff --git a/examples/e2e/app-pages-router/app/isr/page.tsx b/examples/e2e/app-pages-router/app/isr/page.tsx
new file mode 100644
index 00000000..3eadac36
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/isr/page.tsx
@@ -0,0 +1,9 @@
+async function getTime() {
+ return new Date().toISOString();
+}
+
+export const revalidate = 10;
+export default async function ISR() {
+ const time = getTime();
+ return Time: {time}
;
+}
diff --git a/examples/e2e/app-pages-router/app/layout.tsx b/examples/e2e/app-pages-router/app/layout.tsx
new file mode 100644
index 00000000..f281f848
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/layout.tsx
@@ -0,0 +1,22 @@
+import "./globals.css";
+
+import type { Metadata } from "next";
+import { Inter } from "next/font/google";
+
+const inter = Inter({ subsets: ["latin"] });
+
+export const metadata: Metadata = {
+ title: "Nextjs App Router",
+ description: "Generated by create next app",
+};
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/examples/e2e/app-pages-router/app/page.tsx b/examples/e2e/app-pages-router/app/page.tsx
new file mode 100644
index 00000000..f770b063
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/page.tsx
@@ -0,0 +1,48 @@
+import Nav from "@example/shared/components/Nav";
+
+export default function Home() {
+ return (
+ <>
+ App Router
+
+
+
+
+
+
+
+
+
+
+
+ Pages Router
+
+
+
+
+ >
+ );
+}
diff --git a/examples/e2e/app-pages-router/app/parallel/@a/a-page/page.tsx b/examples/e2e/app-pages-router/app/parallel/@a/a-page/page.tsx
new file mode 100644
index 00000000..8108d277
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/parallel/@a/a-page/page.tsx
@@ -0,0 +1,3 @@
+export default function APage() {
+ return A Page
;
+}
diff --git a/examples/e2e/app-pages-router/app/parallel/@a/page.tsx b/examples/e2e/app-pages-router/app/parallel/@a/page.tsx
new file mode 100644
index 00000000..5e7ee146
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/parallel/@a/page.tsx
@@ -0,0 +1,10 @@
+import Link from "next/link";
+
+export default function A() {
+ return (
+
+
Parallel Route A
+ Go to a-page
+
+ );
+}
diff --git a/examples/e2e/app-pages-router/app/parallel/@b/b-page/page.tsx b/examples/e2e/app-pages-router/app/parallel/@b/b-page/page.tsx
new file mode 100644
index 00000000..1750356e
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/parallel/@b/b-page/page.tsx
@@ -0,0 +1,3 @@
+export default function BPage() {
+ return B Page
;
+}
diff --git a/examples/e2e/app-pages-router/app/parallel/@b/page.tsx b/examples/e2e/app-pages-router/app/parallel/@b/page.tsx
new file mode 100644
index 00000000..b217f84e
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/parallel/@b/page.tsx
@@ -0,0 +1,11 @@
+import Link from "next/link";
+
+export default function B() {
+ return (
+
+
Parallel Route B
+
+ Go to b-page
+
+ );
+}
diff --git a/examples/e2e/app-pages-router/app/parallel/layout.tsx b/examples/e2e/app-pages-router/app/parallel/layout.tsx
new file mode 100644
index 00000000..5925ac09
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/parallel/layout.tsx
@@ -0,0 +1,42 @@
+"use client";
+import { useState } from "react";
+
+import type { ReactNode } from "react";
+
+export default function Layout({ a, b, children }: { children: ReactNode; a: ReactNode; b: ReactNode }) {
+ const [routeA, setRouteA] = useState(false);
+ const [routeB, setRouteB] = useState(false);
+
+ return (
+
+ );
+}
diff --git a/examples/e2e/app-pages-router/app/parallel/page.tsx b/examples/e2e/app-pages-router/app/parallel/page.tsx
new file mode 100644
index 00000000..67e08591
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/parallel/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return null;
+}
diff --git a/examples/e2e/app-pages-router/app/redirect-destination/page.tsx b/examples/e2e/app-pages-router/app/redirect-destination/page.tsx
new file mode 100644
index 00000000..1fe01ac3
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/redirect-destination/page.tsx
@@ -0,0 +1,3 @@
+export default function RedirectDestination() {
+ return Redirect Destination
;
+}
diff --git a/examples/e2e/app-pages-router/app/rewrite-destination/page.tsx b/examples/e2e/app-pages-router/app/rewrite-destination/page.tsx
new file mode 100644
index 00000000..67a7bab2
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/rewrite-destination/page.tsx
@@ -0,0 +1,9 @@
+export default async function RewriteDestination(props: { searchParams: Promise<{ a: string }> }) {
+ const searchParams = await props.searchParams;
+ return (
+
+
Rewritten Destination
+
a: {searchParams.a}
+
+ );
+}
diff --git a/examples/e2e/app-pages-router/app/server-actions/client.tsx b/examples/e2e/app-pages-router/app/server-actions/client.tsx
new file mode 100644
index 00000000..8f3bbda3
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/server-actions/client.tsx
@@ -0,0 +1,26 @@
+"use client";
+import { useCallback, useState, useTransition } from "react";
+
+import type { Song as SongType } from "@example/shared/api";
+import { getSong } from "@example/shared/api";
+import Song from "@example/shared/components/Album/Song";
+
+export default function Client() {
+ const [isPending, startTransition] = useTransition();
+ const [song, setSong] = useState();
+
+ const onClick = useCallback(() => {
+ startTransition(async () => {
+ const song = await getSong("Hold Me In Your Arms", "I'm never gonna give you up");
+ setSong(song);
+ });
+ }, []);
+
+ return (
+
+
+ {isPending &&
☎️ing Server Actions...
}
+ {song &&
}
+
+ );
+}
diff --git a/examples/e2e/app-pages-router/app/server-actions/page.tsx b/examples/e2e/app-pages-router/app/server-actions/page.tsx
new file mode 100644
index 00000000..f15c66fc
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/server-actions/page.tsx
@@ -0,0 +1,10 @@
+import Client from "./client";
+
+export default function Page() {
+ return (
+
+
Server Actions
+
+
+ );
+}
diff --git a/examples/e2e/app-pages-router/app/ssr/layout.tsx b/examples/e2e/app-pages-router/app/ssr/layout.tsx
new file mode 100644
index 00000000..68e3e07e
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/ssr/layout.tsx
@@ -0,0 +1,10 @@
+import type { PropsWithChildren } from "react";
+
+export default function Layout({ children }: PropsWithChildren) {
+ return (
+
+
SSR
+ {children}
+
+ );
+}
diff --git a/examples/e2e/app-pages-router/app/ssr/loading.tsx b/examples/e2e/app-pages-router/app/ssr/loading.tsx
new file mode 100644
index 00000000..fc80ef06
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/ssr/loading.tsx
@@ -0,0 +1,3 @@
+export default function Loading() {
+ return Loading...
;
+}
diff --git a/examples/e2e/app-pages-router/app/ssr/page.tsx b/examples/e2e/app-pages-router/app/ssr/page.tsx
new file mode 100644
index 00000000..377a746c
--- /dev/null
+++ b/examples/e2e/app-pages-router/app/ssr/page.tsx
@@ -0,0 +1,22 @@
+import { headers } from "next/headers";
+
+async function getTime() {
+ const res = await new Promise((resolve) => {
+ setTimeout(() => {
+ resolve(new Date().toISOString());
+ }, 1500);
+ });
+ return res;
+}
+
+export default async function SSR() {
+ const time = await getTime();
+ const headerList = await headers();
+ return (
+
+
Time: {time}
+
{headerList.get("host")}
+
Env: {process.env.SOME_ENV_VAR}
+
+ );
+}
diff --git a/examples/e2e/app-pages-router/e2e/api.test.ts b/examples/e2e/app-pages-router/e2e/api.test.ts
new file mode 100644
index 00000000..14bb2f56
--- /dev/null
+++ b/examples/e2e/app-pages-router/e2e/api.test.ts
@@ -0,0 +1,35 @@
+import { expect, test } from "@playwright/test";
+
+test("API call from client", async ({ page }) => {
+ await page.goto("/");
+ await page.locator('[href="/api"]').click();
+
+ await page.waitForURL("/api");
+
+ let el = page.getByText("API: N/A");
+ await expect(el).toBeVisible();
+
+ await page.getByRole("button", { name: "Call /api/client" }).click();
+ el = page.getByText('API: { "hello": "client" }');
+ await expect(el).toBeVisible();
+});
+
+test("API call from middleware", async ({ page }) => {
+ await page.goto("/");
+ await page.getByRole("link", { name: "/API" }).click();
+
+ await page.waitForURL("/api");
+
+ let el = page.getByText("API: N/A");
+ await expect(el).toBeVisible();
+
+ await page.getByRole("button", { name: "Call /api/middleware" }).click();
+ el = page.getByText('API: { "hello": "middleware" }');
+ await expect(el).toBeVisible();
+});
+
+test("API call from middleware with top-level await", async ({ request }) => {
+ const response = await request.get("/api/middlewareTopLevelAwait");
+ const data = await response.json();
+ expect(data).toEqual({ hello: "top-level-await" });
+});
diff --git a/examples/e2e/app-pages-router/e2e/host.test.ts b/examples/e2e/app-pages-router/e2e/host.test.ts
new file mode 100644
index 00000000..98141106
--- /dev/null
+++ b/examples/e2e/app-pages-router/e2e/host.test.ts
@@ -0,0 +1,11 @@
+import { expect, test } from "@playwright/test";
+
+/**
+ * Tests that the request.url is the deployed host and not localhost
+ */
+test("Request.url is host", async ({ baseURL, page }) => {
+ await page.goto("/api/host");
+
+ const el = page.getByText(`{"url":"${baseURL}/api/host"}`);
+ await expect(el).toBeVisible();
+});
diff --git a/examples/e2e/app-pages-router/e2e/image-optimization.test.ts b/examples/e2e/app-pages-router/e2e/image-optimization.test.ts
new file mode 100644
index 00000000..64e02051
--- /dev/null
+++ b/examples/e2e/app-pages-router/e2e/image-optimization.test.ts
@@ -0,0 +1,18 @@
+import { expect, test } from "@playwright/test";
+
+test("Image Optimization", async ({ page }) => {
+ await page.goto("/");
+
+ const imageResponsePromise = page.waitForResponse(/corporate_holiday_card.jpg/);
+ await page.locator('[href="/image-optimization"]').click();
+ const imageResponse = await imageResponsePromise;
+
+ await page.waitForURL("/image-optimization");
+
+ const imageContentType = imageResponse.headers()["content-type"];
+ expect(imageContentType).toBe("image/webp");
+
+ const el = page.locator("img");
+ await expect(el).toHaveJSProperty("complete", true);
+ await expect(el).not.toHaveJSProperty("naturalWidth", 0);
+});
diff --git a/examples/e2e/app-pages-router/e2e/isr.test.ts b/examples/e2e/app-pages-router/e2e/isr.test.ts
new file mode 100644
index 00000000..4d0d35c0
--- /dev/null
+++ b/examples/e2e/app-pages-router/e2e/isr.test.ts
@@ -0,0 +1,41 @@
+import { expect, test } from "@playwright/test";
+
+test("Incremental Static Regeneration", async ({ page }) => {
+ test.setTimeout(60000);
+ await page.goto("/");
+ await page.locator('[href="/isr"]').click();
+ // Load the page a couple times to regenerate ISR
+
+ let el = page.getByText("Time:");
+ // Track the static time
+ let time = await el.textContent();
+ let newTime: typeof time;
+ let tempTime = time;
+ do {
+ await page.waitForTimeout(1000);
+ await page.reload();
+ time = tempTime;
+ el = page.getByText("Time:");
+ newTime = await el.textContent();
+ tempTime = newTime;
+ } while (time !== newTime);
+ await page.reload();
+
+ await page.waitForTimeout(1000);
+ el = page.getByText("Time:");
+ const midTime = await el.textContent();
+ // Expect that the time is still stale
+ expect(midTime).toEqual(newTime);
+
+ // Wait 10 + 1 seconds for ISR to regenerate time
+ await page.waitForTimeout(11000);
+ let finalTime = newTime;
+ do {
+ await page.waitForTimeout(2000);
+ el = page.getByText("Time:");
+ finalTime = await el.textContent();
+ await page.reload();
+ } while (newTime === finalTime);
+
+ expect(newTime).not.toEqual(finalTime);
+});
diff --git a/examples/e2e/app-pages-router/e2e/middleware.redirect.test.ts b/examples/e2e/app-pages-router/e2e/middleware.redirect.test.ts
new file mode 100644
index 00000000..108a0ef0
--- /dev/null
+++ b/examples/e2e/app-pages-router/e2e/middleware.redirect.test.ts
@@ -0,0 +1,20 @@
+import { expect, test } from "@playwright/test";
+
+test("Middleware Redirect", async ({ page, context }) => {
+ await page.goto("/");
+ await page.locator('[href="/redirect"]').click();
+
+ // URL is immediately redirected
+ await page.waitForURL("/redirect-destination");
+ let el = page.getByText("Redirect Destination", { exact: true });
+ await expect(el).toBeVisible();
+
+ // Loading page should also redirect
+ await page.goto("/redirect");
+ await page.waitForURL("/redirect-destination");
+ expect(await context.cookies().then((res) => res.find((cookie) => cookie.name === "test")?.value)).toBe(
+ "success"
+ );
+ el = page.getByText("Redirect Destination", { exact: true });
+ await expect(el).toBeVisible();
+});
diff --git a/examples/e2e/app-pages-router/e2e/middleware.rewrite.test.ts b/examples/e2e/app-pages-router/e2e/middleware.rewrite.test.ts
new file mode 100644
index 00000000..ba09abd6
--- /dev/null
+++ b/examples/e2e/app-pages-router/e2e/middleware.rewrite.test.ts
@@ -0,0 +1,19 @@
+import { expect, test } from "@playwright/test";
+
+test("Middleware Rewrite", async ({ page }) => {
+ await page.goto("/");
+ await page.locator('[href="/rewrite"]').click();
+
+ await page.waitForURL("/rewrite");
+ let el = page.getByText("Rewritten Destination", { exact: true });
+ await expect(el).toBeVisible();
+ el = page.getByText("a: b", { exact: true });
+ await expect(el).toBeVisible();
+ // Loading page should also rewrite
+ await page.goto("/rewrite");
+ await page.waitForURL("/rewrite");
+ el = page.getByText("Rewritten Destination", { exact: true });
+ await expect(el).toBeVisible();
+ el = page.getByText("a: b", { exact: true });
+ await expect(el).toBeVisible();
+});
diff --git a/examples/e2e/app-pages-router/e2e/modals.test.ts b/examples/e2e/app-pages-router/e2e/modals.test.ts
new file mode 100644
index 00000000..5a443024
--- /dev/null
+++ b/examples/e2e/app-pages-router/e2e/modals.test.ts
@@ -0,0 +1,18 @@
+import { expect, test } from "@playwright/test";
+
+test("Route modal and interception", async ({ page }) => {
+ await page.goto("/");
+ await page.getByRole("link", { name: "Albums" }).click();
+ await page.getByRole("link", { name: "Song: I'm never gonna give you up Year: 1965" }).click();
+
+ await page.waitForURL(`/albums/Hold%20Me%20In%20Your%20Arms/I'm%20never%20gonna%20give%20you%20up`);
+
+ const modal = page.getByText("Modal", { exact: true });
+ await expect(modal).toBeVisible();
+
+ // Reload the page to load non intercepted modal
+ await page.reload();
+ await page.waitForURL(`/albums/Hold%20Me%20In%20Your%20Arms/I'm%20never%20gonna%20give%20you%20up`);
+ const notModal = page.getByText("Not Modal", { exact: true });
+ await expect(notModal).toBeVisible();
+});
diff --git a/examples/e2e/app-pages-router/e2e/pages_isr.test.ts b/examples/e2e/app-pages-router/e2e/pages_isr.test.ts
new file mode 100644
index 00000000..93abbd26
--- /dev/null
+++ b/examples/e2e/app-pages-router/e2e/pages_isr.test.ts
@@ -0,0 +1,42 @@
+import { expect, test } from "@playwright/test";
+
+test("Incremental Static Regeneration", async ({ page }) => {
+ test.setTimeout(60000);
+ await page.goto("/");
+ await page.locator('[href="/pages_isr"]').click();
+
+ await page.waitForURL("/pages_isr");
+ // Load the page a couple times to regenerate ISR
+
+ let el = page.getByText("Time:");
+ // Track the static time
+ let time = await el.textContent();
+ let newTime: typeof time;
+ let tempTime = time;
+ do {
+ await page.waitForTimeout(1000);
+ await page.reload();
+ time = tempTime;
+ el = page.getByText("Time:");
+ newTime = await el.textContent();
+ tempTime = newTime;
+ } while (time !== newTime);
+ await page.reload();
+ await page.waitForTimeout(1000);
+ el = page.getByText("Time:");
+ const midTime = await el.textContent();
+ // Expect that the time is still stale
+ expect(midTime).toEqual(newTime);
+
+ // Wait 10 + 1 seconds for ISR to regenerate time
+ await page.waitForTimeout(11000);
+ let finalTime = newTime;
+ do {
+ await page.waitForTimeout(2000);
+ el = page.getByText("Time:");
+ finalTime = await el.textContent();
+ await page.reload();
+ } while (newTime === finalTime);
+
+ expect(newTime).not.toEqual(finalTime);
+});
diff --git a/examples/e2e/app-pages-router/e2e/pages_ssr.test.ts b/examples/e2e/app-pages-router/e2e/pages_ssr.test.ts
new file mode 100644
index 00000000..77e33ac3
--- /dev/null
+++ b/examples/e2e/app-pages-router/e2e/pages_ssr.test.ts
@@ -0,0 +1,27 @@
+import { expect, test } from "@playwright/test";
+
+test("Server Side Render", async ({ page }) => {
+ await page.goto("/");
+ await page.locator('[href="/pages_ssr"]').click();
+
+ await page.waitForURL("/pages_ssr");
+ let el = page.getByText("Time:");
+ await expect(el).toBeVisible();
+ let time = await el.textContent();
+
+ await page.reload();
+
+ el = page.getByText("Time:");
+ let newTime = await el.textContent();
+ await expect(el).toBeVisible();
+
+ for (let i = 0; i < 5; i++) {
+ await page.reload();
+ el = page.getByText("Time:");
+ newTime = await el.textContent();
+ await expect(el).toBeVisible();
+ expect(time).not.toEqual(newTime);
+ time = newTime;
+ await page.waitForTimeout(250);
+ }
+});
diff --git a/examples/e2e/app-pages-router/e2e/parallel.test.ts b/examples/e2e/app-pages-router/e2e/parallel.test.ts
new file mode 100644
index 00000000..8f3bf80f
--- /dev/null
+++ b/examples/e2e/app-pages-router/e2e/parallel.test.ts
@@ -0,0 +1,42 @@
+import { expect, test } from "@playwright/test";
+
+test("Parallel routes", async ({ page }) => {
+ await page.goto("/");
+ await page.locator('[href="/parallel"]').click();
+
+ await page.waitForURL("/parallel");
+
+ // Neither are selected, so A/B shouldn't be rendered
+ let routeA = page.getByText("Parallel Route A");
+ let routeB = page.getByText("Parallel Route B");
+ await expect(routeA).not.toBeVisible();
+ await expect(routeB).not.toBeVisible();
+
+ // Enable A, which should be visible but not B
+ await page.locator('input[name="a"]').check();
+ routeA = page.getByText("Parallel Route A");
+ await expect(routeA).toBeVisible();
+ await expect(routeB).not.toBeVisible();
+
+ // Enable B, both should be visible
+ await page.locator('input[name="b"]').check();
+ routeB = page.getByText("Parallel Route B");
+ await expect(routeA).toBeVisible();
+ await expect(routeB).toBeVisible();
+
+ // Click on A, should go to a-page
+ await page.getByText("Go to a-page").click();
+ await page.waitForURL("/parallel/a-page");
+
+ // Should render contents of a-page
+ routeA = page.getByText("A Page");
+ await expect(routeA).toBeVisible();
+
+ // Click on B, should go to b-page
+ await page.getByText("Go to b-page").click();
+ await page.waitForURL("/parallel/b-page");
+
+ // Should render contents of b-page
+ routeB = page.getByText("B Page");
+ await expect(routeB).toBeVisible();
+});
diff --git a/examples/e2e/app-pages-router/e2e/playwright.config.ts b/examples/e2e/app-pages-router/e2e/playwright.config.ts
new file mode 100644
index 00000000..47fa6c2b
--- /dev/null
+++ b/examples/e2e/app-pages-router/e2e/playwright.config.ts
@@ -0,0 +1,54 @@
+import { defineConfig, devices } from "@playwright/test";
+import type nodeProcess from "node:process";
+
+declare const process: typeof nodeProcess;
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+ testDir: "./",
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Opt out of parallel tests on CI. */
+ workers: process.env.CI ? 1 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: "html",
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL: "http://localhost:8792",
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: "on-first-retry",
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: "chromium",
+ use: { ...devices["Desktop Chrome"] },
+ },
+ // TODO(vicb): enable all browsers
+ // {
+ // name: "firefox",
+ // use: { ...devices["Desktop Firefox"] },
+ // },
+ // {
+ // name: "webkit",
+ // use: { ...devices["Desktop Safari"] },
+ // },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: "pnpm preview",
+ url: "http://localhost:8792",
+ reuseExistingServer: !process.env.CI,
+ timeout: 70_000,
+ },
+});
diff --git a/examples/e2e/app-pages-router/e2e/serverActions.test.ts b/examples/e2e/app-pages-router/e2e/serverActions.test.ts
new file mode 100644
index 00000000..106354e0
--- /dev/null
+++ b/examples/e2e/app-pages-router/e2e/serverActions.test.ts
@@ -0,0 +1,22 @@
+import { expect, test } from "@playwright/test";
+
+test("Server Actions", async ({ page }) => {
+ await page.goto("/");
+ await page.locator('[href="/server-actions"]').click();
+
+ await page.waitForURL("/server-actions");
+ let el = page.getByText("Song: I'm never gonna give you up");
+ await expect(el).not.toBeVisible();
+
+ await page.getByRole("button", { name: "Fire Server Actions" }).click();
+ el = page.getByText("Song: I'm never gonna give you up");
+ await expect(el).toBeVisible();
+
+ // Reload page
+ await page.reload();
+ el = page.getByText("Song: I'm never gonna give you up");
+ await expect(el).not.toBeVisible();
+ await page.getByRole("button", { name: "Fire Server Actions" }).click();
+ el = page.getByText("Song: I'm never gonna give you up");
+ await expect(el).toBeVisible();
+});
diff --git a/examples/e2e/app-pages-router/e2e/skip_trailing.test.ts b/examples/e2e/app-pages-router/e2e/skip_trailing.test.ts
new file mode 100644
index 00000000..2a7033fd
--- /dev/null
+++ b/examples/e2e/app-pages-router/e2e/skip_trailing.test.ts
@@ -0,0 +1,8 @@
+import { expect, test } from "@playwright/test";
+
+test("skipTrailingSlashRedirect redirect", async ({ page }) => {
+ const response = await page.goto("/ssr");
+
+ expect(response?.request().redirectedFrom()).toBeNull();
+ expect(response?.request().url()).toMatch(/\/ssr$/);
+});
diff --git a/examples/e2e/app-pages-router/e2e/ssr.test.ts b/examples/e2e/app-pages-router/e2e/ssr.test.ts
new file mode 100644
index 00000000..63020afd
--- /dev/null
+++ b/examples/e2e/app-pages-router/e2e/ssr.test.ts
@@ -0,0 +1,33 @@
+import { expect, test } from "@playwright/test";
+
+test("Server Side Render", async ({ page }) => {
+ await page.goto("/");
+ await page.locator('[href="/ssr"]').click();
+
+ await page.waitForURL("/ssr");
+ let el = page.getByText("Time:");
+ await expect(el).toBeVisible();
+ let time = await el.textContent();
+
+ await page.reload();
+
+ el = page.getByText("Time:");
+ let newTime = await el.textContent();
+ await expect(el).toBeVisible();
+
+ for (let i = 0; i < 5; i++) {
+ await page.reload();
+ el = page.getByText("Time:");
+ newTime = await el.textContent();
+ await expect(el).toBeVisible();
+ expect(time).not.toEqual(newTime);
+ time = newTime;
+ await page.waitForTimeout(250);
+ }
+});
+
+test("Server Side Render with env", async ({ page }) => {
+ await page.goto("/ssr");
+ const el = page.getByText("Env:");
+ expect(await el.textContent()).toEqual("Env: foo");
+});
diff --git a/examples/e2e/app-pages-router/middleware.ts b/examples/e2e/app-pages-router/middleware.ts
new file mode 100644
index 00000000..5dcf1414
--- /dev/null
+++ b/examples/e2e/app-pages-router/middleware.ts
@@ -0,0 +1,57 @@
+import type { NextRequest } from "next/server";
+import { NextResponse } from "next/server";
+
+// Needed to test top-level await
+// We are using `setTimeout` to simulate a "long" running operation
+// we could have used `Promise.resolve` instead, but it would be running in a different way in the event loop
+// @ts-expect-error - It will cause a warning at build time, but it should just work
+const topLevelAwait = await new Promise((resolve) => {
+ setTimeout(() => {
+ resolve("top-level-await");
+ }, 10);
+});
+
+export function middleware(request: NextRequest) {
+ const path = request.nextUrl.pathname; //new URL(request.url).pathname;
+
+ const host = request.headers.get("host");
+ const protocol = host?.startsWith("localhost") ? "http" : "https";
+ if (path === "/redirect") {
+ const u = new URL("/redirect-destination", `${protocol}://${host}`);
+ return NextResponse.redirect(u, {
+ headers: { "set-cookie": "test=success" },
+ });
+ }
+ if (path === "/rewrite") {
+ const u = new URL("/rewrite-destination", `${protocol}://${host}`);
+ u.searchParams.set("a", "b");
+ return NextResponse.rewrite(u);
+ }
+ if (path === "/api/middleware") {
+ return new NextResponse(JSON.stringify({ hello: "middleware" }), {
+ status: 200,
+ headers: {
+ "content-type": "application/json",
+ },
+ });
+ }
+ if (path === "/api/middlewareTopLevelAwait") {
+ return new NextResponse(JSON.stringify({ hello: topLevelAwait }), {
+ status: 200,
+ headers: {
+ "content-type": "application/json",
+ },
+ });
+ }
+ const rHeaders = new Headers(request.headers);
+ const r = NextResponse.next({
+ request: {
+ headers: rHeaders,
+ },
+ });
+ return r;
+}
+
+export const config = {
+ matcher: ["/((?!_next|favicon.ico|match|static|fonts|api/auth|og).*)"],
+};
diff --git a/examples/e2e/app-pages-router/next.config.ts b/examples/e2e/app-pages-router/next.config.ts
new file mode 100644
index 00000000..f8a1d2f4
--- /dev/null
+++ b/examples/e2e/app-pages-router/next.config.ts
@@ -0,0 +1,19 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+ poweredByHeader: false,
+ cleanDistDir: true,
+ transpilePackages: ["@example/shared"],
+ output: "standalone",
+ // outputFileTracingRoot: "../sst",
+ typescript: {
+ ignoreBuildErrors: true,
+ },
+ eslint: {
+ ignoreDuringBuilds: true,
+ },
+ trailingSlash: true,
+ skipTrailingSlashRedirect: true,
+};
+
+export default nextConfig;
diff --git a/examples/e2e/app-pages-router/open-next.config.ts b/examples/e2e/app-pages-router/open-next.config.ts
new file mode 100644
index 00000000..0f7794ff
--- /dev/null
+++ b/examples/e2e/app-pages-router/open-next.config.ts
@@ -0,0 +1,25 @@
+import type { OpenNextConfig } from "@opennextjs/aws/types/open-next";
+
+const config: OpenNextConfig = {
+ default: {
+ override: {
+ wrapper: "cloudflare-node",
+ converter: "edge",
+ // Unused implementation
+ incrementalCache: "dummy",
+ tagCache: "dummy",
+ queue: "dummy",
+ },
+ },
+
+ middleware: {
+ external: true,
+ override: {
+ wrapper: "cloudflare-edge",
+ converter: "edge",
+ proxyExternalRequest: "fetch",
+ },
+ },
+};
+
+export default config;
diff --git a/examples/e2e/app-pages-router/package.json b/examples/e2e/app-pages-router/package.json
new file mode 100644
index 00000000..6002dc18
--- /dev/null
+++ b/examples/e2e/app-pages-router/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "app-pages-router",
+ "version": "0.1.6",
+ "private": true,
+ "scripts": {
+ "openbuild": "node ../../packages/open-next/dist/index.js build --build-command \"npx turbo build\"",
+ "dev": "next dev --turbopack --port 3003",
+ "build": "next build",
+ "start": "next start --port 3003",
+ "lint": "next lint",
+ "clean": "rm -rf .turbo node_modules .next .open-next",
+ "build:worker": "pnpm opennextjs-cloudflare",
+ "dev:worker": "wrangler dev --port 8792 --inspector-port 9352",
+ "preview": "pnpm build:worker && pnpm dev:worker",
+ "e2e-fix": "playwright test -c e2e/playwright.config.ts"
+ },
+ "dependencies": {
+ "@opennextjs/cloudflare": "workspace:*",
+ "@example/shared": "workspace:*",
+ "next": "catalog:e2e",
+ "react": "catalog:e2e",
+ "react-dom": "catalog:e2e"
+ },
+ "devDependencies": {
+ "@playwright/test": "catalog:",
+ "@types/node": "catalog:e2e",
+ "@types/react": "catalog:e2e",
+ "@types/react-dom": "catalog:e2e",
+ "autoprefixer": "catalog:e2e",
+ "postcss": "catalog:e2e",
+ "tailwindcss": "catalog:e2e",
+ "typescript": "catalog:default"
+ }
+}
diff --git a/examples/e2e/app-pages-router/pages/_app.tsx b/examples/e2e/app-pages-router/pages/_app.tsx
new file mode 100644
index 00000000..a9d6fd8d
--- /dev/null
+++ b/examples/e2e/app-pages-router/pages/_app.tsx
@@ -0,0 +1,7 @@
+import "@/styles/globals.css";
+
+import type { AppProps } from "next/app";
+
+export default function App({ Component, pageProps }: AppProps) {
+ return ;
+}
diff --git a/examples/e2e/app-pages-router/pages/_document.tsx b/examples/e2e/app-pages-router/pages/_document.tsx
new file mode 100644
index 00000000..ffc3f3cc
--- /dev/null
+++ b/examples/e2e/app-pages-router/pages/_document.tsx
@@ -0,0 +1,13 @@
+import { Head, Html, Main, NextScript } from "next/document";
+
+export default function Document() {
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/examples/e2e/app-pages-router/pages/api/hello.ts b/examples/e2e/app-pages-router/pages/api/hello.ts
new file mode 100644
index 00000000..41b59cbc
--- /dev/null
+++ b/examples/e2e/app-pages-router/pages/api/hello.ts
@@ -0,0 +1,10 @@
+// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
+import type { NextApiRequest, NextApiResponse } from "next";
+
+type Data = {
+ hello: string;
+};
+
+export default function handler(req: NextApiRequest, res: NextApiResponse) {
+ res.status(200).json({ hello: "world" });
+}
diff --git a/examples/e2e/app-pages-router/pages/pages_isr/index.tsx b/examples/e2e/app-pages-router/pages/pages_isr/index.tsx
new file mode 100644
index 00000000..69af885d
--- /dev/null
+++ b/examples/e2e/app-pages-router/pages/pages_isr/index.tsx
@@ -0,0 +1,14 @@
+import type { InferGetStaticPropsType } from "next";
+
+export async function getStaticProps() {
+ return {
+ props: {
+ time: new Date().toISOString(),
+ },
+ revalidate: 10,
+ };
+}
+
+export default function Page({ time }: InferGetStaticPropsType) {
+ return Time: {time}
;
+}
diff --git a/examples/e2e/app-pages-router/pages/pages_ssr/index.tsx b/examples/e2e/app-pages-router/pages/pages_ssr/index.tsx
new file mode 100644
index 00000000..ba06018d
--- /dev/null
+++ b/examples/e2e/app-pages-router/pages/pages_ssr/index.tsx
@@ -0,0 +1,13 @@
+import type { InferGetServerSidePropsType } from "next";
+
+export async function getServerSideProps() {
+ return {
+ props: {
+ time: new Date().toISOString(),
+ },
+ };
+}
+
+export default function Page({ time }: InferGetServerSidePropsType) {
+ return Time: {time}
;
+}
diff --git a/examples/e2e/app-pages-router/postcss.config.js b/examples/e2e/app-pages-router/postcss.config.js
new file mode 100644
index 00000000..12a703d9
--- /dev/null
+++ b/examples/e2e/app-pages-router/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/examples/e2e/app-pages-router/public/favicon.ico b/examples/e2e/app-pages-router/public/favicon.ico
new file mode 100644
index 00000000..4ba005f2
Binary files /dev/null and b/examples/e2e/app-pages-router/public/favicon.ico differ
diff --git a/examples/e2e/app-pages-router/public/static/corporate_holiday_card.jpg b/examples/e2e/app-pages-router/public/static/corporate_holiday_card.jpg
new file mode 100644
index 00000000..0df96ae2
Binary files /dev/null and b/examples/e2e/app-pages-router/public/static/corporate_holiday_card.jpg differ
diff --git a/examples/e2e/app-pages-router/public/static/frank.webp b/examples/e2e/app-pages-router/public/static/frank.webp
new file mode 100644
index 00000000..b2cc67f0
Binary files /dev/null and b/examples/e2e/app-pages-router/public/static/frank.webp differ
diff --git a/examples/e2e/app-pages-router/styles/globals.css b/examples/e2e/app-pages-router/styles/globals.css
new file mode 100644
index 00000000..cb7c88f8
--- /dev/null
+++ b/examples/e2e/app-pages-router/styles/globals.css
@@ -0,0 +1,95 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ --max-width: 1100px;
+ --border-radius: 12px;
+ --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono",
+ "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace;
+
+ --foreground-rgb: 0, 0, 0;
+ --background-start-rgb: 214, 219, 220;
+ --background-end-rgb: 255, 255, 255;
+
+ --primary-glow: conic-gradient(
+ from 180deg at 50% 50%,
+ #16abff33 0deg,
+ #0885ff33 55deg,
+ #54d6ff33 120deg,
+ #0071ff33 160deg,
+ transparent 360deg
+ );
+ --secondary-glow: radial-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
+
+ --tile-start-rgb: 239, 245, 249;
+ --tile-end-rgb: 228, 232, 233;
+ --tile-border: conic-gradient(#00000080, #00000040, #00000030, #00000020, #00000010, #00000010, #00000080);
+
+ --callout-rgb: 238, 240, 241;
+ --callout-border-rgb: 172, 175, 176;
+ --card-rgb: 180, 185, 188;
+ --card-border-rgb: 131, 134, 135;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --foreground-rgb: 255, 255, 255;
+ --background-start-rgb: 0, 0, 0;
+ --background-end-rgb: 0, 0, 0;
+
+ --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
+ --secondary-glow: linear-gradient(
+ to bottom right,
+ rgba(1, 65, 255, 0),
+ rgba(1, 65, 255, 0),
+ rgba(1, 65, 255, 0.3)
+ );
+
+ --tile-start-rgb: 2, 13, 46;
+ --tile-end-rgb: 2, 5, 19;
+ --tile-border: conic-gradient(
+ #ffffff80,
+ #ffffff40,
+ #ffffff30,
+ #ffffff20,
+ #ffffff10,
+ #ffffff10,
+ #ffffff80
+ );
+
+ --callout-rgb: 20, 20, 20;
+ --callout-border-rgb: 108, 108, 108;
+ --card-rgb: 100, 100, 100;
+ --card-border-rgb: 200, 200, 200;
+ }
+}
+
+* {
+ box-sizing: border-box;
+ padding: 0;
+ margin: 0;
+}
+
+html,
+body {
+ max-width: 100vw;
+ overflow-x: hidden;
+}
+
+body {
+ color: rgb(var(--foreground-rgb));
+ background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb)))
+ rgb(var(--background-start-rgb));
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+@media (prefers-color-scheme: dark) {
+ html {
+ color-scheme: dark;
+ }
+}
diff --git a/examples/e2e/app-pages-router/tailwind.config.ts b/examples/e2e/app-pages-router/tailwind.config.ts
new file mode 100644
index 00000000..edaaef36
--- /dev/null
+++ b/examples/e2e/app-pages-router/tailwind.config.ts
@@ -0,0 +1,15 @@
+import type { Config } from "tailwindcss";
+
+const config: Config = {
+ content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "../../examples/shared/**/*.{jsx,tsx}"],
+ theme: {
+ extend: {
+ backgroundImage: {
+ "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
+ "gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
+ },
+ },
+ },
+ plugins: [],
+};
+export default config;
diff --git a/examples/e2e/app-pages-router/tsconfig.json b/examples/e2e/app-pages-router/tsconfig.json
new file mode 100644
index 00000000..4116885e
--- /dev/null
+++ b/examples/e2e/app-pages-router/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"],
+ "@example/shared": ["../shared"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../utils.ts"],
+ "exclude": ["node_modules", "open-next.config.ts"]
+}
diff --git a/examples/e2e/app-pages-router/wrangler.json b/examples/e2e/app-pages-router/wrangler.json
new file mode 100644
index 00000000..0ea9def5
--- /dev/null
+++ b/examples/e2e/app-pages-router/wrangler.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "node_modules/wrangler/config-schema.json",
+ "main": ".open-next/worker.js",
+ "name": "app-pages-router",
+ "compatibility_date": "2024-12-30",
+ "compatibility_flags": ["nodejs_compat"],
+ "assets": {
+ "directory": ".open-next/assets",
+ "binding": "ASSETS"
+ }
+}