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
5 changes: 5 additions & 0 deletions .changeset/list-skip-test-files.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions docs/content/docs/reference/next/cli/list.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ and `baseUrl`), so a definition that imports shared codecs from
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

Expand Down
22 changes: 16 additions & 6 deletions packages/next/src/list/discover-route-defs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -80,12 +86,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();
Expand Down
42 changes: 42 additions & 0 deletions packages/next/test/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading