Skip to content

Commit 0a981af

Browse files
fix(routes): ignore type-only export specifiers in the route module export scan
export type { X } and export { type X as default } emit no JavaScript binding, so they must not satisfy the component or named-export contract; this closes the last dual-surface hole where a bin-claimed rendered script passed AB4737 with a type-only default alias.
1 parent ed14048 commit 0a981af

2 files changed

Lines changed: 18 additions & 0 deletions

File tree

‎packages/agent-bundle/src/routes/contract.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ export const scanRouteModuleExports = (
9191
continue;
9292
}
9393
if (ts.isExportDeclaration(statement) && statement.exportClause !== undefined && ts.isNamedExports(statement.exportClause)) {
94+
// Type-only exports (`export type { X }`, `export { type X as default }`)
95+
// emit no JavaScript binding, so they satisfy no runtime contract.
96+
if (statement.isTypeOnly) continue;
9497
for (const element of statement.exportClause.elements) {
98+
if (element.isTypeOnly) continue;
9599
const name = element.name.text;
96100
if (name === 'default' && statement.moduleSpecifier === undefined) {
97101
defaultIdentifier = element.propertyName?.text ?? name;

‎packages/agent-bundle/tests/route-graph.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,12 +302,22 @@ it('gates a bin-claimed rendered script with AB4737 only when it exports no main
302302
" object: './src/scripts/render-object.tsx',",
303303
" poster: './src/scripts/render-poster.tsx',",
304304
" tool: './src/scripts/render-tool.tsx',",
305+
" typed: './src/scripts/render-typed.tsx',",
305306
' },',
306307
" plugin: { name: 'routes-fixture', version: '1.0.0' },",
307308
" targets: ['portable'],",
308309
'};',
309310
'',
310311
].join('\n'),
312+
// main plus a type-only default alias of an async function binding: no
313+
// JavaScript default export is emitted, so the rendered script has no
314+
// component even though a same-named function exists.
315+
'src/scripts/render-typed.tsx': [
316+
'const Component = async () => undefined;',
317+
'export const main = async (argv: readonly string[]): Promise<number> => argv.length;',
318+
'export { type Component as default };',
319+
'',
320+
].join('\n'),
311321
// Exports both: main(argv) for the bin envelope, the component for the
312322
// rendered script, so the module serves both surfaces.
313323
'src/scripts/render-notes.tsx': [
@@ -339,10 +349,12 @@ it('gates a bin-claimed rendered script with AB4737 only when it exports no main
339349
join(project, 'src/scripts/render-object.tsx'),
340350
join(project, 'src/scripts/render-poster.tsx'),
341351
join(project, 'src/scripts/render-tool.tsx'),
352+
join(project, 'src/scripts/render-typed.tsx'),
342353
]);
343354
expect(gate[0]!.message).toContain('render-object.tsx is also the entry of bin "object" but exports no async default Server Component');
344355
expect(gate[1]!.message).toContain('render-poster.tsx is also the entry of bin "poster" but exports no named main');
345356
expect(gate[2]!.message).toContain('render-tool.tsx is also the entry of bin "tool" but exports no async default Server Component');
357+
expect(gate[3]!.message).toContain('render-typed.tsx is also the entry of bin "typed" but exports no async default Server Component');
346358
expect(gate.every((diagnostic) => diagnostic.severity === 'error')).toBe(true);
347359
// Every rendered script stays discovered beside its bin: the gate names
348360
// the conflict instead of dropping a route.
@@ -352,13 +364,15 @@ it('gates a bin-claimed rendered script with AB4737 only when it exports no main
352364
object: './src/scripts/render-object.tsx',
353365
poster: './src/scripts/render-poster.tsx',
354366
tool: './src/scripts/render-tool.tsx',
367+
typed: './src/scripts/render-typed.tsx',
355368
},
356369
}));
357370
expect(graph.scripts.map((route) => route.id)).toEqual([
358371
'script:render-notes',
359372
'script:render-object',
360373
'script:render-poster',
361374
'script:render-tool',
375+
'script:render-typed',
362376
]);
363377
});
364378

0 commit comments

Comments
 (0)