diff --git a/apps/web/app/compatibility/suite-support.ts b/apps/web/app/compatibility/suite-support.ts index cce172314..2b7b8b82d 100644 --- a/apps/web/app/compatibility/suite-support.ts +++ b/apps/web/app/compatibility/suite-support.ts @@ -62,6 +62,12 @@ export const SUITE_SUPPORT_POLICY = { DEFERRED_PARTIAL_PRERENDERING, "test/e2e/app-dir/fallback-shells/fallback-shells.test.ts": DEFERRED_PARTIAL_PRERENDERING, "test/e2e/app-dir/next-config/index.test.ts": NEXT_BUNDLER_SPECIFIC, + "test/e2e/app-dir/next-dynamic-csp-nonce/next-dynamic-csp-nonce.test.ts": { + status: "needs-vite-equivalent", + feature: "CSP nonces on dynamically imported module preloads", + reason: + "Next.js asserts webpack's script preload shape. Vinext emits Vite modulepreload links with the nonce and verifies them on Cloudflare Workers in tests/e2e/cloudflare-workers/dynamic-preload.spec.ts.", + }, "test/e2e/app-dir/next-after-app-deploy/index.test.ts": { status: "unsupported", feature: "Next.js dual Node.js and legacy Edge Runtime builds", diff --git a/scripts/e2e-deploy.sh b/scripts/e2e-deploy.sh index 62447426c..bcca03211 100755 --- a/scripts/e2e-deploy.sh +++ b/scripts/e2e-deploy.sh @@ -388,7 +388,22 @@ function manifestDependencySpecFor(name, spec, fromPackageDir) { throw new Error(`Unable to resolve dependency spec for ${name}`) } +const reactRuntimePackages = new Set(['react', 'react-dom', 'react-server-dom-webpack']) + function dependencySpecFor(name) { + // Deploy fixtures have no lockfile, so open peer ranges can silently mix + // incompatible React releases between nightly runs. Reuse the exact trio + // that built vinext in the already-frozen workspace install. + if (reactRuntimePackages.has(name)) { + for (const parent of [path.join(vinextDir, 'packages', 'vinext'), vinextDir]) { + const manifestPath = path.join(parent, 'node_modules', name, 'package.json') + if (fs.existsSync(manifestPath)) { + return JSON.parse(fs.readFileSync(manifestPath, 'utf8')).version + } + } + throw new Error(`Unable to find installed ${name} version`) + } + for (const deps of [ vinextPkg.peerDependencies, vinextPkg.dependencies, @@ -509,31 +524,13 @@ fs.writeFileSync( pkg.devDependencies = pkg.devDependencies || {} pkg.devDependencies.vinext = 'file:.vinext-local-package' -// App Router fixtures need React to satisfy the same peer range as the -// injected react-server-dom-webpack. If they install an older React pair first, -// `vinext build` runs its RSC compatibility upgrade and pays for a second -// package-manager install inside every throwaway test app. Normalize the temp -// manifest before the first install so the final dependency graph is unchanged -// but setup is single-pass. +// App Router fixtures need the same React trio that built vinext. Pinning the +// throwaway manifest keeps nightly results tied to the frozen workspace install +// and avoids a second compatibility install during `vinext build`. function hasAppRouterDir(root) { return fs.existsSync(path.join(root, 'app')) || fs.existsSync(path.join(root, 'src', 'app')) } -function compareSemver(a, b) { - for (let index = 0; index < 3; index += 1) { - if (a[index] < b[index]) return -1 - if (a[index] > b[index]) return 1 - } - - return 0 -} - -function parseSemverSpec(spec) { - const match = /(\d+)\.(\d+)\.(\d+)/.exec(spec) - if (!match) return null - return [Number(match[1]), Number(match[2]), Number(match[3])] -} - function dependencyBucketFor(name) { for (const bucket of ['dependencies', 'devDependencies', 'peerDependencies']) { if (pkg[bucket]?.[name]) return bucket @@ -545,26 +542,19 @@ function dependencyBucketFor(name) { function normalizeAppRouterReactDeps() { if (!hasAppRouterDir(process.cwd())) return - for (const dep of ['react', 'react-dom']) { - const bucket = dependencyBucketFor(dep) - if (!bucket) continue - + for (const dep of reactRuntimePackages) { + const bucket = dependencyBucketFor(dep) || 'devDependencies' const current = pkg[bucket][dep] - const version = parseSemverSpec(current) const replacement = dependencySpecFor(dep) - const minimumVersion = parseSemverSpec(replacement) - if (!minimumVersion) continue - if (!version || compareSemver(version, minimumVersion) >= 0) continue + if (current === replacement) continue pkg[bucket][dep] = replacement console.log( - `Bumped ${bucket}.${dep} from ${current} to ${replacement} for RSC compatibility`, + `Pinned ${bucket}.${dep} from ${current || '(missing)'} to ${replacement} for RSC compatibility`, ) } } -normalizeAppRouterReactDeps() - // Catalog-tracked deps: spec sourced from vinext or workspace root package.json. // Includes the Vite/RSC peers that vinext consumers must install, plus runtime // deps of vinext that pnpm doesn't hoist into the test app's top-level @@ -585,6 +575,8 @@ for (const dep of [ } } +normalizeAppRouterReactDeps() + // Some Next.js scss test fixtures pin sass to an old version (e.g. 1.54.0) // that predates `sass.initAsyncCompiler`. Vite 8's built-in vite:css preprocessor // calls that API and declares `sass@^1.70.0` / `sass-embedded@^1.70.0` as peers. diff --git a/tests/compatibility-support.test.ts b/tests/compatibility-support.test.ts index ca897e4c1..497ed9c7d 100644 --- a/tests/compatibility-support.test.ts +++ b/tests/compatibility-support.test.ts @@ -31,12 +31,12 @@ describe("compatibility suite support policy", () => { expect(counts).toEqual({ deferred: 25, - "needs-vite-equivalent": 3, + "needs-vite-equivalent": 4, unsupported: 6, }); - expect(NON_SUPPORTED_SUITES).toHaveLength(34); - expect(CLASSIFIED_SUITES).toHaveLength(69); - expect(new Set(CLASSIFIED_SUITES).size).toBe(69); + expect(NON_SUPPORTED_SUITES).toHaveLength(35); + expect(CLASSIFIED_SUITES).toHaveLength(70); + expect(new Set(CLASSIFIED_SUITES).size).toBe(70); }); it("classifies the mixed legacy Edge Runtime suite at file scope", () => { diff --git a/tests/e2e-deploy-script.test.ts b/tests/e2e-deploy-script.test.ts index 7abc97473..a207d369c 100644 --- a/tests/e2e-deploy-script.test.ts +++ b/tests/e2e-deploy-script.test.ts @@ -88,11 +88,32 @@ describe("Next.js deploy harness", () => { fs.mkdirSync(path.join(workspaceRoot, "packages/vinext/dist"), { recursive: true }); fs.mkdirSync(path.join(workspaceRoot, "packages/cloudflare/dist"), { recursive: true }); fs.mkdirSync(path.join(workspaceRoot, "packages/types/next"), { recursive: true }); + for (const packageName of ["react", "react-dom", "react-server-dom-webpack"]) { + const packageRoot = path.join( + workspaceRoot, + packageName === "react-server-dom-webpack" + ? "packages/vinext/node_modules" + : "node_modules", + packageName, + ); + fs.mkdirSync(packageRoot, { recursive: true }); + fs.writeFileSync( + path.join(packageRoot, "package.json"), + JSON.stringify({ name: packageName, version: "19.2.7" }), + ); + } fs.writeFileSync( path.join(workspaceRoot, "packages/types/next/index.d.ts"), 'declare module "next" {}\n', ); - fs.writeFileSync(path.join(appRoot, "package.json"), '{"name":"fixture"}\n'); + fs.mkdirSync(path.join(appRoot, "app")); + fs.writeFileSync( + path.join(appRoot, "package.json"), + JSON.stringify({ + name: "fixture", + dependencies: { react: "latest", "react-dom": "^19.0.0" }, + }), + ); execFileSync(process.execPath, ["-e", injection!], { cwd: appRoot, @@ -112,9 +133,13 @@ describe("Next.js deploy harness", () => { const localTypes = JSON.parse( fs.readFileSync(path.join(appRoot, ".vinext-local-types-package/package.json"), "utf8"), ); + const fixture = JSON.parse(fs.readFileSync(path.join(appRoot, "package.json"), "utf8")); expect(localVinext.dependencies["@vinext/types"]).toBe("file:../.vinext-local-types-package"); expect(localCloudflare.peerDependencies.vinext).toBe("file:../.vinext-local-package"); expect(localTypes.name).toBe("@vinext/types"); + expect(fixture.dependencies.react).toBe("19.2.7"); + expect(fixture.dependencies["react-dom"]).toBe("19.2.7"); + expect(fixture.devDependencies["react-server-dom-webpack"]).toBe("19.2.7"); expect(fs.existsSync(path.join(appRoot, ".vinext-local-types-package/next/index.d.ts"))).toBe( true, );