33 * Handles finding FastAPI apps via pyproject.toml, VS Code settings, or automatic detection.
44 */
55
6- import { existsSync } from "node:fs"
7- import { isAbsolute , sep } from "node:path"
86import * as toml from "toml"
97import * as vscode from "vscode"
108import type { EntryPoint } from "./core/internal"
119import type { Parser } from "./core/parser"
12- import { findProjectRoot } from "./core/pathUtils"
10+ import { findProjectRoot , uriPath } from "./core/pathUtils"
1311import { buildRouterGraph } from "./core/routerResolver"
1412import { routerNodeToAppDefinition } from "./core/transformer"
1513import type { AppDefinition } from "./core/types"
14+ import { vscodeFileSystem } from "./providers/vscodeFileSystem"
15+ import { log } from "./utils/logger"
1616
1717export type { EntryPoint }
1818
1919/**
2020 * Scans for common FastAPI entry point files (main.py, __init__.py).
21- * Returns paths sorted by depth (shallower first).
21+ * Returns URI strings sorted by depth (shallower first).
2222 */
2323async function automaticDetectEntryPoints (
24- folderPath : string ,
24+ folder : vscode . WorkspaceFolder ,
2525) : Promise < string [ ] > {
2626 const [ mainFiles , initFiles ] = await Promise . all ( [
2727 vscode . workspace . findFiles (
28- new vscode . RelativePattern ( folderPath , "**/main.py" ) ,
28+ new vscode . RelativePattern ( folder , "**/main.py" ) ,
2929 ) ,
3030 vscode . workspace . findFiles (
31- new vscode . RelativePattern ( folderPath , "**/__init__.py" ) ,
31+ new vscode . RelativePattern ( folder , "**/__init__.py" ) ,
3232 ) ,
3333 ] )
3434
3535 return [ ...mainFiles , ...initFiles ]
36- . map ( ( uri ) => uri . fsPath )
37- . sort ( ( a , b ) => a . split ( sep ) . length - b . split ( sep ) . length )
36+ . map ( ( uri ) => uri . toString ( ) )
37+ . sort ( ( a , b ) => uriPath ( a ) . split ( "/" ) . length - uriPath ( b ) . split ( "/" ) . length )
3838}
3939
4040/**
4141 * Parses pyproject.toml to find a defined entrypoint.
4242 * Supports module:variable notation, e.g. "my_app.main:app"
4343 */
4444async function parsePyprojectForEntryPoint (
45- folderPath : string ,
45+ folderUri : vscode . Uri ,
4646) : Promise < EntryPoint | null > {
47- const pyprojectPath = vscode . Uri . joinPath (
48- vscode . Uri . file ( folderPath ) ,
49- "pyproject.toml" ,
50- )
47+ const pyprojectUri = vscode . Uri . joinPath ( folderUri , "pyproject.toml" )
5148
52- if ( ! existsSync ( pyprojectPath . fsPath ) ) {
49+ if ( ! ( await vscodeFileSystem . exists ( pyprojectUri . toString ( ) ) ) ) {
5350 return null
5451 }
5552
5653 try {
57- const document = await vscode . workspace . openTextDocument ( pyprojectPath )
54+ const document = await vscode . workspace . openTextDocument ( pyprojectUri )
5855 const contents = toml . parse ( document . getText ( ) ) as Record < string , unknown >
5956
6057 const entrypoint = ( contents . tool as Record < string , unknown > | undefined )
@@ -73,13 +70,12 @@ async function parsePyprojectForEntryPoint(
7370 colonIndex === - 1 ? undefined : entrypointValue . slice ( colonIndex + 1 )
7471
7572 // Convert module path to file path: my_app.main -> my_app/main.py
76- const relativePath = `${ modulePath . replace ( / \. / g, sep ) } .py`
77- const fullPath = vscode . Uri . joinPath (
78- vscode . Uri . file ( folderPath ) ,
79- relativePath ,
80- ) . fsPath
73+ const relativePath = `${ modulePath . replace ( / \. / g, "/" ) } .py`
74+ const fullUri = vscode . Uri . joinPath ( folderUri , relativePath )
8175
82- return existsSync ( fullPath ) ? { filePath : fullPath , variableName } : null
76+ return ( await vscodeFileSystem . exists ( fullUri . toString ( ) ) )
77+ ? { filePath : fullUri . toString ( ) , variableName }
78+ : null
8379 } catch {
8480 // Invalid TOML syntax - silently fall back to auto-detection
8581 return null
@@ -94,7 +90,14 @@ export async function discoverFastAPIApps(
9490 parser : Parser ,
9591) : Promise < AppDefinition [ ] > {
9692 const workspaceFolders = vscode . workspace . workspaceFolders
97- if ( ! workspaceFolders ) return [ ]
93+ if ( ! workspaceFolders ) {
94+ log ( "No workspace folders found" )
95+ return [ ]
96+ }
97+
98+ log (
99+ `Discovering FastAPI apps in ${ workspaceFolders . length } workspace folder(s)...` ,
100+ )
98101
99102 const apps : AppDefinition [ ] = [ ]
100103
@@ -106,53 +109,77 @@ export async function discoverFastAPIApps(
106109
107110 // If user specified an entry point in settings, use that
108111 if ( customEntryPoint ) {
109- const entryPath = isAbsolute ( customEntryPoint )
110- ? customEntryPoint
111- : vscode . Uri . joinPath ( folder . uri , customEntryPoint ) . fsPath
112+ const entryUri = customEntryPoint . startsWith ( "/" )
113+ ? vscode . Uri . file ( customEntryPoint )
114+ : vscode . Uri . joinPath ( folder . uri , customEntryPoint )
112115
113- if ( ! existsSync ( entryPath ) ) {
116+ if ( ! ( await vscodeFileSystem . exists ( entryUri . toString ( ) ) ) ) {
117+ log ( `Custom entry point not found: ${ customEntryPoint } ` )
114118 vscode . window . showWarningMessage (
115119 `FastAPI entry point not found: ${ customEntryPoint } ` ,
116120 )
117121 continue
118122 }
119123
120- candidates = [ { filePath : entryPath } ]
124+ log ( `Using custom entry point: ${ customEntryPoint } ` )
125+ candidates = [ { filePath : entryUri . toString ( ) } ]
121126 } else {
122127 // Otherwise, check pyproject.toml or auto-detect
123- const pyprojectEntry = await parsePyprojectForEntryPoint (
124- folder . uri . fsPath ,
125- )
126- candidates = pyprojectEntry
127- ? [ pyprojectEntry ]
128- : ( await automaticDetectEntryPoints ( folder . uri . fsPath ) ) . map (
129- ( filePath ) => ( { filePath } ) ,
130- )
128+ const pyprojectEntry = await parsePyprojectForEntryPoint ( folder . uri )
129+ if ( pyprojectEntry ) {
130+ candidates = [ pyprojectEntry ]
131+ } else {
132+ const detected = await automaticDetectEntryPoints ( folder )
133+ candidates = detected . map ( ( filePath ) => ( { filePath } ) )
134+ log (
135+ `Found ${ candidates . length } candidate entry file(s) in ${ folder . name } ` ,
136+ )
137+ }
131138
132139 // If no candidates found, try the active editor as a last resort
133140 if ( candidates . length === 0 ) {
134141 const activeEditor = vscode . window . activeTextEditor
135142 if ( activeEditor ?. document . languageId === "python" ) {
136- candidates = [ { filePath : activeEditor . document . uri . fsPath } ]
143+ candidates = [ { filePath : activeEditor . document . uri . toString ( ) } ]
137144 }
138145 }
139146 }
140147
141148 for ( const candidate of candidates ) {
142- const projectRoot = findProjectRoot ( candidate . filePath , folder . uri . fsPath )
143- const routerNode = buildRouterGraph (
149+ const projectRoot = await findProjectRoot (
150+ candidate . filePath ,
151+ folder . uri . toString ( ) ,
152+ vscodeFileSystem ,
153+ )
154+ const routerNode = await buildRouterGraph (
144155 candidate . filePath ,
145156 parser ,
146157 projectRoot ,
158+ vscodeFileSystem ,
147159 candidate . variableName ,
148160 )
149161
150162 if ( routerNode ) {
151- apps . push ( routerNodeToAppDefinition ( routerNode , folder . uri . fsPath ) )
163+ const app = routerNodeToAppDefinition ( routerNode , folder . uri . fsPath )
164+ // Count all routes: direct routes + routes in all routers (recursively)
165+ const countRoutes = ( routers : typeof app . routers ) : number =>
166+ routers . reduce (
167+ ( sum , r ) => sum + r . routes . length + countRoutes ( r . children ) ,
168+ 0 ,
169+ )
170+ const totalRoutes = app . routes . length + countRoutes ( app . routers )
171+ log (
172+ `Found FastAPI app "${ app . name } " with ${ totalRoutes } route(s) in ${ app . routers . length } router(s)` ,
173+ )
174+ apps . push ( app )
152175 break
153176 }
154177 }
155178 }
156179
180+ if ( apps . length === 0 ) {
181+ log ( "No FastAPI apps found in workspace" )
182+ }
183+
157184 return apps
158185}
0 commit comments