Files
cloudflare__vinext/scripts/nextjs-deploy-manifest.mjs
James Anderson 617bffa95e test(deploy): skip suites that stall deploy shards (#2573)
* test(deploy): skip segment cache memory pressure suite

* test(deploy): skip instant navigation suite
2026-07-09 08:20:55 +00:00

188 lines
5.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Generates a filtered deploy-tests manifest from the Next.js repo.
*
* Usage:
* node scripts/nextjs-deploy-manifest.mjs <nextjs-dir> <output-json-path> \
* [--filter pages|app|all] [--match <path>]...
*
* Filters:
* pages — exclude test/e2e/app-dir/** (Pages Router only)
* app — include only test/e2e/app-dir/** (App Router only)
* all — pass through the full manifest unfiltered (default)
*
* --match narrows further to specific suites by path prefix. Repeatable, or
* comma-separated. A directory prefix like `test/e2e/edge-async-local-storage`
* keeps every suite under it; a full test file path keeps just that file.
* Applied AFTER --filter, so `--filter app --match test/e2e/app-dir/foo`
* works as expected.
*/
import fs from "node:fs/promises";
import path from "node:path";
function printUsage() {
console.error(
"Usage: node scripts/nextjs-deploy-manifest.mjs <nextjs-dir> <output-json-path> [--filter pages|app|all] [--match <path>]...",
);
}
const TEST_FILE_RE = /\.test\.(t|j)sx?$/;
function matchToInclude(match) {
// Already a test file path: include verbatim.
if (TEST_FILE_RE.test(match)) return match;
// Directory prefix: glob every test file underneath.
const trimmed = match.replace(/\/+$/, "");
return `${trimmed}/**/*.test.{t,j}s{,x}`;
}
function suiteMatchesAny(suite, matches) {
for (const m of matches) {
const trimmed = m.replace(/\/+$/, "");
if (suite === trimmed) return true;
if (suite.startsWith(`${trimmed}/`)) return true;
}
return false;
}
function isAppDirSuite(suite) {
return suite.startsWith("test/e2e/app-dir/");
}
/**
* Suites that live outside test/e2e/app-dir/ but exercise App Router
* behavior. Excluded from the "pages" filter so they don't gate Pages
* Router-only runs.
*/
const APP_ROUTER_NON_APP_DIR_SUITES = ["test/e2e/next-form/default/next-form-prefetch.test.ts"];
const CUSTOM_DEPLOY_ADAPTER_EXCLUDES = [
"test/e2e/app-dir/instant-navigation-testing-api/instant-navigation-testing-api.test.ts",
"test/e2e/app-dir/segment-cache/memory-pressure/segment-cache-memory-pressure.test.ts",
];
const CUSTOM_DEPLOY_ADAPTER_SUITE_OVERRIDES = {
// Custom deploy adapters snapshot cliOutput during setup, before this test emits runtime logs.
"test/e2e/repeated-forward-slashes-error/repeated-forward-slashes-error.test.ts": {
failed: [
"repeated-forward-slashes-error should log error when href has repeated forward-slashes",
],
},
};
async function main() {
const positionals = [];
let filter = "all";
const matches = [];
// Simple arg parsing: positionals + optional --filter / --match
for (let i = 2; i < process.argv.length; i++) {
const arg = process.argv[i];
if (arg === "--filter" && i + 1 < process.argv.length) {
filter = process.argv[++i];
} else if (arg === "--match" && i + 1 < process.argv.length) {
for (const part of process.argv[++i].split(",")) {
const trimmed = part.trim();
if (trimmed) matches.push(trimmed);
}
} else if (!arg.startsWith("-")) {
positionals.push(arg);
}
}
const [nextjsDirArg, outputPathArg] = positionals;
if (!nextjsDirArg || !outputPathArg) {
printUsage();
process.exitCode = 1;
return;
}
if (!["pages", "app", "all"].includes(filter)) {
console.error(`Invalid --filter value: ${filter}. Must be pages, app, or all.`);
process.exitCode = 1;
return;
}
const nextjsDir = path.resolve(nextjsDirArg);
const outputPath = path.resolve(outputPathArg);
const sourcePath = path.join(nextjsDir, "test", "deploy-tests-manifest.json");
const source = JSON.parse(await fs.readFile(sourcePath, "utf8"));
if (source.version !== 2) {
throw new Error(`Expected Next.js deploy manifest version 2, got ${source.version}`);
}
let suites = { ...source.suites };
for (const [suite, override] of Object.entries(CUSTOM_DEPLOY_ADAPTER_SUITE_OVERRIDES)) {
const current = suites[suite] ?? {};
suites[suite] = {
...current,
failed: Array.from(new Set([...(current.failed ?? []), ...(override.failed ?? [])])),
};
}
let extraExcludes = [...CUSTOM_DEPLOY_ADAPTER_EXCLUDES];
if (filter === "pages") {
suites = Object.fromEntries(Object.entries(suites).filter(([suite]) => !isAppDirSuite(suite)));
extraExcludes.push("test/e2e/app-dir/**/*", ...APP_ROUTER_NON_APP_DIR_SUITES);
} else if (filter === "app") {
suites = Object.fromEntries(Object.entries(suites).filter(([suite]) => isAppDirSuite(suite)));
}
let include = source.rules?.include?.length
? source.rules.include
: ["test/e2e/**/*.test.{t,j}s{,x}"];
if (matches.length > 0) {
suites = Object.fromEntries(
Object.entries(suites).filter(([suite]) => suiteMatchesAny(suite, matches)),
);
// Narrow include to just the matched paths so the runner doesn't scan
// the rest of test/e2e/. Excludes from --filter still apply.
include = matches.map(matchToInclude);
}
const exclude = Array.from(new Set([...(source.rules?.exclude ?? []), ...extraExcludes]));
const manifest = {
version: 2,
suites,
rules: {
include,
exclude,
},
};
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, `${JSON.stringify(manifest, null, 2)}\n`);
const totalSuites = Object.keys(source.suites ?? {}).length;
const appDirSuites = Object.keys(source.suites ?? {}).filter(isAppDirSuite).length;
const outputSuites = Object.keys(suites).length;
console.log(`Wrote ${outputPath}`);
console.log(
JSON.stringify(
{
source: sourcePath,
filter,
matches,
totalSuites,
outputSuites,
appDirSuites,
nonAppDirSuites: totalSuites - appDirSuites,
},
null,
2,
),
);
}
main().catch((error) => {
console.error(error?.stack || error);
process.exitCode = 1;
});