Skip to content

Commit f4e558d

Browse files
🐛 Fix discovery when multiple routers are imported from the same file (#129)
1 parent 0fa56a8 commit f4e558d

File tree

5 files changed

+65
-7
lines changed

5 files changed

+65
-7
lines changed

src/core/routerResolver.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,13 @@ async function buildRouterGraphInternal(
122122
}
123123

124124
// Prevent infinite recursion on circular imports
125-
if (visited.has(entryFileUri)) {
126-
log(`Skipping already visited file: "${entryFileUri}"`)
125+
const visitedKey = `${entryFileUri}#${targetVariable ?? ""}`
126+
if (visited.has(visitedKey)) {
127+
log(`Skipping already visited: "${visitedKey}"`)
127128
return null
128129
}
129130

130-
visited.add(entryFileUri)
131+
visited.add(visitedKey)
131132

132133
// Helper to analyze a file with the filesystem
133134
const analyzeFileFn = (uri: string) => analyzeFile(uri, parser, fs)
@@ -361,11 +362,13 @@ async function resolveRouterReference(
361362
(r) => r.variableName === attributeName,
362363
)
363364
if (targetRouter) {
365+
const visitedKey = `${importedFileUri}#${attributeName}`
366+
364367
// Mark as visited to prevent infinite recursion
365-
if (visited.has(importedFileUri)) {
368+
if (visited.has(visitedKey)) {
366369
return null
367370
}
368-
visited.add(importedFileUri)
371+
visited.add(visitedKey)
369372

370373
// Get routes belonging to this router
371374
const routerRoutes = importedAnalysis.routes.filter(
@@ -387,8 +390,9 @@ async function resolveRouterReference(
387390

388391
return routerNode
389392
}
390-
// If not found as a router, fall through to try building from file
391393
}
392394

393-
return buildRouterGraphInternal(importedFileUri, ctx)
395+
// Resolve by variable name for named imports, or fall back to full-file discovery
396+
const targetVarName = namedImport ? originalName : undefined
397+
return buildRouterGraphInternal(importedFileUri, ctx, targetVarName)
394398
}

src/test/core/routerResolver.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,5 +678,37 @@ suite("routerResolver", () => {
678678
assert.strictEqual(usersRouter.prefix, "/users")
679679
assert.strictEqual(usersRouter.routes.length, 2)
680680
})
681+
682+
test("resolves multiple routers imported from same file", async () => {
683+
const result = await buildRouterGraph(
684+
fixtures.multiRouterSameFile.mainPy,
685+
parser,
686+
fixtures.multiRouterSameFile.root,
687+
nodeFileSystem,
688+
)
689+
690+
assert.ok(result)
691+
assert.strictEqual(result.type, "FastAPI")
692+
assert.strictEqual(
693+
result.children.length,
694+
2,
695+
"Should resolve both routers from same file",
696+
)
697+
698+
const prefixes = result.children.map((c) => c.router.prefix)
699+
assert.ok(
700+
prefixes.includes("/v1"),
701+
"Should include router1 with /v1 prefix",
702+
)
703+
assert.ok(
704+
prefixes.includes("/v2"),
705+
"Should include router2 with /v2 prefix",
706+
)
707+
708+
for (const child of result.children) {
709+
assert.strictEqual(child.router.routes.length, 1)
710+
assert.strictEqual(child.router.routes[0].path, "/items")
711+
}
712+
})
681713
})
682714
})
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from fastapi import FastAPI
2+
from .routers import router1, router2
3+
4+
app = FastAPI()
5+
app.include_router(router1)
6+
app.include_router(router2)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from fastapi import APIRouter
2+
3+
router1 = APIRouter(prefix="/v1")
4+
router2 = APIRouter(prefix="/v2")
5+
6+
@router1.get("/items")
7+
def get_items_v1():
8+
pass
9+
10+
@router2.get("/items")
11+
def get_items_v2():
12+
pass

src/test/testUtils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ export const fixtures = {
6565
root: uri(join(fixturesPath, "multi-app")),
6666
mainPy: uri(join(fixturesPath, "multi-app", "main.py")),
6767
},
68+
multiRouterSameFile: {
69+
root: uri(join(fixturesPath, "multi-router-same-file")),
70+
mainPy: uri(join(fixturesPath, "multi-router-same-file", "main.py")),
71+
},
6872
namespace: {
6973
root: uri(join(fixturesPath, "namespace")),
7074
mainPy: uri(join(fixturesPath, "namespace", "app", "main.py")),

0 commit comments

Comments
 (0)