-
Notifications
You must be signed in to change notification settings - Fork 27
Expand file tree
/
Copy pathpathUtils.ts
More file actions
130 lines (118 loc) · 4.35 KB
/
Copy pathpathUtils.ts
File metadata and controls
130 lines (118 loc) · 4.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import { existsSync } from "node:fs"
import { dirname, join, relative, sep } from "node:path"
/**
* Strips leading dynamic segments (like {settings.API_V1_STR}) from a path.
*
* Examples:
* "{settings.API_V1_STR}/users/{id}" -> "/users/{id}"
* "{BASE}/api/items" -> "/api/items"
* "/users/{id}/posts" -> "/users/{id}/posts" (unchanged)
* "{settings.API_V1_STR}" -> "/"
*/
export function stripLeadingDynamicSegments(path: string): string {
return path.replace(/^(\{[^}]+\})+/, "") || "/"
}
/**
* Checks if a path is within or equal to a base directory.
* Uses relative path calculation to avoid false positives from string prefix matching.
*/
export function isWithinDirectory(filePath: string, baseDir: string): boolean {
const rel = relative(baseDir, filePath)
// If relative path starts with "..", the path is outside baseDir
return !rel.startsWith("..") && !rel.startsWith(sep)
}
/**
* Finds the Python project root by walking up from the entry file
* until we find a directory without __init__.py (or hit the workspace root).
* This is the directory from which absolute imports are resolved.
*/
export function findProjectRoot(
entryPath: string,
workspaceRoot: string,
): string {
let dir = dirname(entryPath)
// If the entry file's directory doesn't have __init__.py, it's a top-level script
if (!existsSync(join(dir, "__init__.py"))) {
return dir
}
// Walk up until we find a directory whose parent doesn't have __init__.py
while (isWithinDirectory(dir, workspaceRoot) && dir !== workspaceRoot) {
const parent = dirname(dir)
if (!existsSync(join(parent, "__init__.py"))) {
return parent
}
dir = parent
}
return workspaceRoot
}
/**
* Gets the first N segments of a path.
*
* Examples:
* getPathSegments("/integrations/neon/foo", 2) -> "/integrations/neon"
* getPathSegments("/users", 1) -> "/users"
* getPathSegments("/a/b/c", 5) -> "/a/b/c" (returns full path if count >= segments)
*/
export function getPathSegments(path: string, count: number): string {
const segments = path.split("/").filter(Boolean)
if (count >= segments.length) return path
return `/${segments.slice(0, count).join("/")}`
}
/**
* Counts the number of segments in a path.
*
* Examples:
* countSegments("/integrations/neon") -> 2
* countSegments("/") -> 0
* countSegments("/users") -> 1
*/
export function countSegments(path: string): number {
return path.split("/").filter(Boolean).length
}
/**
* Checks if a test path matches an endpoint path pattern.
* Both paths may contain dynamic segments like {item_id} or {settings.API_V1_STR}
* which match any segment.
*
* Leading dynamic prefixes (like {settings.API_V1_STR}) and query strings are stripped
* before comparison.
*
* Examples:
* pathMatchesEndpoint("/items/123", "/items/{item_id}") -> true
* pathMatchesEndpoint("/items/123/details", "/items/{item_id}") -> false
* pathMatchesEndpoint("/users/abc/posts/456", "/users/{user_id}/posts/{post_id}") -> true
* pathMatchesEndpoint("/items/", "/items/{item_id}") -> false
* pathMatchesEndpoint("{settings.API}/apps/{id}", "/apps/{app_id}") -> true
* pathMatchesEndpoint("{BASE}/users/{id}", "/users/{user_id}") -> true
* pathMatchesEndpoint("/teams/?owner=true", "/teams") -> true (query string stripped)
*/
export function pathMatchesEndpoint(
testPath: string,
endpointPath: string,
): boolean {
// Strip query string from test path (e.g., "/teams/?owner=true" -> "/teams/")
const testPathWithoutQuery = testPath.split("?")[0]
// Strip leading dynamic segments (e.g., {settings.API_V1_STR}) for comparison
const testSegments = stripLeadingDynamicSegments(testPathWithoutQuery)
.split("/")
.filter(Boolean)
const endpointSegments = stripLeadingDynamicSegments(endpointPath)
.split("/")
.filter(Boolean)
// Segment counts must match
if (testSegments.length !== endpointSegments.length) {
return false
}
// Compare each segment positionally
return testSegments.every((testSeg, i) => {
const endpointSeg = endpointSegments[i]
// Dynamic segments (e.g., {id}, {app.id}) match any value
const testIsDynamic = testSeg.startsWith("{") && testSeg.endsWith("}")
const endpointIsDynamic =
endpointSeg.startsWith("{") && endpointSeg.endsWith("}")
if (testIsDynamic || endpointIsDynamic) {
return true
}
return testSeg === endpointSeg
})
}