@@ -16,13 +16,23 @@ export interface BackendFunction {
1616 allowedConnectionIds : string [ ] ;
1717}
1818
19+ export interface BackendExport {
20+ /** Exported backend function name. */
21+ name : string ;
22+ /** Local binding name when the export points at a local/imported binding. */
23+ localName ?: string ;
24+ /** Source specifier for re-exported bindings. */
25+ source ?: string ;
26+ }
27+
1928/**
20- * Extract exported value (non-type) symbols from an ESTree AST.
29+ * Enumerate exported value (non-type) symbols from an ESTree AST.
2130 * Expects plain JavaScript — TypeScript types must already be stripped
2231 * (e.g. by Vite's built-in esbuild transform that runs before our hook).
2332 *
2433 * Throws on invalid exports (e.g. default exports) and unexpected AST shapes.
2534 * Returns an empty array when the file has no named exports.
35+ * This is the shared source of truth for supported backend export shapes.
2636 *
2737 * @param ast - AstNode from `this.parse()` in unplugin's transform hook
2838 * @param filePath - Path to the source file (used in error messages)
@@ -31,7 +41,7 @@ function isProgramNode(node: AstNode): node is AstNode & Program {
3141 return node . type === 'Program' ;
3242}
3343
34- export function extractExportedFunctions ( ast : AstNode , filePath : string ) : string [ ] {
44+ export function enumerateBackendExports ( ast : AstNode , filePath : string ) : BackendExport [ ] {
3545 if ( ! isProgramNode ( ast ) ) {
3646 throw new Error (
3747 `Expected a Program node from this.parse() for ${ filePath } , got ${ ast . type } ` ,
@@ -41,7 +51,7 @@ export function extractExportedFunctions(ast: AstNode, filePath: string): string
4151 // Build a map of top-level declarations so we can validate export specifiers.
4252 const declarations = buildDeclarationMap ( ast ) ;
4353
44- const names : string [ ] = [ ] ;
54+ const exports : BackendExport [ ] = [ ] ;
4555 for ( const node of ast . body ) {
4656 // handles: export default ...
4757 if ( node . type === 'ExportDefaultDeclaration' ) {
@@ -61,7 +71,7 @@ export function extractExportedFunctions(ast: AstNode, filePath: string): string
6171
6272 // handles: export function add() {} / export const add = ...
6373 if ( node . declaration ) {
64- names . push ( ...namesFromDeclaration ( node . declaration , filePath ) ) ;
74+ exports . push ( ...exportsFromDeclaration ( node . declaration , filePath ) ) ;
6575 }
6676
6777 for ( const spec of node . specifiers ) {
@@ -74,17 +84,29 @@ export function extractExportedFunctions(ast: AstNode, filePath: string): string
7484 `Default exports are not supported in .backend.ts files. Use a named export instead: ${ filePath } ` ,
7585 ) ;
7686 }
77- // Validate specifier binding is callable when we can resolve it.
78- // e.g. `const VERSION = '1.0'; export { VERSION };` — rejected
79- // e.g. `function add() {}; export { add };` — allowed
8087 if ( spec . local . type === 'Identifier' ) {
88+ if ( node . source && typeof node . source . value === 'string' ) {
89+ exports . push ( {
90+ name : spec . exported . name ,
91+ localName : spec . local . name ,
92+ source : node . source . value ,
93+ } ) ;
94+ continue ;
95+ }
96+ // Validate specifier binding is callable when we can resolve it.
97+ // e.g. `const VERSION = '1.0'; export { VERSION };` — rejected
98+ // e.g. `function add() {}; export { add };` — allowed
8199 validateSpecifierBinding ( spec . local . name , declarations , filePath ) ;
100+ // handles: export { add, multiply } and aliases
101+ exports . push ( { name : spec . exported . name , localName : spec . local . name } ) ;
82102 }
83- // handles: export { add, multiply }
84- names . push ( spec . exported . name ) ;
85103 }
86104 }
87- return names ;
105+ return exports ;
106+ }
107+
108+ export function extractExportedFunctions ( ast : AstNode , filePath : string ) : string [ ] {
109+ return enumerateBackendExports ( ast , filePath ) . map ( ( backendExport ) => backendExport . name ) ;
88110}
89111
90112/** Init types that are definitively non-callable at runtime. */
@@ -110,10 +132,10 @@ function isNonCallableInit(init: Expression | null | undefined): boolean {
110132 * Handles `export function foo()` and `export const foo = ...` forms.
111133 * Throws when a variable export has a non-callable initializer.
112134 */
113- function namesFromDeclaration ( decl : Declaration , filePath : string ) : string [ ] {
135+ function exportsFromDeclaration ( decl : Declaration , filePath : string ) : BackendExport [ ] {
114136 // export function add(a, b) { return a + b; }
115137 if ( decl . type === 'FunctionDeclaration' && decl . id ) {
116- return [ decl . id . name ] ;
138+ return [ { name : decl . id . name , localName : decl . id . name } ] ;
117139 }
118140 // export class MyClass {} — classes are not callable as RPC endpoints
119141 if ( decl . type === 'ClassDeclaration' ) {
@@ -122,7 +144,7 @@ function namesFromDeclaration(decl: Declaration, filePath: string): string[] {
122144 ) ;
123145 }
124146 if ( decl . type === 'VariableDeclaration' ) {
125- return decl . declarations . flatMap ( ( d ) => {
147+ return decl . declarations . flatMap ( ( d ) : BackendExport [ ] => {
126148 // export const { a, b } = obj;
127149 // export const [a, b] = arr;
128150 if ( d . id . type !== 'Identifier' ) {
@@ -139,7 +161,7 @@ function namesFromDeclaration(decl: Declaration, filePath: string): string[] {
139161 }
140162 // export const add = (a, b) => a + b;
141163 // export const handler = importedFn; — ambiguous, allowed
142- return [ d . id . name ] ;
164+ return [ { name : d . id . name , localName : d . id . name } ] ;
143165 } ) ;
144166 }
145167 throw new Error (
0 commit comments