From c9c3c8f5c77a8d5d213c3e8eab9cf0851ecbffcb Mon Sep 17 00:00:00 2001 From: Jason Paff Date: Fri, 2 Oct 2026 17:23:47 -0400 Subject: [PATCH] fix(next): skip test files in the automatic route-definition scan The scan loads any .ts/.js file that mentions defineAppRoute, which includes tests of route codecs. In the Cadence rc trial, list/doctor evaluated a vitest file outside its runner. The automatic scan now ignores *.test.*, *.spec.* and __tests__/. Explicit routeFiles globs are taken as written. Co-Authored-By: Claude Opus 5.5 (1M context) --- .changeset/list-skip-test-files.md | 5 +++ docs/content/docs/reference/next/cli/list.mdx | 2 + packages/next/src/list/discover-route-defs.ts | 22 +++++++--- packages/next/test/list.test.ts | 42 +++++++++++++++++++ 4 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 .changeset/list-skip-test-files.md diff --git a/.changeset/list-skip-test-files.md b/.changeset/list-skip-test-files.md new file mode 100644 index 0000000..05ac7a4 --- /dev/null +++ b/.changeset/list-skip-test-files.md @@ -0,0 +1,5 @@ +--- +"@paramour-js/next": patch +--- + +The automatic route-definition scan in `paramour list` and `paramour doctor` no longer loads test files (`*.test.*`, `*.spec.*`, and files under `__tests__/`). A test that mentions `defineAppRoute` used to be evaluated outside its test runner, which ran its top-level `describe`/`it` calls and any setup side effects. Explicit `routeFiles` globs are unchanged and can still name test files. diff --git a/docs/content/docs/reference/next/cli/list.mdx b/docs/content/docs/reference/next/cli/list.mdx index 3e868dc..4deadf2 100644 --- a/docs/content/docs/reference/next/cli/list.mdx +++ b/docs/content/docs/reference/next/cli/list.mdx @@ -32,6 +32,8 @@ panel. That evaluation is why definitions should stay Modules that fail to load are reported and skipped. Set `routeFiles` globs in [`paramour.config`](/docs/reference/next/with-typed-routes#paramourconfig) to pin which modules are scanned when the automatic content scan misfires. +The automatic scan never loads test files (`*.test.*`, `*.spec.*`, and +anything under `__tests__/`). `routeFiles` globs are taken as written. ## Coverage warnings diff --git a/packages/next/src/list/discover-route-defs.ts b/packages/next/src/list/discover-route-defs.ts index 8476b1b..3ebea5b 100644 --- a/packages/next/src/list/discover-route-defs.ts +++ b/packages/next/src/list/discover-route-defs.ts @@ -58,6 +58,12 @@ const IGNORE_PATTERNS = [ "**/out/**", ]; +// Test files often mention defineAppRoute (they test route codecs), and +// evaluating them outside the runner executes their top-level +// describe/it calls and any setup side effects. These are ignored only in +// the automatic scan: explicit `routeFiles` globs are taken as written. +const TEST_IGNORE_PATTERNS = ["**/*.{test,spec}.*", "**/__tests__/**"]; + /** Files past this size skip the content pre-filter (bundles, lockfiles). */ const MAX_PREFILTER_BYTES = 512 * 1024; @@ -79,12 +85,16 @@ export async function discoverRouteDefinitions( // Dynamic imports, same stance as config.ts: only commands that actually // discover definitions pay for tinyglobby/jiti. const { glob } = await import("tinyglobby"); - const files = await glob( - options.routeFiles === undefined - ? DEFAULT_PATTERNS - : [...options.routeFiles], - { absolute: true, cwd: projectRoot, ignore: IGNORE_PATTERNS }, - ); + const { routeFiles } = options; + const [patterns, ignore] = + routeFiles === undefined + ? [DEFAULT_PATTERNS, [...IGNORE_PATTERNS, ...TEST_IGNORE_PATTERNS]] + : [[...routeFiles], IGNORE_PATTERNS]; + const files = await glob(patterns, { + absolute: true, + cwd: projectRoot, + ignore, + }); // Deterministic load order — dedupe's "first wins" must not depend on // filesystem enumeration order. files.sort(); diff --git a/packages/next/test/list.test.ts b/packages/next/test/list.test.ts index 959456f..78501c5 100644 --- a/packages/next/test/list.test.ts +++ b/packages/next/test/list.test.ts @@ -175,6 +175,48 @@ export const a = defineAppRoute("/a/[id]", { params: { id: p.integer() } }); expect(text).toMatch(/\/b\/\[id\]\s+⚠ filesystem only/); }); + it("never evaluates test files in the automatic scan", async () => { + const root = makeProject(["app/a/[id]/page.tsx"], { + "app/a/[id]/route.def.ts": `import { defineAppRoute, p } from "paramour"; +export const a = defineAppRoute("/a/[id]", { params: { id: p.integer() } }); +`, + "lib/__tests__/routes.ts": `// defineAppRoute +import { writeFileSync } from "node:fs"; +writeFileSync("evaluated-tests", ""); +`, + "lib/codecs.spec.mts": `// defineAppRoute +import { writeFileSync } from "node:fs"; +writeFileSync("evaluated-spec", ""); +`, + "lib/codecs.test.ts": `// defineAppRoute +import { writeFileSync } from "node:fs"; +writeFileSync("evaluated-test", ""); +`, + }); + const run = await list(); + expect(run.code).toBe(0); + expect(run.out.join("\n")).not.toContain("failed to load"); + for (const marker of [ + "evaluated-test", + "evaluated-spec", + "evaluated-tests", + ]) { + expect(existsSync(join(root, marker))).toBe(false); + } + }); + + it("routeFiles globs can still name test files explicitly", async () => { + makeProject(["app/a/[id]/page.tsx"], { + "defs/a.test.ts": `import { defineAppRoute, p } from "paramour"; +export const a = defineAppRoute("/a/[id]", { params: { id: p.integer() } }); +`, + "paramour.config.json": `{ "routeFiles": ["defs/**/*.ts"] }`, + }); + const run = await list(); + expect(run.code).toBe(0); + expect(run.out.join("\n")).toMatch(/\/a\/\[id\]\s+defs\/a\.test\.ts/); + }); + it("dedupes by (router, path): first file wins, later ones are reported", async () => { makeProject(["app/dupe/[id]/page.tsx"], { "lib/a-first.ts": `import { defineAppRoute, p } from "paramour";