mirror of
https://github.com/simstudioai/sim.git
synced 2026-10-30 19:32:46 +08:00
fix(ci): require a default export before treating a file as a route entry (#7028)
* 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. * fix(ci): count every form that declares a default export `export { default } from './page'` is a valid Next entry and the regex required `as default`, so such an entry would have dropped out of the walk and skipped both the registry gate and the graph-weight ratchet — silently, which is the dangerous direction for a discriminator to fail in. Latent rather than live: the form appears once under `app/workspace`, in a barrel, not in an entry filename. Four forms now count — `export default …`, `export { default } from`, `export { default, … } from`, and `export { X as default }`. `export { default as X }` still does not: it re-exports another module's default under a name and leaves this one without one. Verified all ten variants, including that last distinction. Raised by both Cursor and Greptile on #7028.
This commit is contained in:
@@ -72,13 +72,46 @@ const ENTRY_FILENAMES = new Set([
|
||||
'error.tsx',
|
||||
'loading.tsx',
|
||||
'not-found.tsx',
|
||||
'template.tsx',
|
||||
'default.tsx',
|
||||
])
|
||||
|
||||
/**
|
||||
* A default export, which every convention-composed entry must have — Next
|
||||
* renders the default and nothing else.
|
||||
*
|
||||
* The filename alone is not enough. `[workspaceId]/components/error/error.tsx`
|
||||
* is named like a boundary and is not one: it exports `ErrorShell` and
|
||||
* `ErrorState` for the thirteen real boundaries to use, and Next would reject
|
||||
* it as a boundary for having no default. Counting it as an entry both inflated
|
||||
* the coverage number and would have recorded a shared component in the
|
||||
* graph-weight baseline as though it were a route.
|
||||
*
|
||||
* All four declaring forms count — `export default …`, `export { default } from`,
|
||||
* `export { default, … } from`, and `export { X as default }`. `export { default
|
||||
* as X }` does not: it re-exports someone else's default under a name and leaves
|
||||
* the module without one. Missing a form is the dangerous direction, since the
|
||||
* entry would drop out of the walk and skip both the registry gate and the
|
||||
* graph-weight ratchet silently.
|
||||
*/
|
||||
const DEFAULT_EXPORT_RE =
|
||||
/(?:^|\n)\s*export\s+default\b|(?:^|\n)\s*export\s*\{[^}]*(?:\bas\s+default\b|\bdefault\s*[,}])/
|
||||
|
||||
function hasDefaultExport(file: string): boolean {
|
||||
try {
|
||||
return DEFAULT_EXPORT_RE.test(readFileSync(file, 'utf8'))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function collectEntries(dir: string, found: string[] = []): string[] {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = join(dir, entry.name)
|
||||
if (entry.isDirectory()) collectEntries(full, found)
|
||||
else if (ENTRY_FILENAMES.has(entry.name)) found.push(relative(APP, full))
|
||||
else if (ENTRY_FILENAMES.has(entry.name) && hasDefaultExport(full)) {
|
||||
found.push(relative(APP, full))
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user