From c01be22d1e26217b1632c909417aa866e876d390 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 23 Aug 2026 19:20:59 -0700 Subject: [PATCH 1/2] fix(ci): require a default export before treating a file as a route entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- scripts/check-tool-registry-boundary.ts | 28 ++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/scripts/check-tool-registry-boundary.ts b/scripts/check-tool-registry-boundary.ts index 1ab0392b344..4416f403957 100644 --- a/scripts/check-tool-registry-boundary.ts +++ b/scripts/check-tool-registry-boundary.ts @@ -72,13 +72,39 @@ 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. + */ +const DEFAULT_EXPORT_RE = + /(?:^|\n)\s*export\s+default\b|(?:^|\n)\s*export\s*\{[^}]*\bas\s+default\b/ + +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 } From 376e373ec23fe57309c8a9b9b68ee4b5737df1c7 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 23 Aug 2026 20:01:05 -0700 Subject: [PATCH 2/2] fix(ci): count every form that declares a default export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- scripts/check-tool-registry-boundary.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/check-tool-registry-boundary.ts b/scripts/check-tool-registry-boundary.ts index 4416f403957..e662d20f33f 100644 --- a/scripts/check-tool-registry-boundary.ts +++ b/scripts/check-tool-registry-boundary.ts @@ -86,9 +86,16 @@ const ENTRY_FILENAMES = new Set([ * 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/ + /(?:^|\n)\s*export\s+default\b|(?:^|\n)\s*export\s*\{[^}]*(?:\bas\s+default\b|\bdefault\s*[,}])/ function hasDefaultExport(file: string): boolean { try {