post-api-sync 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,184 @@
1
+ const traverse = require('@babel/traverse').default;
2
+ const { parseFile } = require('./ast');
3
+ const { normalizePath, toKey, HTTP_METHODS, joinPaths } = require('../utils');
4
+ const { resolveZodSchema } = require('./zod');
5
+
6
+ function getStringLiteral(node) {
7
+ if (!node) return '';
8
+ if (node.type === 'StringLiteral') return node.value;
9
+ if (node.type === 'TemplateLiteral' && node.quasis.length === 1) {
10
+ return node.quasis[0].value.cooked || '';
11
+ }
12
+ return '';
13
+ }
14
+
15
+ function isHttpMethod(prop) {
16
+ return HTTP_METHODS.includes(prop);
17
+ }
18
+
19
+ function getMemberObjectName(memberExpr) {
20
+ if (!memberExpr || memberExpr.type !== 'MemberExpression') return null;
21
+ if (memberExpr.object.type === 'Identifier') return memberExpr.object.name;
22
+ return null;
23
+ }
24
+
25
+ function getRouteCallInfo(callExpr) {
26
+ // router.route('/path')
27
+ if (!callExpr || callExpr.type !== 'CallExpression') return null;
28
+ if (callExpr.callee.type !== 'MemberExpression') return null;
29
+ const prop = callExpr.callee.property;
30
+ if (!prop || prop.type !== 'Identifier' || prop.name !== 'route') return null;
31
+ const routerName = getMemberObjectName(callExpr.callee);
32
+ const args = callExpr.arguments || [];
33
+ const routePath = getStringLiteral(args[0]);
34
+ if (!routerName || !routePath) return null;
35
+ return { routerName, routePath };
36
+ }
37
+
38
+ function collectRouterBases(ast) {
39
+ const baseMap = new Map();
40
+
41
+ function getBase(name) {
42
+ return baseMap.get(name) || '';
43
+ }
44
+
45
+ traverse(ast, {
46
+ CallExpression(path) {
47
+ const node = path.node;
48
+ if (node.callee.type !== 'MemberExpression') return;
49
+ const prop = node.callee.property;
50
+ if (!prop || prop.type !== 'Identifier' || prop.name !== 'use') return;
51
+ const ownerName = getMemberObjectName(node.callee);
52
+ if (!ownerName) return;
53
+ const args = node.arguments || [];
54
+ if (args.length < 2) return;
55
+ const prefix = getStringLiteral(args[0]);
56
+ const routerArg = args[1];
57
+ if (!prefix || !routerArg) return;
58
+ if (routerArg.type !== 'Identifier') return;
59
+ const routerName = routerArg.name;
60
+ const combined = joinPaths(getBase(ownerName), prefix);
61
+ baseMap.set(routerName, combined);
62
+ }
63
+ });
64
+
65
+ return baseMap;
66
+ }
67
+
68
+ async function extractExpressEndpoints(filePath) {
69
+ const ast = await parseFile(filePath);
70
+ const endpoints = [];
71
+ const routerBases = collectRouterBases(ast);
72
+ const schemaDefs = new Map();
73
+
74
+ // First pass: collect potential schemas
75
+ traverse(ast, {
76
+ VariableDeclarator(path) {
77
+ const node = path.node;
78
+ if (node.id.type === 'Identifier' && node.init) {
79
+ // Collect everything that looks like it could be a schema (Call/Member expression)
80
+ if (node.init.type === 'CallExpression' || node.init.type === 'MemberExpression') {
81
+ schemaDefs.set(node.id.name, node.init);
82
+ }
83
+ }
84
+ }
85
+ });
86
+
87
+ traverse(ast, {
88
+ CallExpression(path) {
89
+ const node = path.node;
90
+ if (node.callee.type !== 'MemberExpression') return;
91
+ const property = node.callee.property;
92
+ if (!property || property.type !== 'Identifier') return;
93
+ const methodName = property.name;
94
+
95
+ // Extract schemas from middleware args
96
+ function extractSchemasFromArgs(args, httpMethod) {
97
+ let body = null;
98
+ let query = [];
99
+
100
+ for (const arg of args) {
101
+ // Look for middleware calls: validate(schema)
102
+ if (arg.type === 'CallExpression') {
103
+ const middlewareArgs = arg.arguments || [];
104
+ if (middlewareArgs.length > 0) {
105
+ // Heuristic: check if the first arg resolves to a Zod schema
106
+ const possibleSchema = middlewareArgs[0];
107
+ const resolved = resolveZodSchema(possibleSchema, schemaDefs);
108
+
109
+ // If it resolves to something meaningful (not just string fallback)
110
+ // For now, resolveZodSchema defaults to {type:'string'} if parsing fails,
111
+ // but if it's an object with properties, it's likely a schema.
112
+ if (resolved && resolved.type === 'object' && resolved.properties) {
113
+ if (['GET', 'DELETE', 'HEAD'].includes(httpMethod)) {
114
+ // Likely query params
115
+ for (const [key, val] of Object.entries(resolved.properties)) {
116
+ query.push({ name: key, required: !val.optional });
117
+ }
118
+ } else {
119
+ // Likely body
120
+ body = resolved;
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ return { body, query };
127
+ }
128
+
129
+ if (isHttpMethod(methodName) && node.callee.object.type === 'Identifier') {
130
+ // Handle router.get('/path', ...)
131
+ const args = node.arguments || [];
132
+ if (!args.length) return;
133
+ const routePath = getStringLiteral(args[0]);
134
+ if (!routePath) return;
135
+
136
+ let base = '';
137
+ const ownerName = getMemberObjectName(node.callee);
138
+ if (ownerName && routerBases.has(ownerName)) {
139
+ base = routerBases.get(ownerName);
140
+ }
141
+
142
+ const method = methodName.toUpperCase();
143
+ const fullPath = normalizePath(joinPaths(base, routePath));
144
+
145
+ const { body, query } = extractSchemasFromArgs(args.slice(1), method);
146
+
147
+ endpoints.push({
148
+ method,
149
+ path: fullPath,
150
+ description: `${method} ${fullPath}`,
151
+ parameters: { body, query },
152
+ filePath,
153
+ key: toKey(method, fullPath)
154
+ });
155
+ return;
156
+ }
157
+
158
+ // Handle router.route('/path').get(...)
159
+ if (property.name && isHttpMethod(property.name) && node.callee.object.type === 'CallExpression') {
160
+ const owner = node.callee.object;
161
+ const routeInfo = getRouteCallInfo(owner);
162
+ if (!routeInfo) return;
163
+ const method = property.name.toUpperCase();
164
+ const base = routerBases.get(routeInfo.routerName) || '';
165
+ const fullPath = normalizePath(joinPaths(base, routeInfo.routePath));
166
+
167
+ const { body, query } = extractSchemasFromArgs(node.arguments, method);
168
+
169
+ endpoints.push({
170
+ method,
171
+ path: fullPath,
172
+ description: `${method} ${fullPath}`,
173
+ parameters: { body, query },
174
+ filePath,
175
+ key: toKey(method, fullPath)
176
+ });
177
+ }
178
+ }
179
+ });
180
+
181
+ return endpoints;
182
+ }
183
+
184
+ module.exports = { extractExpressEndpoints };
@@ -0,0 +1,522 @@
1
+ const traverse = require('@babel/traverse').default;
2
+ const path = require('path');
3
+ const fs = require('fs-extra');
4
+ const { parseFile } = require('./ast');
5
+ const { normalizePath, joinPaths, toKey, HTTP_METHODS } = require('../utils');
6
+ const { resolveZodSchema } = require('./zod');
7
+
8
+ const ROUTER_CLASS_NAMES = new Set(['Hono', 'OpenAPIHono']);
9
+ const HONO_METHODS = new Set([...HTTP_METHODS, 'all']);
10
+
11
+ function getStringLiteral(node) {
12
+ if (!node) return '';
13
+ if (node.type === 'StringLiteral') return node.value;
14
+ if (node.type === 'TemplateLiteral' && node.quasis.length === 1) {
15
+ return node.quasis[0].value.cooked || '';
16
+ }
17
+ return '';
18
+ }
19
+
20
+ function getPropertyName(node) {
21
+ if (!node) return null;
22
+ if (node.type === 'Identifier') return node.name;
23
+ return null;
24
+ }
25
+
26
+ function resolveImportFile(baseDir, source) {
27
+ const tryFiles = [];
28
+
29
+ if (path.isAbsolute(source)) {
30
+ tryFiles.push(source);
31
+ } else {
32
+ tryFiles.push(path.resolve(baseDir, source));
33
+ }
34
+
35
+ const ext = path.extname(source);
36
+ if (ext === '.js') {
37
+ tryFiles.push(path.resolve(baseDir, source.replace(/\.js$/, '.ts')));
38
+ tryFiles.push(path.resolve(baseDir, source.replace(/\.js$/, '.tsx')));
39
+ }
40
+ if (ext === '.jsx') {
41
+ tryFiles.push(path.resolve(baseDir, source.replace(/\.jsx$/, '.tsx')));
42
+ }
43
+
44
+ if (!ext) {
45
+ tryFiles.push(path.resolve(baseDir, `${source}.ts`));
46
+ tryFiles.push(path.resolve(baseDir, `${source}.tsx`));
47
+ tryFiles.push(path.resolve(baseDir, `${source}.js`));
48
+ tryFiles.push(path.resolve(baseDir, `${source}.jsx`));
49
+ tryFiles.push(path.resolve(baseDir, source, 'index.ts'));
50
+ tryFiles.push(path.resolve(baseDir, source, 'index.js'));
51
+ }
52
+
53
+ for (const candidate of tryFiles) {
54
+ if (fs.existsSync(candidate)) return candidate;
55
+ }
56
+ return null;
57
+ }
58
+
59
+ function parseCreateRoute(node) {
60
+ if (!node || node.type !== 'CallExpression') return null;
61
+ if (node.callee.type !== 'Identifier' || node.callee.name !== 'createRoute') return null;
62
+ const arg = node.arguments && node.arguments[0];
63
+ if (!arg || arg.type !== 'ObjectExpression') return null;
64
+ let method = null;
65
+ let routePath = null;
66
+ let summary = null;
67
+ for (const prop of arg.properties) {
68
+ if (prop.type !== 'ObjectProperty') continue;
69
+ if (prop.key.type !== 'Identifier') continue;
70
+ if (prop.key.name === 'method' && prop.value.type === 'StringLiteral') {
71
+ method = prop.value.value;
72
+ }
73
+ if (prop.key.name === 'path' && prop.value.type === 'StringLiteral') {
74
+ routePath = prop.value.value;
75
+ }
76
+ if (prop.key.name === 'summary' && prop.value.type === 'StringLiteral') {
77
+ summary = prop.value.value;
78
+ }
79
+ }
80
+ if (!method || !routePath) return null;
81
+ return { method: method.toUpperCase(), path: routePath, description: summary };
82
+ }
83
+
84
+ function parseOpenApiRouteArg(node, routeDefs) {
85
+ if (!node) return null;
86
+ if (node.type === 'Identifier' && routeDefs.has(node.name)) {
87
+ return routeDefs.get(node.name);
88
+ }
89
+ if (node.type === 'CallExpression') {
90
+ const parsed = parseCreateRoute(node);
91
+ if (parsed) return parsed;
92
+ }
93
+ if (node.type === 'ObjectExpression') {
94
+ let method = null;
95
+ let routePath = null;
96
+ for (const prop of node.properties) {
97
+ if (prop.type !== 'ObjectProperty') continue;
98
+ if (prop.key.type !== 'Identifier') continue;
99
+ if (prop.key.name === 'method' && prop.value.type === 'StringLiteral') {
100
+ method = prop.value.value;
101
+ }
102
+ if (prop.key.name === 'path' && prop.value.type === 'StringLiteral') {
103
+ routePath = prop.value.value;
104
+ }
105
+ }
106
+ if (method && routePath) return { method: method.toUpperCase(), path: routePath };
107
+ }
108
+ return null;
109
+ }
110
+
111
+ function parseCallChain(node) {
112
+ const calls = [];
113
+ let current = node;
114
+ while (current && current.type === 'CallExpression' && current.callee.type === 'MemberExpression') {
115
+ const propName = getPropertyName(current.callee.property);
116
+ if (!propName) break;
117
+ calls.unshift({ name: propName, args: current.arguments || [] });
118
+ current = current.callee.object;
119
+ }
120
+ return { root: current, calls };
121
+ }
122
+
123
+ function isHonoNewExpression(node) {
124
+ if (!node || node.type !== 'NewExpression') return false;
125
+ if (node.callee.type !== 'Identifier') return false;
126
+ return ROUTER_CLASS_NAMES.has(node.callee.name);
127
+ }
128
+
129
+ function recordBasePath(routers, routerVar, basePath) {
130
+ const existing = routers.get(routerVar) || { basePath: '' };
131
+ if (!basePath) return;
132
+ existing.basePath = normalizePath(joinPaths(existing.basePath, basePath));
133
+ routers.set(routerVar, existing);
134
+ }
135
+
136
+ function parseMethodsFromOn(node) {
137
+ if (!node || node.type !== 'CallExpression') return [];
138
+ const args = node.arguments || [];
139
+ const methodsArg = args[0];
140
+ const methods = [];
141
+ if (!methodsArg) return methods;
142
+ if (methodsArg.type === 'StringLiteral') {
143
+ methods.push(methodsArg.value.toUpperCase());
144
+ } else if (methodsArg.type === 'ArrayExpression') {
145
+ for (const el of methodsArg.elements || []) {
146
+ if (el && el.type === 'StringLiteral') methods.push(el.value.toUpperCase());
147
+ }
148
+ }
149
+ return methods;
150
+ }
151
+
152
+ function parseValidatorArgs(args, schemaDefs) {
153
+ // zValidator('json', schema)
154
+ if (args.length < 2) return null;
155
+ const target = getStringLiteral(args[0]);
156
+ if (!['json', 'form', 'query', 'param'].includes(target)) return null;
157
+
158
+ const schemaNode = args[1];
159
+ const schema = resolveZodSchema(schemaNode, schemaDefs);
160
+ return { target, schema };
161
+ }
162
+
163
+ async function parseHonoFile(filePath) {
164
+ const ast = await parseFile(filePath);
165
+ const routers = new Map();
166
+ const endpoints = [];
167
+ const mounts = [];
168
+ const exports = { default: null, named: new Map() };
169
+ const imports = new Map();
170
+ const routeDefs = new Map();
171
+ const schemaDefs = new Map(); // Store Zod schemas defined in variables
172
+
173
+ traverse(ast, {
174
+ ImportDeclaration(p) {
175
+ const node = p.node;
176
+ const source = node.source.value;
177
+ if (!source || !source.startsWith('.')) return;
178
+ const baseDir = path.dirname(filePath);
179
+ const resolved = resolveImportFile(baseDir, source);
180
+ if (!resolved) return;
181
+ for (const spec of node.specifiers || []) {
182
+ if (spec.type === 'ImportDefaultSpecifier') {
183
+ imports.set(spec.local.name, { sourceFile: resolved, importName: 'default', isDefault: true });
184
+ }
185
+ if (spec.type === 'ImportSpecifier') {
186
+ imports.set(spec.local.name, { sourceFile: resolved, importName: spec.imported.name, isDefault: false });
187
+ }
188
+ }
189
+ },
190
+
191
+ ExportDefaultDeclaration(path) {
192
+ const node = path.node;
193
+ if (node.declaration && node.declaration.type === 'Identifier') {
194
+ exports.default = node.declaration.name;
195
+ }
196
+ },
197
+
198
+ ExportNamedDeclaration(path) {
199
+ const node = path.node;
200
+ if (node.declaration && node.declaration.type === 'VariableDeclaration') {
201
+ for (const decl of node.declaration.declarations) {
202
+ if (decl.id.type === 'Identifier') {
203
+ exports.named.set(decl.id.name, decl.id.name);
204
+ }
205
+ }
206
+ }
207
+ for (const spec of node.specifiers || []) {
208
+ if (spec.type === 'ExportSpecifier') {
209
+ exports.named.set(spec.exported.name, spec.local.name);
210
+ }
211
+ }
212
+ },
213
+
214
+ VariableDeclarator(path) {
215
+ const node = path.node;
216
+ if (node.id.type !== 'Identifier') return;
217
+ const varName = node.id.name;
218
+ const init = node.init;
219
+ if (!init) return;
220
+
221
+ if (init.type === 'CallExpression' || init.type === 'MemberExpression') {
222
+ schemaDefs.set(varName, init);
223
+ }
224
+
225
+ const createRouteParsed = parseCreateRoute(init);
226
+ if (createRouteParsed) {
227
+ routeDefs.set(varName, createRouteParsed);
228
+ return;
229
+ }
230
+
231
+ if (init.type === 'NewExpression' && isHonoNewExpression(init)) {
232
+ routers.set(varName, { basePath: '' });
233
+ return;
234
+ }
235
+
236
+ if (init.type === 'CallExpression') {
237
+ const { root, calls } = parseCallChain(init);
238
+ if (isHonoNewExpression(root)) {
239
+ routers.set(varName, { basePath: '' });
240
+ for (const call of calls) {
241
+ if (call.name === 'basePath') {
242
+ const bp = getStringLiteral(call.args[0]);
243
+ recordBasePath(routers, varName, bp);
244
+ }
245
+ if (HONO_METHODS.has(call.name)) {
246
+ const method = call.name === 'all' ? 'ALL' : call.name.toUpperCase();
247
+ const routePath = getStringLiteral(call.args[0]);
248
+ if (routePath) {
249
+ const params = {};
250
+ for (const arg of call.args.slice(1)) {
251
+ if (arg.type === 'CallExpression' && arg.callee.name === 'zValidator') {
252
+ const res = parseValidatorArgs(arg.arguments, schemaDefs);
253
+ if (res) {
254
+ if (res.target === 'json' || res.target === 'form') params.body = res.schema;
255
+ if (res.target === 'query') {
256
+ const queryParams = [];
257
+ if (res.schema.properties) {
258
+ for (const [key, val] of Object.entries(res.schema.properties)) {
259
+ queryParams.push({ name: key, required: !val.optional });
260
+ }
261
+ }
262
+ params.query = queryParams;
263
+ }
264
+ }
265
+ }
266
+ }
267
+
268
+ endpoints.push({
269
+ routerVar: varName,
270
+ method,
271
+ path: routePath,
272
+ description: `${method} ${routePath}`,
273
+ parameters: params
274
+ });
275
+ }
276
+ }
277
+ if (call.name === 'on') {
278
+ const methods = parseMethodsFromOn({ arguments: call.args });
279
+ const routePath = getStringLiteral(call.args[1]);
280
+ for (const method of methods) {
281
+ endpoints.push({ routerVar: varName, method, path: routePath, description: `${method} ${routePath}` });
282
+ }
283
+ }
284
+ if (call.name === 'openapi') {
285
+ const route = parseOpenApiRouteArg(call.args[0], routeDefs);
286
+ if (route) {
287
+ endpoints.push({
288
+ routerVar: varName,
289
+ method: route.method,
290
+ path: route.path,
291
+ description: route.description || `${route.method} ${route.path}`
292
+ });
293
+ }
294
+ }
295
+ }
296
+ }
297
+ }
298
+ },
299
+
300
+ CallExpression(path) {
301
+ const node = path.node;
302
+ if (node.callee.type !== 'MemberExpression') return;
303
+ const prop = node.callee.property;
304
+ if (prop.type !== 'Identifier') return;
305
+ const methodName = prop.name;
306
+
307
+ if (node.callee.object.type === 'Identifier') {
308
+ const routerVar = node.callee.object.name;
309
+
310
+ if (methodName === 'basePath') {
311
+ if (!routers.has(routerVar)) routers.set(routerVar, { basePath: '' });
312
+ const bp = getStringLiteral(node.arguments[0]);
313
+ if (bp) recordBasePath(routers, routerVar, bp);
314
+ return;
315
+ }
316
+
317
+ if (methodName === 'route') {
318
+ if (!routers.has(routerVar)) routers.set(routerVar, { basePath: '' });
319
+ const prefix = getStringLiteral(node.arguments[0]);
320
+ const child = node.arguments[1];
321
+ if (prefix && child && child.type === 'Identifier') {
322
+ mounts.push({ parentVar: routerVar, childIdent: child.name, prefix });
323
+ }
324
+ return;
325
+ }
326
+
327
+ if (HONO_METHODS.has(methodName)) {
328
+ if (!routers.has(routerVar)) routers.set(routerVar, { basePath: '' });
329
+ const method = methodName === 'all' ? 'ALL' : methodName.toUpperCase();
330
+ const routePath = getStringLiteral(node.arguments[0]);
331
+ if (routePath) {
332
+ const params = {};
333
+ for (const arg of node.arguments.slice(1)) {
334
+ if (arg.type === 'CallExpression' && arg.callee.name === 'zValidator') {
335
+ const res = parseValidatorArgs(arg.arguments, schemaDefs);
336
+ if (res) {
337
+ if (res.target === 'json' || res.target === 'form') params.body = res.schema;
338
+ if (res.target === 'query') {
339
+ const queryParams = [];
340
+ if (res.schema.properties) {
341
+ for (const [key, val] of Object.entries(res.schema.properties)) {
342
+ queryParams.push({ name: key, required: !val.optional });
343
+ }
344
+ }
345
+ params.query = queryParams;
346
+ }
347
+ }
348
+ }
349
+ }
350
+
351
+ endpoints.push({
352
+ routerVar,
353
+ method,
354
+ path: routePath,
355
+ description: `${method} ${routePath}`,
356
+ parameters: params
357
+ });
358
+ }
359
+ return;
360
+ }
361
+
362
+ if (methodName === 'on') {
363
+ if (!routers.has(routerVar)) routers.set(routerVar, { basePath: '' });
364
+ const methods = parseMethodsFromOn(node);
365
+ const routePath = getStringLiteral(node.arguments[1]);
366
+ for (const method of methods) {
367
+ endpoints.push({ routerVar, method, path: routePath, description: `${method} ${routePath}` });
368
+ }
369
+ return;
370
+ }
371
+
372
+ if (methodName === 'openapi') {
373
+ if (!routers.has(routerVar)) routers.set(routerVar, { basePath: '' });
374
+ const route = parseOpenApiRouteArg(node.arguments[0], routeDefs);
375
+ if (route) {
376
+ endpoints.push({
377
+ routerVar,
378
+ method: route.method,
379
+ path: route.path,
380
+ description: route.description || `${route.method} ${route.path}`
381
+ });
382
+ }
383
+ }
384
+ }
385
+ }
386
+ });
387
+
388
+ return { filePath, routers, endpoints, mounts, exports, imports };
389
+ }
390
+
391
+ function routerId(filePath, varName) {
392
+ return `${filePath}::${varName}`;
393
+ }
394
+
395
+ function buildRouterIndex(fileDataMap) {
396
+ const routers = new Map();
397
+
398
+ for (const data of fileDataMap.values()) {
399
+ for (const [varName, meta] of data.routers.entries()) {
400
+ routers.set(routerId(data.filePath, varName), {
401
+ filePath: data.filePath,
402
+ varName,
403
+ basePath: meta.basePath || ''
404
+ });
405
+ }
406
+ }
407
+
408
+ return routers;
409
+ }
410
+
411
+ function resolveChildRouterId(childIdent, data, fileDataMap) {
412
+ if (data.routers.has(childIdent)) {
413
+ return routerId(data.filePath, childIdent);
414
+ }
415
+
416
+ if (data.imports.has(childIdent)) {
417
+ const imp = data.imports.get(childIdent);
418
+ const target = fileDataMap.get(imp.sourceFile);
419
+ if (!target) return null;
420
+ if (imp.isDefault) {
421
+ if (target.exports.default) return routerId(target.filePath, target.exports.default);
422
+ return null;
423
+ }
424
+ const mapped = target.exports.named.get(imp.importName);
425
+ if (mapped) return routerId(target.filePath, mapped);
426
+ }
427
+
428
+ return null;
429
+ }
430
+
431
+ function addPrefix(prefixesByRouter, routerIdValue, prefix) {
432
+ const set = prefixesByRouter.get(routerIdValue) || new Set();
433
+ if (set.has(prefix)) return false;
434
+ set.add(prefix);
435
+ prefixesByRouter.set(routerIdValue, set);
436
+ return true;
437
+ }
438
+
439
+ async function extractHonoEndpoints(files) {
440
+ const fileDataMap = new Map();
441
+ for (const file of files) {
442
+ try {
443
+ const data = await parseHonoFile(file);
444
+ fileDataMap.set(file, data);
445
+ } catch (err) {
446
+ // Ignore parse errors here; handled at caller level
447
+ }
448
+ }
449
+
450
+ const routers = buildRouterIndex(fileDataMap);
451
+ const endpointsByRouter = new Map();
452
+ const edges = new Map();
453
+ const childHasParent = new Set();
454
+
455
+ for (const data of fileDataMap.values()) {
456
+ for (const endpoint of data.endpoints) {
457
+ const id = routerId(data.filePath, endpoint.routerVar);
458
+ if (!endpointsByRouter.has(id)) endpointsByRouter.set(id, []);
459
+ endpointsByRouter.get(id).push(endpoint);
460
+ }
461
+
462
+ for (const mount of data.mounts) {
463
+ const parentId = routerId(data.filePath, mount.parentVar);
464
+ const childId = resolveChildRouterId(mount.childIdent, data, fileDataMap);
465
+ if (!childId) continue;
466
+ if (!edges.has(parentId)) edges.set(parentId, []);
467
+ edges.get(parentId).push({ childId, prefix: mount.prefix });
468
+ childHasParent.add(childId);
469
+ }
470
+ }
471
+
472
+ const prefixesByRouter = new Map();
473
+
474
+ const routerIds = Array.from(routers.keys());
475
+ const roots = routerIds.filter((id) => !childHasParent.has(id));
476
+ const start = roots.length ? roots : routerIds;
477
+
478
+ const queue = [];
479
+ for (const id of start) {
480
+ if (addPrefix(prefixesByRouter, id, '')) {
481
+ queue.push({ id, prefix: '' });
482
+ }
483
+ }
484
+
485
+ while (queue.length) {
486
+ const { id, prefix } = queue.shift();
487
+ const router = routers.get(id);
488
+ if (!router) continue;
489
+ const effective = joinPaths(prefix, router.basePath || '');
490
+ const children = edges.get(id) || [];
491
+ for (const edge of children) {
492
+ const childPrefix = joinPaths(effective, edge.prefix);
493
+ if (addPrefix(prefixesByRouter, edge.childId, childPrefix)) {
494
+ queue.push({ id: edge.childId, prefix: childPrefix });
495
+ }
496
+ }
497
+ }
498
+
499
+ const results = [];
500
+ for (const [id, list] of endpointsByRouter.entries()) {
501
+ const router = routers.get(id);
502
+ const prefixes = prefixesByRouter.get(id) || new Set(['']);
503
+ for (const prefix of prefixes) {
504
+ const effective = joinPaths(prefix, router ? router.basePath || '' : '');
505
+ for (const endpoint of list) {
506
+ const fullPath = normalizePath(joinPaths(effective, endpoint.path));
507
+ results.push({
508
+ method: endpoint.method,
509
+ path: fullPath,
510
+ description: endpoint.description || `${endpoint.method} ${fullPath}`,
511
+ parameters: endpoint.parameters || {},
512
+ filePath: router ? router.filePath : undefined,
513
+ key: toKey(endpoint.method, fullPath)
514
+ });
515
+ }
516
+ }
517
+ }
518
+
519
+ return results;
520
+ }
521
+
522
+ module.exports = { extractHonoEndpoints };