From f581eaa3eb1abbb04b2ba45c15bdc6deb11fb4f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 14 Aug 2026 22:47:47 +0200 Subject: [PATCH 1/2] test(next): add pinned production App Route fixture (#8034) Materializes the #8034 fixture generator as real files: Next.js 16.3.0 / React 19.2.4 pinned with lockfile, the provider host/linker, and a perry-host.js that drives routeModule.handle and throws if a generated handler bypasses it. --- tests/fixtures/next-app-route/.gitignore | 3 + tests/fixtures/next-app-route/README.md | 38 + .../next-app-route/app/api/benchmark/route.ts | 2 + tests/fixtures/next-app-route/app/layout.tsx | 3 + tests/fixtures/next-app-route/app/page.tsx | 3 + .../fixtures/next-app-route/lib/lazy-work.ts | 7 + .../fixtures/next-app-route/lib/route-impl.ts | 52 + tests/fixtures/next-app-route/next-env.d.ts | 7 + tests/fixtures/next-app-route/next.config.ts | 7 + .../fixtures/next-app-route/package-lock.json | 1016 +++++++++++++++++ tests/fixtures/next-app-route/package.json | 21 + tests/fixtures/next-app-route/perry-host.js | 45 + .../fixtures/next-app-route/provider-host.rs | 96 ++ .../next-app-route/provider-linker.sh | 126 ++ .../next-app-route/provider/Cargo.toml | 36 + .../next-app-route/provider/src/lib.rs | 25 + tests/fixtures/next-app-route/tsconfig.json | 26 + tests/fixtures/next-app-route/verify.mjs | 48 + tests/test_next_app_route_node_oracle.sh | 89 ++ 19 files changed, 1650 insertions(+) create mode 100644 tests/fixtures/next-app-route/.gitignore create mode 100644 tests/fixtures/next-app-route/README.md create mode 100644 tests/fixtures/next-app-route/app/api/benchmark/route.ts create mode 100644 tests/fixtures/next-app-route/app/layout.tsx create mode 100644 tests/fixtures/next-app-route/app/page.tsx create mode 100644 tests/fixtures/next-app-route/lib/lazy-work.ts create mode 100644 tests/fixtures/next-app-route/lib/route-impl.ts create mode 100644 tests/fixtures/next-app-route/next-env.d.ts create mode 100644 tests/fixtures/next-app-route/next.config.ts create mode 100644 tests/fixtures/next-app-route/package-lock.json create mode 100644 tests/fixtures/next-app-route/package.json create mode 100644 tests/fixtures/next-app-route/perry-host.js create mode 100644 tests/fixtures/next-app-route/provider-host.rs create mode 100755 tests/fixtures/next-app-route/provider-linker.sh create mode 100644 tests/fixtures/next-app-route/provider/Cargo.toml create mode 100644 tests/fixtures/next-app-route/provider/src/lib.rs create mode 100644 tests/fixtures/next-app-route/tsconfig.json create mode 100644 tests/fixtures/next-app-route/verify.mjs create mode 100755 tests/test_next_app_route_node_oracle.sh 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..630a3e278e --- /dev/null +++ b/tests/fixtures/next-app-route/provider/Cargo.toml @@ -0,0 +1,36 @@ +[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" } + +[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_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" From 093c046a727c4cc6a9b71cfe996521fcca459c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 14 Aug 2026 22:47:58 +0200 Subject: [PATCH 2/2] ci(next): add the app-only-dylib App Route gate (#8034) --- .github/workflows/next-app-route.yml | 78 +++++ changelog.d/8161-next-app-route-fixture.md | 25 ++ .../next-app-route/provider/Cargo.toml | 10 + tests/test_next_app_route_dylib.sh | 324 ++++++++++++++++++ 4 files changed, 437 insertions(+) create mode 100644 .github/workflows/next-app-route.yml create mode 100644 changelog.d/8161-next-app-route-fixture.md create mode 100755 tests/test_next_app_route_dylib.sh 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/provider/Cargo.toml b/tests/fixtures/next-app-route/provider/Cargo.toml index 630a3e278e..eab2018231 100644 --- a/tests/fixtures/next-app-route/provider/Cargo.toml +++ b/tests/fixtures/next-app-route/provider/Cargo.toml @@ -25,6 +25,16 @@ perry-stdlib = { path = "../../../../crates/perry-stdlib", default-features = fa ] } 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 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"