Skip to content

Commit c01be22

Browse files
committed
fix(ci): require a default export before treating a file as a route entry
Follow-up to #7026, which added `error.tsx` to the entry filenames and with it picked up `[workspaceId]/components/error/error.tsx` — named like a boundary, and not one. It exports `ErrorShell` and `ErrorState` for the thirteen real boundaries to use; Next would reject it as a boundary for having no default export. Counting it inflated the coverage number and would have recorded a shared component in the graph-weight baseline as though it were a route. The filename was never the right test. Every convention-composed entry must default-export the thing Next renders, so that is the discriminator now. Entry count goes 60 → 59, and all thirteen real `error.tsx` boundaries still walk. Also adds `template.tsx` and `default.tsx`. Neither exists under `app/workspace` today, so this changes nothing now — but the enumeration claims to cover what Next composes, and leaving two out makes that claim false the day someone adds one. Both raised in review on #7026 (Cursor and Greptile respectively); I merged before reading them, so this lands separately.
1 parent f37c24e commit c01be22

1 file changed

Lines changed: 27 additions & 1 deletion

File tree

scripts/check-tool-registry-boundary.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,39 @@ const ENTRY_FILENAMES = new Set([
7272
'error.tsx',
7373
'loading.tsx',
7474
'not-found.tsx',
75+
'template.tsx',
76+
'default.tsx',
7577
])
7678

79+
/**
80+
* A default export, which every convention-composed entry must have — Next
81+
* renders the default and nothing else.
82+
*
83+
* The filename alone is not enough. `[workspaceId]/components/error/error.tsx`
84+
* is named like a boundary and is not one: it exports `ErrorShell` and
85+
* `ErrorState` for the thirteen real boundaries to use, and Next would reject
86+
* it as a boundary for having no default. Counting it as an entry both inflated
87+
* the coverage number and would have recorded a shared component in the
88+
* graph-weight baseline as though it were a route.
89+
*/
90+
const DEFAULT_EXPORT_RE =
91+
/(?:^|\n)\s*export\s+default\b|(?:^|\n)\s*export\s*\{[^}]*\bas\s+default\b/
92+
93+
function hasDefaultExport(file: string): boolean {
94+
try {
95+
return DEFAULT_EXPORT_RE.test(readFileSync(file, 'utf8'))
96+
} catch {
97+
return false
98+
}
99+
}
100+
77101
function collectEntries(dir: string, found: string[] = []): string[] {
78102
for (const entry of readdirSync(dir, { withFileTypes: true })) {
79103
const full = join(dir, entry.name)
80104
if (entry.isDirectory()) collectEntries(full, found)
81-
else if (ENTRY_FILENAMES.has(entry.name)) found.push(relative(APP, full))
105+
else if (ENTRY_FILENAMES.has(entry.name) && hasDefaultExport(full)) {
106+
found.push(relative(APP, full))
107+
}
82108
}
83109
return found
84110
}

0 commit comments

Comments
 (0)