Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/web/app/compatibility/suite-support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
56 changes: 24 additions & 32 deletions scripts/e2e-deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions tests/compatibility-support.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
27 changes: 26 additions & 1 deletion tests/e2e-deploy-script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
);
Expand Down
Loading