Skip to content

Commit 7a233eb

Browse files
Remove non-dynamic prefix stripping
1 parent 4a638d7 commit 7a233eb

6 files changed

Lines changed: 73 additions & 47 deletions

File tree

src/core/extractors.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,12 +333,20 @@ export function includeRouterExtractor(node: Node): IncludeRouterInfo | null {
333333
const router = routerArg ? routerArg.text : ""
334334

335335
let prefix = ""
336+
const tags: string[] = []
336337
for (const argNode of argumentsNode.namedChildren) {
337338
if (argNode.type === "keyword_argument") {
338339
const nameNode = argNode.childForFieldName("name")
339340
const valueNode = argNode.childForFieldName("value")
340341
if (nameNode?.text === "prefix" && valueNode) {
341342
prefix = extractPathFromNode(valueNode)
343+
} else if (nameNode?.text === "tags" && valueNode?.type === "list") {
344+
for (const elem of valueNode.namedChildren) {
345+
const tagValue = extractStringValue(elem)
346+
if (tagValue !== null) {
347+
tags.push(tagValue)
348+
}
349+
}
342350
}
343351
}
344352
}
@@ -347,6 +355,7 @@ export function includeRouterExtractor(node: Node): IncludeRouterInfo | null {
347355
object: objectNode.text,
348356
router,
349357
prefix,
358+
tags,
350359
}
351360
}
352361

src/core/internal.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export interface IncludeRouterInfo {
6969
object: string
7070
router: string
7171
prefix: string
72+
tags: string[]
7273
}
7374

7475
export interface MountInfo {
@@ -101,5 +102,5 @@ export interface RouterNode {
101102
line: number
102103
column: number
103104
}[]
104-
children: { router: RouterNode; prefix: string }[]
105+
children: { router: RouterNode; prefix: string; tags: string[] }[]
105106
}

src/core/pathUtils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { dirname, join, relative, sep } from "node:path"
33

