diff --git a/.github/workflows/next-app-route.yml b/.github/workflows/next-app-route.yml
new file mode 100644
index 0000000000..f8467e8d02
--- /dev/null
+++ b/.github/workflows/next-app-route.yml
@@ -0,0 +1,78 @@
+name: Next App Route dylib
+
+# NOT a per-PR gate yet, by design. The pinned production graph is 104 modules
+# and one generated chunk alone has taken ~112 min in a single codegen unit, so a
+# `pull_request` trigger on `crates/**` would attach a multi-hour job to nearly
+# every PR. It also cannot pass yet: the computed relative chunk require is still
+# in flight (#8146). Run it on demand and nightly until it is green on `main`,
+# then promote it — a gate that has never been green must not be made required
+# (CLAUDE.md, "a *new* gate has never been green").
+on:
+ workflow_dispatch:
+ schedule:
+ # 03:17 UTC daily, off the hour to avoid the Actions scheduling spike.
+ - cron: "17 3 * * *"
+
+permissions:
+ contents: read
+
+concurrency:
+ # `github.run_id` makes every run its own group. A group that is CONSTANT
+ # across scheduled runs lets GitHub keep at most one pending run and cancel
+ # the rest with zero jobs, so only one scheduled run would ever execute
+ # (#7205, relapsed as #7966) — and a cancelled run is a gate that did not run
+ # (CLAUDE.md, "four ways a gate can be unable to fail", #3).
+ group: next-app-route-${{ github.run_id }}
+ cancel-in-progress: false
+
+jobs:
+ next-app-route:
+ runs-on: ubuntu-latest
+ timeout-minutes: 180
+ env:
+ CARGO_INCREMENTAL: "0"
+ RUSTC_WRAPPER: sccache
+ SCCACHE_GHA_ENABLED: "true"
+ SCCACHE_CACHE_SIZE: "4G"
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ persist-credentials: false
+
+ - uses: dtolnay/rust-toolchain@stable
+ - uses: ./.github/actions/setup-llvm22
+
+ - uses: mozilla-actions/sccache-action@v0.0.11
+ - uses: Swatinem/rust-cache@v2
+ with:
+ shared-key: "${{ runner.os }}-perry"
+ save-if: ${{ github.ref == 'refs/heads/main' }}
+
+ - uses: actions/setup-node@v7
+ with:
+ node-version-file: .node-version
+ cache: npm
+ cache-dependency-path: tests/fixtures/next-app-route/package-lock.json
+
+ - name: Build Perry and provider archives
+ run: >-
+ cargo build --profile perry-dev
+ -p perry
+ -p perry-runtime
+ -p perry-stdlib
+ -p perry-runtime-static
+ -p perry-stdlib-static
+
+ - name: Focused four-store continuation parity
+ env:
+ PERRY_BIN: target/perry-dev/perry
+ PERRY_RUNTIME_DIR: target/perry-dev
+ PERRY_SKIP_BUILD: "1"
+ run: >-
+ ./run_parity_tests.sh
+ --suite node-suite
+ --module async_hooks
+ --filter next-route-continuations
+
+ - name: Production App Route provider gate
+ run: tests/test_next_app_route_dylib.sh
diff --git a/changelog.d/8161-next-app-route-fixture.md b/changelog.d/8161-next-app-route-fixture.md
new file mode 100644
index 0000000000..c962aa0ca6
--- /dev/null
+++ b/changelog.d/8161-next-app-route-fixture.md
@@ -0,0 +1,25 @@
+### Testing
+
+- Add the pinned Next.js 16.3.0 / React 19.2.4 production App Route fixture and
+ its 21-request verifier as real, checked-in files (`tests/fixtures/next-app-route`),
+ plus a Node oracle test that rebuilds the app from its lockfile and proves the
+ generated bundle exports `AppRouteRouteModule.handle` for `/api/benchmark`
+ before any Perry-specific assertion runs (#8034).
+
+- Add `tests/test_next_app_route_dylib.sh`: it compiles that unmodified
+ production route to an **app-only dylib**, links it against separately built
+ runtime and stdlib/HTTP provider images, checks every undefined `js_`/`perry_`
+ symbol the app needs is exported by those providers, and then drives 10 cold
+ starts x 10 verifier repetitions (the 100 repetitions #8037 asks for).
+
+ The host installs a wrapper around `routeModule.handle` and fails any request
+ that reaches the generated handler without passing through it, so the route
+ cannot be "passed" by a compatibility path that calls the userland `GET`
+ directly. Because that assertion runs inside a `.then()` after the response is
+ already sent, the verifier's exit code cannot carry it — the gate greps the
+ host log for the diagnostic instead.
+
+- Add the `Next App Route dylib` workflow on `workflow_dispatch` + a nightly
+ schedule. It is deliberately **not** a per-PR required gate yet: the graph is
+ 104 modules, one generated chunk has taken ~112 min in a single codegen unit,
+ and the route still needs the computed relative chunk require from #8146.
diff --git a/tests/fixtures/next-app-route/.gitignore b/tests/fixtures/next-app-route/.gitignore
new file mode 100644
index 0000000000..30542bd287
--- /dev/null
+++ b/tests/fixtures/next-app-route/.gitignore
@@ -0,0 +1,3 @@
+/.next/
+/node_modules/
+/provider/Cargo.lock
diff --git a/tests/fixtures/next-app-route/README.md b/tests/fixtures/next-app-route/README.md
new file mode 100644
index 0000000000..90aa0d5bb7
--- /dev/null
+++ b/tests/fixtures/next-app-route/README.md
@@ -0,0 +1,38 @@
+# Production Next App Route fixture
+
+This is the pinned fixture from #8034. Its application and verifier sources are
+copied verbatim from the issue:
+
+- Next.js 16.3.0, built with `next build --webpack`
+- React and React DOM 19.2.4
+- twenty concurrent GET requests plus one POST request
+- the generated `routeModule.handle` / `AppRouteRouteModule.handle` path
+- request-local `headers()` reads across a dynamic import and a timer
+- a two-chunk streamed `NextResponse` with status, header, and cookie checks
+
+`package-lock.json` is committed so a gate cannot silently move the framework
+graph. Generated `.next/` output and `node_modules/` are deliberately ignored.
+
+Run the Node oracle and generated-route structural check with:
+
+```sh
+tests/test_next_app_route_node_oracle.sh
+```
+
+The Node check is only the oracle half of #8034. It does not claim Perry
+acceptance. The Perry half compiles the production webpack output as an
+app-only dylib, loads separate runtime and stdlib provider images before the app
+with eager relocation, and builds both providers from one unified Cargo graph
+so all runtime bindings share one image. It enters the generated route-module
+handle path and runs the same `verify.mjs` without a direct `GET` call or
+fabricated response.
+
+Run that integration gate with:
+
+```sh
+PERRY_BIN=target/perry-dev/perry tests/test_next_app_route_dylib.sh
+```
+
+By default it performs ten cold provider-host starts and ten verifier passes
+per start (100 total). `PERRY_NEXT_COLD_STARTS` and
+`PERRY_NEXT_VERIFICATIONS_PER_START` can reduce the count for local iteration.
diff --git a/tests/fixtures/next-app-route/app/api/benchmark/route.ts b/tests/fixtures/next-app-route/app/api/benchmark/route.ts
new file mode 100644
index 0000000000..79498ff14d
--- /dev/null
+++ b/tests/fixtures/next-app-route/app/api/benchmark/route.ts
@@ -0,0 +1,2 @@
+export const dynamic = "force-dynamic";
+export { GET, POST } from "../../../lib/route-impl";
diff --git a/tests/fixtures/next-app-route/app/layout.tsx b/tests/fixtures/next-app-route/app/layout.tsx
new file mode 100644
index 0000000000..86c5ebe27c
--- /dev/null
+++ b/tests/fixtures/next-app-route/app/layout.tsx
@@ -0,0 +1,3 @@
+export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
+ return
{children};
+}
diff --git a/tests/fixtures/next-app-route/app/page.tsx b/tests/fixtures/next-app-route/app/page.tsx
new file mode 100644
index 0000000000..e1e86a0de5
--- /dev/null
+++ b/tests/fixtures/next-app-route/app/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return Perry Next.js App Route fixture;
+}
diff --git a/tests/fixtures/next-app-route/lib/lazy-work.ts b/tests/fixtures/next-app-route/lib/lazy-work.ts
new file mode 100644
index 0000000000..c65009a6bb
--- /dev/null
+++ b/tests/fixtures/next-app-route/lib/lazy-work.ts
@@ -0,0 +1,7 @@
+export function checksum(iterations: number): number {
+ let value = 0x811c9dc5;
+ for (let index = 0; index < iterations; index += 1) {
+ value = Math.imul(value ^ index, 0x01000193) >>> 0;
+ }
+ return value;
+}
diff --git a/tests/fixtures/next-app-route/lib/route-impl.ts b/tests/fixtures/next-app-route/lib/route-impl.ts
new file mode 100644
index 0000000000..e8b83480c9
--- /dev/null
+++ b/tests/fixtures/next-app-route/lib/route-impl.ts
@@ -0,0 +1,52 @@
+import { headers } from "next/headers";
+import { NextRequest, NextResponse } from "next/server";
+
+async function handle(request: NextRequest): Promise {
+ const id = request.nextUrl.searchParams.get("id") ?? "missing";
+ const requestedIterations = Number(request.nextUrl.searchParams.get("iterations") ?? "100");
+ const iterations = Number.isInteger(requestedIterations)
+ ? Math.max(1, Math.min(1_000, requestedIterations))
+ : 100;
+
+ const beforeAwait = (await headers()).get("x-request-id");
+ const { checksum } = await import("./lazy-work");
+ await new Promise((resolve) => setTimeout(resolve, 1));
+ const afterAwait = (await headers()).get("x-request-id");
+ const requestBody = request.method === "POST" ? await request.text() : "";
+
+ const payload = JSON.stringify({
+ runtime: "next",
+ method: request.method,
+ pathname: request.nextUrl.pathname,
+ id,
+ iterations,
+ checksum: checksum(iterations),
+ beforeAwait,
+ afterAwait,
+ requestBody,
+ });
+ const bytes = new TextEncoder().encode(payload);
+ const split = Math.max(1, Math.floor(bytes.length / 2));
+ const stream = new ReadableStream({
+ start(controller) {
+ controller.enqueue(bytes.subarray(0, split));
+ queueMicrotask(() => {
+ controller.enqueue(bytes.subarray(split));
+ controller.close();
+ });
+ },
+ });
+
+ const response = new NextResponse(stream, {
+ status: 207,
+ headers: {
+ "content-type": "application/json; charset=utf-8",
+ "x-perry-repro": id,
+ },
+ });
+ response.cookies.set("perry_ctx", id, { httpOnly: true, sameSite: "strict" });
+ return response;
+}
+
+export const GET = handle;
+export const POST = handle;
diff --git a/tests/fixtures/next-app-route/next-env.d.ts b/tests/fixtures/next-app-route/next-env.d.ts
new file mode 100644
index 0000000000..ce4e94a6b1
--- /dev/null
+++ b/tests/fixtures/next-app-route/next-env.d.ts
@@ -0,0 +1,7 @@
+///
+///
+import "./.next/types/routes.d.ts";
+import "./.next/types/root-params.d.ts";
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/tests/fixtures/next-app-route/next.config.ts b/tests/fixtures/next-app-route/next.config.ts
new file mode 100644
index 0000000000..68a6c64d27
--- /dev/null
+++ b/tests/fixtures/next-app-route/next.config.ts
@@ -0,0 +1,7 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+ output: "standalone",
+};
+
+export default nextConfig;
diff --git a/tests/fixtures/next-app-route/package-lock.json b/tests/fixtures/next-app-route/package-lock.json
new file mode 100644
index 0000000000..5130e0fcb0
--- /dev/null
+++ b/tests/fixtures/next-app-route/package-lock.json
@@ -0,0 +1,1016 @@
+{
+ "name": "perry-next-app-route-fixture",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "perry-next-app-route-fixture",
+ "version": "1.0.0",
+ "dependencies": {
+ "next": "16.3.0",
+ "react": "19.2.4",
+ "react-dom": "19.2.4"
+ },
+ "devDependencies": {
+ "@types/node": "25.3.3",
+ "@types/react": "19.2.14",
+ "@types/react-dom": "19.2.3",
+ "typescript": "5.9.3"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
+ "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@img/colour": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz",
+ "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz",
+ "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-freebsd-wasm32": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz",
+ "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "dependencies": {
+ "@img/sharp-wasm32": "0.35.3"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz",
+ "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz",
+ "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz",
+ "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz",
+ "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz",
+ "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz",
+ "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz",
+ "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz",
+ "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz",
+ "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz",
+ "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz",
+ "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz",
+ "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-linux-ppc64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz",
+ "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-ppc64": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-linux-riscv64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz",
+ "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-riscv64": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz",
+ "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz",
+ "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz",
+ "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz",
+ "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz",
+ "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==",
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.11.1"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-webcontainers-wasm32": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz",
+ "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@img/sharp-wasm32": "0.35.3"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-arm64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz",
+ "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz",
+ "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz",
+ "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@next/env": {
+ "version": "16.3.0",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.0.tgz",
+ "integrity": "sha512-o9r1S0BNiNreHP9Vs+Qnqd9kviDkJh8xIACY7UFZSmiGbbQRzPBBosvHzAU4TULHOIuOj/18RSsyz2qrREmIFw==",
+ "license": "MIT"
+ },
+ "node_modules/@next/swc-darwin-arm64": {
+ "version": "16.3.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.0.tgz",
+ "integrity": "sha512-55hpqq18bEVAlxedlTt3tFqZmKg2nUXT1kn1G/BGEy0R13h3LwtwHPVzzjG6P4LLeOHE32PFDQUVaJEWvBEZBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-darwin-x64": {
+ "version": "16.3.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.0.tgz",
+ "integrity": "sha512-SOi96kSaF5T+0wW4koiM1bWzSPwjzTesC1p3df+FjdOi5LIQkBK/blxh7HdoKnNuI4PURF1OO7TZqtfnbWDSgw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "16.3.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.0.tgz",
+ "integrity": "sha512-P0gZAoPMF4dyTRzhmkV4PrqVzSOB6t4mC1oI3c4dqijJ+OVEVx5clIXAKR4/uQpsqw2KKM/0D5tVumcR2r5blg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-musl": {
+ "version": "16.3.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.0.tgz",
+ "integrity": "sha512-tXXGKJw0m37O0eKJARVTX/TheKPhz0QFVtVVZXmOig+9YKLQOSP6hvf2pxv5DO7CLEJyTHx3Pg043CDQkv1G4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-gnu": {
+ "version": "16.3.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.0.tgz",
+ "integrity": "sha512-pjGxK5EY7yWml78ALejFkWmgHsU7wbFQrISiugpH6FbUJhgEvw3xFZ/EBAtLl7QtL0WdQKiG9eWJ3mOKGTukHw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-musl": {
+ "version": "16.3.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.0.tgz",
+ "integrity": "sha512-sjo++Xx+lomlPs3HRsHWhVDyGG6ms1kGW5EtHLERdII8AyG1i+f6aq68xHREO6AEMlhjTNEWBSmfJfqm9orf7g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "16.3.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.0.tgz",
+ "integrity": "sha512-C5JSgiO54wURdaxdEUIXqkz04uMqC9UmPX1gtDrV/5Tf1UowdWYI8uA5hfFbPolTlp0q4KZ60xlHePNibf0VIw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-x64-msvc": {
+ "version": "16.3.0",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.0.tgz",
+ "integrity": "sha512-fDOggsweNb5SSw0ZKVk6U+gxSyGFFlIBY/LBc1r8GUj4u/6t6oArL+Pmkg0MBnsgR+KkdsURilVH4F3GXUGepA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.15",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
+ "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "25.3.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz",
+ "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.14",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.11.14",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz",
+ "integrity": "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==",
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001809",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz",
+ "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/client-only": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/next": {
+ "version": "16.3.0",
+ "resolved": "https://registry.npmjs.org/next/-/next-16.3.0.tgz",
+ "integrity": "sha512-NEdGOzH+08eTXMUp9UYkA99Nhi5N6Thrhc1jgFOQgfgnGK/dA2hRwBpXep+exdFQrnwlRf/3Wixyp8lLBUpE2A==",
+ "license": "MIT",
+ "dependencies": {
+ "@next/env": "16.3.0",
+ "@swc/helpers": "0.5.15",
+ "baseline-browser-mapping": "^2.9.19",
+ "caniuse-lite": "^1.0.30001579",
+ "postcss": "8.5.23",
+ "styled-jsx": "5.1.6"
+ },
+ "bin": {
+ "next": "dist/bin/next"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "optionalDependencies": {
+ "@next/swc-darwin-arm64": "16.3.0",
+ "@next/swc-darwin-x64": "16.3.0",
+ "@next/swc-linux-arm64-gnu": "16.3.0",
+ "@next/swc-linux-arm64-musl": "16.3.0",
+ "@next/swc-linux-x64-gnu": "16.3.0",
+ "@next/swc-linux-x64-musl": "16.3.0",
+ "@next/swc-win32-arm64-msvc": "16.3.0",
+ "@next/swc-win32-x64-msvc": "16.3.0",
+ "sharp": "^0.35.3"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0",
+ "@playwright/test": "^1.51.1",
+ "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_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.23",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
+ "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.16",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
+ "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
+ "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.4"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "license": "ISC",
+ "optional": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sharp": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz",
+ "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@img/colour": "^1.1.0",
+ "detect-libc": "^2.1.2",
+ "semver": "^7.8.5"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.35.3",
+ "@img/sharp-darwin-x64": "0.35.3",
+ "@img/sharp-freebsd-wasm32": "0.35.3",
+ "@img/sharp-libvips-darwin-arm64": "1.3.2",
+ "@img/sharp-libvips-darwin-x64": "1.3.2",
+ "@img/sharp-libvips-linux-arm": "1.3.2",
+ "@img/sharp-libvips-linux-arm64": "1.3.2",
+ "@img/sharp-libvips-linux-ppc64": "1.3.2",
+ "@img/sharp-libvips-linux-riscv64": "1.3.2",
+ "@img/sharp-libvips-linux-s390x": "1.3.2",
+ "@img/sharp-libvips-linux-x64": "1.3.2",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.2",
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.2",
+ "@img/sharp-linux-arm": "0.35.3",
+ "@img/sharp-linux-arm64": "0.35.3",
+ "@img/sharp-linux-ppc64": "0.35.3",
+ "@img/sharp-linux-riscv64": "0.35.3",
+ "@img/sharp-linux-s390x": "0.35.3",
+ "@img/sharp-linux-x64": "0.35.3",
+ "@img/sharp-linuxmusl-arm64": "0.35.3",
+ "@img/sharp-linuxmusl-x64": "0.35.3",
+ "@img/sharp-webcontainers-wasm32": "0.35.3",
+ "@img/sharp-win32-arm64": "0.35.3",
+ "@img/sharp-win32-ia32": "0.35.3",
+ "@img/sharp-win32-x64": "0.35.3"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/styled-jsx": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
+ "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
+ "license": "MIT",
+ "dependencies": {
+ "client-only": "0.0.1"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "dev": true,
+ "license": "MIT"
+ }
+ }
+}
diff --git a/tests/fixtures/next-app-route/package.json b/tests/fixtures/next-app-route/package.json
new file mode 100644
index 0000000000..c64de44187
--- /dev/null
+++ b/tests/fixtures/next-app-route/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "perry-next-app-route-fixture",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "build": "next build --webpack",
+ "start": "next start",
+ "verify": "node verify.mjs"
+ },
+ "dependencies": {
+ "next": "16.3.0",
+ "react": "19.2.4",
+ "react-dom": "19.2.4"
+ },
+ "devDependencies": {
+ "@types/node": "25.3.3",
+ "@types/react": "19.2.14",
+ "@types/react-dom": "19.2.3",
+ "typescript": "5.9.3"
+ }
+}
diff --git a/tests/fixtures/next-app-route/perry-host.js b/tests/fixtures/next-app-route/perry-host.js
new file mode 100644
index 0000000000..3e3330c264
--- /dev/null
+++ b/tests/fixtures/next-app-route/perry-host.js
@@ -0,0 +1,45 @@
+const { createServer } = require("node:http");
+const {
+ handler,
+ routeModule,
+} = require("./.next/server/app/api/benchmark/route.js");
+
+if (typeof routeModule.handle !== "function" || typeof handler !== "function") {
+ throw new Error("production App Route handler exports are missing");
+}
+
+const enteredRequestIds = new Set();
+const routeModuleHandle = routeModule.handle.bind(routeModule);
+routeModule.handle = async (request, context) => {
+ enteredRequestIds.add(request.nextUrl.searchParams.get("id") ?? "missing");
+ return routeModuleHandle(request, context);
+};
+
+const pending = new Set();
+const port = Number(process.env.PORT ?? "3100");
+const hostname = process.env.HOSTNAME ?? "127.0.0.1";
+
+const server = createServer((request, response) => {
+ const work = handler(request, response, {
+ waitUntil(promise) {
+ pending.add(promise);
+ promise.finally(() => pending.delete(promise));
+ },
+ });
+ work
+ .then(() => {
+ const id = new URL(request.url, `http://${hostname}:${port}`).searchParams.get("id") ?? "missing";
+ if (!enteredRequestIds.delete(id)) {
+ throw new Error(`${id}: generated handler bypassed routeModule.handle`);
+ }
+ })
+ .catch((error) => {
+ console.error(error);
+ if (!response.headersSent) response.statusCode = 500;
+ response.end();
+ });
+});
+
+server.listen(port, hostname, () => {
+ console.log(`PERRY_NEXT_APP_ROUTE_READY http://${hostname}:${port}`);
+});
diff --git a/tests/fixtures/next-app-route/provider-host.rs b/tests/fixtures/next-app-route/provider-host.rs
new file mode 100644
index 0000000000..75acdd8808
--- /dev/null
+++ b/tests/fixtures/next-app-route/provider-host.rs
@@ -0,0 +1,96 @@
+use std::ffi::{c_char, c_int, c_void, CString};
+
+const RTLD_NOW: c_int = 2;
+#[cfg(target_os = "macos")]
+const RTLD_GLOBAL: c_int = 8;
+#[cfg(target_os = "linux")]
+const RTLD_GLOBAL: c_int = 0x100;
+#[cfg(target_os = "macos")]
+const RTLD_LOCAL: c_int = 4;
+#[cfg(target_os = "linux")]
+const RTLD_LOCAL: c_int = 0;
+
+unsafe extern "C" {
+ fn dlopen(path: *const c_char, mode: c_int) -> *mut c_void;
+ fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
+ fn dlerror() -> *const c_char;
+}
+
+type VoidFn = unsafe extern "C" fn();
+type TickFn = unsafe extern "C" fn() -> i32;
+type ProbeFn = unsafe extern "C" fn() -> usize;
+
+fn loader_error(context: &str) -> String {
+ let message = unsafe {
+ let error = dlerror();
+ if error.is_null() {
+ "unknown loader error".to_string()
+ } else {
+ std::ffi::CStr::from_ptr(error)
+ .to_string_lossy()
+ .into_owned()
+ }
+ };
+ format!("{context}: {message}")
+}
+
+fn open(path: &str, mode: c_int) -> Result<*mut c_void, String> {
+ let path = CString::new(path).map_err(|_| "library path contains NUL".to_string())?;
+ let handle = unsafe { dlopen(path.as_ptr(), mode) };
+ if handle.is_null() {
+ Err(loader_error("dlopen failed"))
+ } else {
+ Ok(handle)
+ }
+}
+
+unsafe fn symbol(handle: *mut c_void, name: &str) -> Result {
+ let name = CString::new(name).map_err(|_| "symbol name contains NUL".to_string())?;
+ let address = unsafe { dlsym(handle, name.as_ptr()) };
+ if address.is_null() {
+ return Err(loader_error("dlsym failed"));
+ }
+ Ok(unsafe { std::mem::transmute_copy(&address) })
+}
+
+fn main() -> Result<(), String> {
+ let arguments: Vec = std::env::args().collect();
+ if arguments.len() != 4 {
+ return Err("usage: provider-host runtime stdlib app".into());
+ }
+
+ let runtime = open(&arguments[1], RTLD_NOW | RTLD_GLOBAL)?;
+ let stdlib = open(&arguments[2], RTLD_NOW | RTLD_GLOBAL)?;
+ let app = open(&arguments[3], RTLD_NOW | RTLD_LOCAL)?;
+
+ let gc_init: VoidFn = unsafe { symbol(runtime, "js_gc_init")? };
+ let provider_probe: ProbeFn =
+ unsafe { symbol(stdlib, "next_app_route_provider_runtime_probe")? };
+ if unsafe { provider_probe() } != gc_init as usize {
+ return Err("stdlib provider is bound to a different runtime image".into());
+ }
+
+ let module_init: VoidFn = unsafe { symbol(app, "perry_module_init")? };
+ let run_microtasks: TickFn =
+ unsafe { symbol(runtime, "js_promise_run_microtasks_event_loop")? };
+ let timer_tick: TickFn = unsafe { symbol(runtime, "js_timer_tick")? };
+ let callback_timer_tick: TickFn = unsafe { symbol(runtime, "js_callback_timer_tick")? };
+ let interval_timer_tick: TickFn = unsafe { symbol(runtime, "js_interval_timer_tick")? };
+ let run_stdlib_pump: VoidFn = unsafe { symbol(runtime, "js_run_stdlib_pump")? };
+ let wait_for_event: VoidFn = unsafe { symbol(runtime, "js_wait_for_event")? };
+
+ unsafe {
+ gc_init();
+ module_init();
+ }
+ loop {
+ unsafe {
+ run_microtasks();
+ timer_tick();
+ callback_timer_tick();
+ interval_timer_tick();
+ run_stdlib_pump();
+ wait_for_event();
+ }
+ }
+}
diff --git a/tests/fixtures/next-app-route/provider-linker.sh b/tests/fixtures/next-app-route/provider-linker.sh
new file mode 100755
index 0000000000..0a8e3726f8
--- /dev/null
+++ b/tests/fixtures/next-app-route/provider-linker.sh
@@ -0,0 +1,126 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+runtime_library=${PERRY_NEXT_RUNTIME_LIBRARY:?set the runtime provider path}
+required_symbols=${PERRY_NEXT_REQUIRED_SYMBOLS:?set the app undefined-symbol list}
+real_cc=${PERRY_NEXT_REAL_CC:-/usr/bin/cc}
+host_os=$(uname -s)
+original_arguments=("$@")
+arguments=()
+rlibs=()
+original_export_list=""
+skip_export_list_value=false
+saw_runtime_rlib=false
+is_runtime_dylib=false
+
+for argument in "$@"; do
+ if [[ "$skip_export_list_value" == true ]]; then
+ original_export_list=${argument#-Wl,}
+ skip_export_list_value=false
+ continue
+ fi
+ case "$argument" in
+ */libperry_runtime.dylib)
+ is_runtime_dylib=true
+ arguments+=("$argument")
+ ;;
+ */libperry_runtime.rlib|*libperry_runtime-*.rlib)
+ saw_runtime_rlib=true
+ if [[ "$host_os" == Linux ]]; then
+ arguments+=(
+ '-Wl,-Bdynamic' '-Wl,--no-as-needed' "$runtime_library"
+ '-Wl,--as-needed' '-Wl,-Bstatic'
+ )
+ else
+ arguments+=("$runtime_library")
+ fi
+ ;;
+ *.rlib)
+ rlibs+=("$argument")
+ arguments+=("$argument")
+ ;;
+ -Wl,-exported_symbols_list)
+ skip_export_list_value=true
+ ;;
+ -Wl,-exported_symbols_list,*)
+ original_export_list=${argument#-Wl,-exported_symbols_list,}
+ ;;
+ -Wl,--version-script=*)
+ # Replaced below with a script that also exports selected dependency ABI.
+ ;;
+ *) arguments+=("$argument") ;;
+ esac
+done
+
+if [[ "$saw_runtime_rlib" != true ]]; then
+ if [[ "$host_os" == Darwin && "$is_runtime_dylib" == true ]]; then
+ exec "$real_cc" "${original_arguments[@]}" \
+ -framework CoreFoundation -framework Foundation
+ fi
+ exec "$real_cc" "${original_arguments[@]}"
+fi
+
+if [[ "$host_os" == Darwin ]]; then
+ # The provider records this install name while linking. The gate copies
+ # both images beside one another before loading them.
+ install_name_tool -id '@rpath/libperry_runtime.dylib' "$runtime_library"
+fi
+
+scratch=$(mktemp -d "${TMPDIR:-/tmp}/perry-next-provider-link.XXXXXX")
+cleanup() {
+ rm -rf "$scratch"
+}
+trap cleanup EXIT
+
+defined="$scratch/defined"
+selected="$scratch/selected"
+: >"$defined"
+for rlib in "${rlibs[@]}"; do
+ if [[ "$host_os" == Darwin ]]; then
+ nm -gU "$rlib" 2>/dev/null | awk 'NF >= 3 { print $3 }' >>"$defined" || true
+ else
+ nm -g --defined-only "$rlib" 2>/dev/null | awk 'NF >= 3 { print $3 }' >>"$defined" || true
+ fi
+done
+sort -u "$defined" -o "$defined"
+sort -u "$required_symbols" | comm -12 - "$defined" >"$selected"
+
+if [[ ! -s "$selected" ]]; then
+ echo "provider link selected no app ABI symbols" >&2
+ exit 1
+fi
+
+if [[ "$host_os" == Darwin ]]; then
+ exports="$scratch/exports"
+ {
+ if [[ -n "$original_export_list" ]]; then
+ sed -n '/next_app_route_provider_runtime_probe/p' "$original_export_list"
+ else
+ echo '_next_app_route_provider_runtime_probe'
+ fi
+ cat "$selected"
+ } | sort -u >"$exports"
+ while IFS= read -r symbol; do
+ arguments+=("-Wl,-u,$symbol")
+ done <"$selected"
+ arguments+=(
+ '-Wl,-exported_symbols_list' "-Wl,$exports"
+ '-Wl,-rpath,@loader_path' '-Wl,-flat_namespace' '-Wl,-interposable'
+ )
+else
+ version_script="$scratch/exports.map"
+ {
+ echo '{ global:'
+ echo 'next_app_route_provider_runtime_probe;'
+ sed 's/^/ /; s/$/;/' "$selected"
+ echo 'local: *; };'
+ } >"$version_script"
+ while IFS= read -r symbol; do
+ arguments+=("-Wl,--undefined=$symbol")
+ done <"$selected"
+ arguments+=("-Wl,--version-script=$version_script")
+ # shellcheck disable=SC2016 # $ORIGIN must reach the ELF linker literally.
+ arguments+=('-Wl,-rpath,$ORIGIN' '-Wl,-soname,libperry_stdlib.so')
+fi
+
+"$real_cc" "${arguments[@]}"
diff --git a/tests/fixtures/next-app-route/provider/Cargo.toml b/tests/fixtures/next-app-route/provider/Cargo.toml
new file mode 100644
index 0000000000..eab2018231
--- /dev/null
+++ b/tests/fixtures/next-app-route/provider/Cargo.toml
@@ -0,0 +1,46 @@
+[package]
+name = "next-app-route-provider"
+version = "0.0.0"
+edition = "2021"
+publish = false
+
+[lib]
+name = "next_app_route_provider"
+crate-type = ["cdylib"]
+
+[dependencies]
+perry-stdlib = { path = "../../../../crates/perry-stdlib", default-features = false, features = [
+ "async-runtime",
+ "bundled-events",
+ "bundled-streams",
+ "compression",
+ "crypto",
+ "external-http-client-pump",
+ "external-http-server-pump",
+ "external-net-pump",
+ "external-ws-pump",
+ "http-client",
+ "http-server",
+ "ids",
+] }
+perry-ext-http = { path = "../../../../crates/perry-ext-http" }
+
+# #7302's bug class: Perry's invoke/landingpad exception transport requires any
+# workspace that builds a runtime archive to be `panic = "abort"`. Under
+# `unwind`, rustc plants RFC-2945 abort-on-unwind guards in every `extern "C"`
+# fn with an interior Rust call — `js_throw` included — so a JS throw aborts the
+# process below `_js_throw` with a handler already armed. `[profile.provider]`
+# alone is not enough: `release` would still take cargo's default here, and an
+# explicit override beats `inherits`.
+[profile.release]
+panic = "abort"
+
+[profile.provider]
+inherits = "release"
+opt-level = 2
+lto = false
+codegen-units = 16
+panic = "abort"
+strip = false
+
+[workspace]
diff --git a/tests/fixtures/next-app-route/provider/src/lib.rs b/tests/fixtures/next-app-route/provider/src/lib.rs
new file mode 100644
index 0000000000..84852b3eaa
--- /dev/null
+++ b/tests/fixtures/next-app-route/provider/src/lib.rs
@@ -0,0 +1,25 @@
+//! Provider image for the production Next App Route dylib gate.
+//!
+//! The final-link driver retains and exports the exact Perry ABI symbols the
+//! compiled app leaves undefined. These anchors ensure Cargo places the stdlib
+//! and HTTP wrapper rlibs on that final link in one coherent tokio build.
+
+extern crate perry_ext_http;
+extern crate perry_stdlib;
+
+unsafe extern "C" {
+ fn js_gc_init();
+}
+
+#[used]
+static PIN_STDLIB: extern "C" fn() -> i32 = perry_stdlib::common::js_stdlib_process_pending;
+
+#[used]
+static PIN_HTTP: unsafe extern "C" fn() -> i32 = perry_ext_http::js_http_process_pending;
+
+/// Proves that this provider binds stateful runtime calls to the runtime image
+/// loaded by the host instead of satisfying them from a private runtime copy.
+#[no_mangle]
+pub extern "C" fn next_app_route_provider_runtime_probe() -> usize {
+ js_gc_init as *const () as usize
+}
diff --git a/tests/fixtures/next-app-route/tsconfig.json b/tests/fixtures/next-app-route/tsconfig.json
new file mode 100644
index 0000000000..1e3591c320
--- /dev/null
+++ b/tests/fixtures/next-app-route/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["dom", "dom.iterable", "es2022"],
+ "strict": true,
+ "noEmit": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "plugins": [{ "name": "next" }],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "incremental": true,
+ "esModuleInterop": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true
+ },
+ "include": [
+ "next-env.d.ts",
+ ".next/types/**/*.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/dev/types/**/*.ts"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/tests/fixtures/next-app-route/verify.mjs b/tests/fixtures/next-app-route/verify.mjs
new file mode 100644
index 0000000000..6506cc778e
--- /dev/null
+++ b/tests/fixtures/next-app-route/verify.mjs
@@ -0,0 +1,48 @@
+const base = process.env.BASE_URL ?? "http://127.0.0.1:3100";
+
+function checksum(iterations) {
+ let value = 0x811c9dc5;
+ for (let index = 0; index < iterations; index += 1) {
+ value = Math.imul(value ^ index, 0x01000193) >>> 0;
+ }
+ return value;
+}
+
+async function verify(id, iterations, method = "GET", requestBody = "") {
+ const response = await fetch(
+ `${base}/api/benchmark?id=${encodeURIComponent(id)}&iterations=${iterations}`,
+ {
+ method,
+ headers: {
+ "x-request-id": id,
+ ...(method === "POST" ? { "content-type": "text/plain" } : {}),
+ },
+ ...(method === "POST" ? { body: requestBody } : {}),
+ },
+ );
+ const body = await response.json();
+ const cookie = response.headers.get("set-cookie") ?? "";
+ const expected = {
+ runtime: "next",
+ method,
+ pathname: "/api/benchmark",
+ id,
+ iterations,
+ checksum: checksum(iterations),
+ beforeAwait: id,
+ afterAwait: id,
+ requestBody,
+ };
+ if (response.status !== 207) throw new Error(`${id}: status ${response.status}`);
+ if (response.headers.get("x-perry-repro") !== id) throw new Error(`${id}: response header lost`);
+ if (!cookie.includes(`perry_ctx=${id}`)) throw new Error(`${id}: response cookie lost`);
+ if (JSON.stringify(body) !== JSON.stringify(expected)) {
+ throw new Error(`${id}: ${JSON.stringify(body)} != ${JSON.stringify(expected)}`);
+ }
+}
+
+await Promise.all(
+ Array.from({ length: 20 }, (_, index) => verify(`request-${index}`, index + 1)),
+);
+await verify("post-request", 31, "POST", "perry-request-body");
+console.log("PASS: 21 production App Route requests");
diff --git a/tests/test_next_app_route_dylib.sh b/tests/test_next_app_route_dylib.sh
new file mode 100755
index 0000000000..7661606f8a
--- /dev/null
+++ b/tests/test_next_app_route_dylib.sh
@@ -0,0 +1,324 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# #8037: run the exact #8034 production route through Next's generated
+# AppRouteRouteModule.handle while Perry is embedded as an app-only dylib.
+# Runtime and stdlib/HTTP are separate, eagerly relocated provider images.
+
+repo_root=$(cd "$(dirname "$0")/.." && pwd)
+fixture="$repo_root/tests/fixtures/next-app-route"
+profile=${PERRY_NEXT_PROFILE:-perry-dev}
+target_dir=${CARGO_TARGET_DIR:-$repo_root/target}
+perry=${PERRY_BIN:-$target_dir/$profile/perry}
+port=${PERRY_NEXT_PORT:-3100}
+cold_starts=${PERRY_NEXT_COLD_STARTS:-10}
+verifications_per_start=${PERRY_NEXT_VERIFICATIONS_PER_START:-10}
+cargo_jobs=${PERRY_NEXT_CARGO_JOBS:-2}
+real_cc=$(command -v cc)
+host_os=$(uname -s)
+host_arch=$(uname -m)
+forbidden_diagnostics='generated handler bypassed|\[perry-gc\].*SKIPPED|unsettled.await|unimplemented|compatibility.fallback'
+
+find_llvm22_clang() {
+ local candidate
+ local -a candidates=()
+ if [[ -n ${PERRY_LLVM_CLANG:-} ]]; then
+ candidates+=("$PERRY_LLVM_CLANG")
+ fi
+ if [[ -n ${LLVM_SYS_221_PREFIX:-} ]]; then
+ candidates+=("$LLVM_SYS_221_PREFIX/bin/clang")
+ fi
+ if [[ "$host_os" == Darwin ]]; then
+ candidates+=(
+ /opt/homebrew/opt/llvm@22/bin/clang
+ /opt/homebrew/opt/llvm/bin/clang
+ /usr/local/opt/llvm@22/bin/clang
+ /usr/local/opt/llvm/bin/clang
+ )
+ fi
+ candidates+=(clang-22 clang)
+
+ for candidate in "${candidates[@]}"; do
+ if [[ "$candidate" != */* ]]; then
+ candidate=$(command -v "$candidate" 2>/dev/null || true)
+ fi
+ if [[ -x "$candidate" ]] && "$candidate" --version 2>/dev/null | head -n 1 | grep -Eq 'version 22\.'; then
+ printf '%s\n' "$candidate"
+ return 0
+ fi
+ done
+ return 1
+}
+
+find_llvm22_opt() {
+ local candidate
+ local -a candidates=()
+ if [[ -n ${PERRY_LLVM_OPT:-} ]]; then
+ candidates+=("$PERRY_LLVM_OPT")
+ fi
+ if [[ -n ${LLVM_SYS_221_PREFIX:-} ]]; then
+ candidates+=("$LLVM_SYS_221_PREFIX/bin/opt")
+ fi
+ candidates+=("$(dirname "$llvm_clang")/opt" opt-22 opt)
+
+ for candidate in "${candidates[@]}"; do
+ if [[ "$candidate" != */* ]]; then
+ candidate=$(command -v "$candidate" 2>/dev/null || true)
+ fi
+ if [[ -x "$candidate" ]] && "$candidate" --version 2>/dev/null | head -n 1 | grep -Eq 'LLVM version 22\.'; then
+ printf '%s\n' "$candidate"
+ return 0
+ fi
+ done
+ return 1
+}
+
+case "$host_os/$host_arch" in
+ Darwin/arm64|Darwin/x86_64)
+ library_extension=dylib
+ runtime_filename=libperry_runtime.dylib
+ stdlib_filename=libperry_stdlib.dylib
+ if [[ "$host_arch" == arm64 ]]; then
+ cargo_linker_env=CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER
+ else
+ cargo_linker_env=CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER
+ fi
+ ;;
+ Linux/aarch64|Linux/x86_64)
+ library_extension=so
+ runtime_filename=libperry_runtime.so
+ stdlib_filename=libperry_stdlib.so
+ if [[ "$host_arch" == aarch64 ]]; then
+ cargo_linker_env=CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER
+ else
+ cargo_linker_env=CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER
+ fi
+ ;;
+ *)
+ echo "SKIP: Next App Route dylib gate does not support $host_os/$host_arch"
+ exit 0
+ ;;
+esac
+
+for value in "$cold_starts" "$verifications_per_start" "$port" "$cargo_jobs"; do
+ if [[ ! "$value" =~ ^[1-9][0-9]*$ ]]; then
+ echo "cold starts, verifications, port, and cargo jobs must be positive integers" >&2
+ exit 1
+ fi
+done
+if [[ ! -x "$perry" ]]; then
+ echo "Perry compiler is missing: $perry" >&2
+ echo "Build it with: cargo build --profile $profile -p perry" >&2
+ exit 1
+fi
+if ! llvm_clang=$(find_llvm22_clang); then
+ echo "LLVM 22 clang is required for Perry's LLVM 22 textual IR" >&2
+ echo "Set PERRY_LLVM_CLANG to the matching clang binary" >&2
+ exit 1
+fi
+if ! llvm_opt=$(find_llvm22_opt); then
+ echo "LLVM 22 opt is required alongside Perry's LLVM 22 clang" >&2
+ echo "Set PERRY_LLVM_OPT to the matching opt binary" >&2
+ exit 1
+fi
+commit=$(git -C "$repo_root" rev-parse HEAD)
+next_version=$(node -p "require('$fixture/package-lock.json').packages['node_modules/next'].version")
+echo "Perry commit: $commit"
+echo "Next version: $next_version"
+echo "Compile mode: app-only dylib; unified runtime and stdlib/HTTP provider graph"
+echo "LLVM compiler: $($llvm_clang --version | head -n 1)"
+echo "LLVM optimizer: $($llvm_opt --version | head -n 1)"
+
+scratch=$(mktemp -d "${TMPDIR:-/tmp}/perry-next-app-route.XXXXXX")
+runtime_source="$scratch/runtime-source"
+provider_target_dir=${PERRY_NEXT_PROVIDER_TARGET_DIR:-$scratch/provider-target}
+providers="$scratch/providers"
+host_pid=""
+worktree_added=false
+cleanup() {
+ if [[ -n "$host_pid" ]] && kill -0 "$host_pid" 2>/dev/null; then
+ kill "$host_pid" 2>/dev/null || true
+ wait "$host_pid" 2>/dev/null || true
+ fi
+ if [[ "$worktree_added" == true ]]; then
+ git -C "$repo_root" worktree remove --force "$runtime_source" >/dev/null 2>&1 || true
+ fi
+ rm -rf "$scratch"
+}
+trap cleanup EXIT INT TERM
+mkdir -p "$provider_target_dir" "$providers"
+
+if [[ ${PERRY_NEXT_SKIP_NODE_ORACLE:-0} != 1 ]]; then
+ PERRY_NEXT_ORACLE_PORT=$((port + 1)) "$repo_root/tests/test_next_app_route_node_oracle.sh"
+elif [[ ! -f "$fixture/.next/server/app/api/benchmark/route.js" ]]; then
+ echo "PERRY_NEXT_SKIP_NODE_ORACLE=1 requires an existing fixture build" >&2
+ exit 1
+fi
+
+route_bundle="$fixture/.next/server/app/api/benchmark/route.js"
+node - "$route_bundle" <<'NODE'
+const generated = require(require("node:path").resolve(process.argv[2]));
+if (typeof generated.routeModule?.handle !== "function") {
+ throw new Error("production routeModule.handle is missing");
+}
+if (generated.routeModule?.definition?.pathname !== "/api/benchmark") {
+ throw new Error("production routeModule has the wrong pathname");
+}
+NODE
+
+app="$providers/next-app-route.$library_extension"
+compile_log="$scratch/compile.log"
+(
+ cd "$fixture"
+ env \
+ PERRY_NO_AUTO_OPTIMIZE=1 \
+ PERRY_DISABLE_WELL_KNOWN=1 \
+ PERRY_LLVM_INPROCESS="${PERRY_LLVM_INPROCESS:-0}" \
+ PERRY_LLVM_CLANG="$llvm_clang" \
+ PERRY_LLVM_OPT="$llvm_opt" \
+ PERRY_LL_SIZE_OPT="${PERRY_LL_SIZE_OPT:-0}" \
+ PERRY_CODEGEN_UNIT_BYTES="${PERRY_CODEGEN_UNIT_BYTES:-8388608}" \
+ PERRY_MODULE_JOBS="${PERRY_MODULE_JOBS:-1}" \
+ PERRY_CODEGEN_UNIT_JOBS="${PERRY_CODEGEN_UNIT_JOBS:-1}" \
+ PERRY_RUNTIME_DIR="$target_dir/$profile" \
+ "$perry" compile --no-auto-optimize --output-type dylib \
+ -o "$app" perry-host.js
+) 2>&1 | tee "$compile_log"
+[[ -f "$app" ]] || { echo "Perry did not emit the app dylib" >&2; exit 1; }
+if grep -Eiq "$forbidden_diagnostics" "$compile_log"; then
+ echo "forbidden Perry diagnostic during app compilation" >&2
+ grep -Ein "$forbidden_diagnostics" "$compile_log" | sed -n '1,120p' >&2
+ exit 1
+fi
+
+required_symbols="$scratch/app-required-symbols"
+if [[ "$host_os" == Darwin ]]; then
+ nm -u "$app" \
+ | awk 'NF >= 1 && $NF ~ /^_(js_|perry_)/ { print $NF }' \
+ | sort -u >"$required_symbols"
+else
+ nm -D --undefined-only "$app" \
+ | awk 'NF >= 1 && $NF ~ /^(js_|perry_)/ { print $NF }' \
+ | sort -u >"$required_symbols"
+fi
+if [[ ! -s "$required_symbols" ]]; then
+ echo "app dylib has no undefined Perry ABI symbols" >&2
+ exit 1
+fi
+
+git -C "$repo_root" worktree add --detach "$runtime_source" HEAD >/dev/null
+worktree_added=true
+runtime_manifest="$runtime_source/crates/perry-runtime/Cargo.toml"
+perl -0pi -e 's/crate-type = \["rlib"\]/crate-type = ["rlib", "dylib"]/ or die "runtime crate-type marker missing\n"' "$runtime_manifest"
+
+# Build the runtime dylib and the stdlib/HTTP provider from one resolved Cargo
+# graph. Rust's internal symbols include a crate disambiguator, so independently
+# resolved provider and runtime builds are not interchangeable even when their
+# public C ABI is identical.
+provider_source="$runtime_source/crates/next-app-route-provider"
+cp -R "$fixture/provider" "$provider_source"
+perl -0pi -e '
+ s#\.\./\.\./\.\./\.\./crates/perry-stdlib#../perry-stdlib#g;
+ s#\.\./\.\./\.\./\.\./crates/perry-ext-http#../perry-ext-http#g;
+ s/\n\[profile\.provider\].*?\n\[workspace\]\n/\n/s
+ or die "provider workspace/profile markers missing\n";
+' "$provider_source/Cargo.toml"
+perl -0pi -e 's/("crates\/perry-runtime",\n)/$1 "crates\/next-app-route-provider",\n/ or die "workspace member marker missing\n"' \
+ "$runtime_source/Cargo.toml"
+
+runtime_build_library="$provider_target_dir/$profile/libperry_runtime.$library_extension"
+provider_linker="$fixture/provider-linker.sh"
+env \
+ CARGO_TARGET_DIR="$provider_target_dir" \
+ PERRY_NEXT_RUNTIME_LIBRARY="$runtime_build_library" \
+ PERRY_NEXT_REQUIRED_SYMBOLS="$required_symbols" \
+ PERRY_NEXT_REAL_CC="$real_cc" \
+ "$cargo_linker_env=$provider_linker" \
+ cargo build --manifest-path "$runtime_source/Cargo.toml" \
+ --profile "$profile" --jobs "$cargo_jobs" \
+ -p perry-runtime -p next-app-route-provider
+
+runtime_library="$providers/$runtime_filename"
+cp "$runtime_build_library" "$runtime_library"
+stdlib_library="$providers/$stdlib_filename"
+cp "$provider_target_dir/$profile/libnext_app_route_provider.$library_extension" "$stdlib_library"
+if [[ "$host_os" == Darwin ]]; then
+ install_name_tool -id '@rpath/libperry_runtime.dylib' "$runtime_library"
+ install_name_tool -id '@rpath/libperry_stdlib.dylib' "$stdlib_library"
+ available_symbols="$scratch/available-symbols"
+ {
+ nm -gU "$runtime_library" | awk 'NF >= 3 { print $3 }'
+ nm -gU "$stdlib_library" | awk 'NF >= 3 { print $3 }'
+ } | sort -u >"$available_symbols"
+else
+ available_symbols="$scratch/available-symbols"
+ {
+ nm -D --defined-only "$runtime_library" | awk 'NF >= 3 { print $3 }'
+ nm -D --defined-only "$stdlib_library" | awk 'NF >= 3 { print $3 }'
+ } | sort -u >"$available_symbols"
+fi
+missing_symbols="$scratch/missing-symbols"
+comm -23 "$required_symbols" "$available_symbols" >"$missing_symbols"
+if [[ -s "$missing_symbols" ]]; then
+ echo "provider images do not satisfy the app ABI:" >&2
+ sed -n '1,120p' "$missing_symbols" >&2
+ exit 1
+fi
+
+host="$scratch/provider-host"
+rustc --edition 2021 -O "$fixture/provider-host.rs" -o "$host"
+provider_abi=$(shasum -a 256 "$available_symbols" | awk '{print $1}')
+echo "Provider ABI hash: $provider_abi"
+
+for cold_start in $(seq 1 "$cold_starts"); do
+ host_log="$scratch/host-$cold_start.log"
+ (
+ # Next's generated webpack runtime resolves `./chunks/*.js` from the
+ # production server root when it loads an on-demand route chunk.
+ cd "$fixture/.next/server"
+ if [[ "$host_os" == Darwin ]]; then
+ PORT="$port" HOSTNAME=127.0.0.1 DYLD_LIBRARY_PATH="$providers" \
+ "$host" "$runtime_library" "$stdlib_library" "$app"
+ else
+ PORT="$port" HOSTNAME=127.0.0.1 LD_LIBRARY_PATH="$providers" \
+ "$host" "$runtime_library" "$stdlib_library" "$app"
+ fi
+ ) >"$host_log" 2>&1 &
+ host_pid=$!
+
+ ready=false
+ for _ in $(seq 1 240); do
+ if curl --fail --silent --output /dev/null \
+ "http://127.0.0.1:$port/api/benchmark?id=ready&iterations=1"; then
+ ready=true
+ break
+ fi
+ if ! kill -0 "$host_pid" 2>/dev/null; then
+ echo "provider host exited during cold start $cold_start" >&2
+ sed -n '1,240p' "$host_log" >&2
+ exit 1
+ fi
+ sleep 0.25
+ done
+ if [[ "$ready" != true ]]; then
+ echo "provider host was not ready during cold start $cold_start" >&2
+ sed -n '1,240p' "$host_log" >&2
+ exit 1
+ fi
+
+ for verification in $(seq 1 "$verifications_per_start"); do
+ BASE_URL="http://127.0.0.1:$port" node "$fixture/verify.mjs"
+ echo "PASS: cold start $cold_start/$cold_starts, verifier $verification/$verifications_per_start"
+ done
+ kill "$host_pid" 2>/dev/null || true
+ wait "$host_pid" 2>/dev/null || true
+ host_pid=""
+ if grep -Eiq "$forbidden_diagnostics" "$host_log"; then
+ echo "forbidden Perry diagnostic during cold start $cold_start" >&2
+ sed -n '1,240p' "$host_log" >&2
+ exit 1
+ fi
+done
+
+total=$((cold_starts * verifications_per_start))
+echo "PASS: $total production App Route verifier repetitions through app-only dylib providers"
diff --git a/tests/test_next_app_route_node_oracle.sh b/tests/test_next_app_route_node_oracle.sh
new file mode 100755
index 0000000000..9abad00837
--- /dev/null
+++ b/tests/test_next_app_route_node_oracle.sh
@@ -0,0 +1,89 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+FIXTURE="$REPO_ROOT/tests/fixtures/next-app-route"
+PORT="${PERRY_NEXT_ORACLE_PORT:-3100}"
+WORK="$(mktemp -d)"
+SERVER_PID=""
+
+cleanup() {
+ if [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then
+ kill "$SERVER_PID" 2>/dev/null || true
+ wait "$SERVER_PID" 2>/dev/null || true
+ fi
+ rm -rf "$WORK"
+}
+trap cleanup EXIT INT TERM
+
+node - "$FIXTURE/package-lock.json" <<'NODE'
+const lock = require(process.argv[2]);
+const expected = {
+ "node_modules/next": "16.3.0",
+ "node_modules/react": "19.2.4",
+ "node_modules/react-dom": "19.2.4",
+};
+for (const [entry, version] of Object.entries(expected)) {
+ const actual = lock.packages?.[entry]?.version;
+ if (actual !== version) {
+ throw new Error(`${entry}: lockfile has ${actual}, expected ${version}`);
+ }
+}
+console.log("PASS: pinned Next 16.3.0 / React 19.2.4 lockfile");
+NODE
+
+(
+ cd "$FIXTURE"
+ npm ci
+ npm run build
+)
+
+ROUTE_BUNDLE="$FIXTURE/.next/server/app/api/benchmark/route.js"
+node - "$ROUTE_BUNDLE" <<'NODE'
+const routePath = require("node:path").resolve(process.argv[2]);
+const generated = require(routePath);
+if (typeof generated.routeModule?.handle !== "function") {
+ throw new Error("generated production bundle lacks routeModule.handle");
+}
+if (generated.routeModule?.definition?.pathname !== "/api/benchmark") {
+ throw new Error(
+ `unexpected generated pathname: ${generated.routeModule?.definition?.pathname}`,
+ );
+}
+if (typeof generated.handler !== "function") {
+ throw new Error("generated production bundle lacks its server handler");
+}
+console.log("PASS: production bundle exports routeModule.handle for /api/benchmark");
+NODE
+
+(
+ cd "$FIXTURE"
+ PORT="$PORT" HOSTNAME=127.0.0.1 npm start >"$WORK/node.log" 2>&1
+) &
+SERVER_PID=$!
+
+ready=false
+for _ in $(seq 1 120); do
+ if curl --fail --silent --output /dev/null "http://127.0.0.1:$PORT/api/benchmark?id=ready&iterations=1"; then
+ ready=true
+ break
+ fi
+ if ! kill -0 "$SERVER_PID" 2>/dev/null; then
+ echo "FAIL: Next oracle server exited before readiness" >&2
+ sed -n '1,240p' "$WORK/node.log" >&2
+ exit 1
+ fi
+ sleep 0.25
+done
+if [[ "$ready" != "true" ]]; then
+ echo "FAIL: Next oracle server did not become ready" >&2
+ sed -n '1,240p' "$WORK/node.log" >&2
+ exit 1
+fi
+
+(
+ cd "$FIXTURE"
+ BASE_URL="http://127.0.0.1:$PORT" npm run verify
+)
+
+echo "PASS: pinned production Next App Route Node oracle"