44
/**
55
* Strips leading dynamic segments (like {settings.API_V1_STR}) from a path.
6-
* These are runtime variables, not URL path parameters.
76
*
87
* Examples:
98
* "{settings.API_V1_STR}/users/{id}" -> "/users/{id}"

src/core/routerResolver.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,14 @@ function buildRouterGraphInternal(
117117
visited,
118118
)
119119
if (childRouter) {
120+
// Merge tags from include_router call with the router's own tags
121+
if (include.tags.length > 0) {
122+
childRouter.tags = [...new Set([...childRouter.tags, ...include.tags])]
123+
}
120124
rootRouter.children.push({
121125
router: childRouter,
122126
prefix: include.prefix,
127+
tags: include.tags,
123128
})
124129
}
125130
}
@@ -138,6 +143,7 @@ function buildRouterGraphInternal(
138143
rootRouter.children.push({
139144
router: childRouter,
140145
prefix: mount.path,
146+
tags: [],
141147
})
142148
}
143149
}

src/core/transformer.ts

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ function buildPrefixHierarchy(
107107
const strippedPrefix = stripLeadingDynamicSegments(router.prefix)
108108
const segmentCount = countSegments(strippedPrefix)
109109

110-
// Skip routers with no meaningful prefix (root level)
110+
// Handle routers with no meaningful prefix
111111
if (segmentCount === 0) {
112112
rootRouters.push(router)
113113
prefixToRouter.set(strippedPrefix, router)
@@ -147,30 +147,53 @@ function buildPrefixHierarchy(
147147
let groupRouter = prefixToRouter.get(groupPrefix)
148148

149149
if (!groupRouter) {
150-
// Check if there are other routers that would be siblings under this group
151-
const wouldHaveSiblings = sorted.some((other) => {
152-
if (other === router) return false
153-
const otherPrefix = stripLeadingDynamicSegments(other.prefix)
154-
const otherSegments = countSegments(otherPrefix)
150+
// Check if there's a root-level router with matching tag that should be the parent
151+
const matchingRootRouter = rootRouters.find((r) => {
152+
if (r === router) return false
153+
// Router has no prefix but has a tag matching this group
154+
const rPrefix = stripLeadingDynamicSegments(r.prefix)
155155
return (
156-
otherSegments >= 2 &&
157-
getPathSegments(otherPrefix, 1) === groupPrefix &&
158-
otherPrefix !== strippedPrefix
156+
countSegments(rPrefix) === 0 &&
157+
r.tags.length > 0 &&
158+
`/${r.tags[0]}` === groupPrefix
159159
)
160160
})
161161

162-
if (wouldHaveSiblings) {
163-
// Create a synthetic group router
164-
groupRouter = {
165-
name: groupPrefix.replace(/^\//, ""),
166-
prefix: groupPrefix,
167-
tags: [],
168-
location: router.location,
169-
routes: [],
170-
children: [],
162+
if (matchingRootRouter) {
163+
// Use this router as the group parent
164+
groupRouter = matchingRootRouter
165+
// Remove from root and re-register under the group prefix
166+
const idx = rootRouters.indexOf(matchingRootRouter)
167+
if (idx !== -1) rootRouters.splice(idx, 1)
168+
matchingRootRouter.prefix = groupPrefix
169+
prefixToRouter.set(groupPrefix, matchingRootRouter)
170+
rootRouters.push(matchingRootRouter)
171+
} else {
172+
// Check if there are other routers that would be siblings under this group
173+
const wouldHaveSiblings = sorted.some((other) => {
174+
if (other === router) return false
175+
const otherPrefix = stripLeadingDynamicSegments(other.prefix)
176+
const otherSegments = countSegments(otherPrefix)
177+
return (
178+
otherSegments >= 2 &&
179+
getPathSegments(otherPrefix, 1) === groupPrefix &&
180+
otherPrefix !== strippedPrefix
181+
)
182+
})
183+
184+
if (wouldHaveSiblings) {
185+
// Create a synthetic group router
186+
groupRouter = {
187+
name: groupPrefix.replace(/^\//, ""),
188+
prefix: groupPrefix,
189+
tags: [],
190+
location: router.location,
191+
routes: [],
192+
children: [],
193+
}
194+
prefixToRouter.set(groupPrefix, groupRouter)
195+
rootRouters.push(groupRouter)
171196
}
172-
prefixToRouter.set(groupPrefix, groupRouter)
173-
rootRouters.push(groupRouter)
174197
}
175198
}
176199

src/providers/EndpointTreeProvider.ts

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -325,12 +325,11 @@ export class EndpointTreeProvider
325325
}
326326

327327
case "router": {
328-
// Use prefix as label (stripped of dynamic segments), or first tag, or filename as fallback
328+
// Use prefix as label, showing relative path if nested under a parent
329329
const strippedPrefix = stripLeadingDynamicSegments(
330330
element.router.prefix,
331331
)
332332

333-
// If nested under a parent router, show only the relative part
334333
const parentRouter = this.findParentRouter(element.router)
335334
const parentPrefix = parentRouter
336335
? stripLeadingDynamicSegments(parentRouter.prefix)
@@ -339,15 +338,18 @@ export class EndpointTreeProvider
339338

340339
let routerLabel = displayPrefix !== "/" ? displayPrefix : ""
341340
if (!routerLabel) {
341+
// Fallback: use tag, then filename
342342
if (element.router.tags.length > 0) {
343-
// Add / prefix to tag-based labels for consistency
344343
routerLabel = `/${element.router.tags[0]}`
345344
} else {
346-
// Use filename as label (e.g., "api_routes" from "routes/api_routes.py")
347-
const filePath = element.router.location.filePath
348-
const fileName =
349-
filePath.split("/").pop()?.replace(/\.py$/, "") ?? ""
350-
routerLabel = fileName
345+
const parts = element.router.location.filePath.split("/")
346+
const fileName = parts.pop()?.replace(/\.py$/, "") ?? ""
347+
// Use parent directory for generic filenames
348+
if (fileName === "router" || fileName === "routes") {
349+
routerLabel = parts.pop() ?? fileName
350+
} else {
351+
routerLabel = fileName
352+
}
351353
}
352354
}
353355
const routerItem = new TreeItem(
@@ -369,23 +371,9 @@ export class EndpointTreeProvider
369371
}
370372

371373
case "route": {
372-
// Strip leading dynamic segments for cleaner display
373-
const strippedPath = stripLeadingDynamicSegments(element.route.path)
374-
375-
// Find parent router to show relative path
376-
const parentRouter = this.findParentRouterForRoute(element.route)
377-
const parentPrefix = parentRouter
378-
? stripLeadingDynamicSegments(parentRouter.prefix)
379-
: "/"
380-
let displayPath = this.getRelativePath(strippedPath, parentPrefix)
381-
382-
// Ensure path starts with / and doesn't end with / (unless it's just "/")
383-
if (!displayPath.startsWith("/")) {
384-
displayPath = `/${displayPath}`
385-
}
386-
if (displayPath.length > 1 && displayPath.endsWith("/")) {
387-
displayPath = displayPath.slice(0, -1)
388-
}
374+
// Only strip leading dynamic segments (like {settings.API_V1_STR})
375+
// Keep the full path otherwise for clarity
376+
const displayPath = stripLeadingDynamicSegments(element.route.path)
389377

390378
const label =
391379
element.route.method === "WEBSOCKET"

0 commit comments

Comments
 (0)