(projectDir: string, url: string)
| 127 | } |
| 128 | |
| 129 | function resolveLocalAssetCandidates(projectDir: string, url: string): string[] { |
| 130 | const cleanUrl = cleanAssetUrl(url); |
| 131 | const projectRoot = resolve(projectDir); |
| 132 | const candidates: string[] = []; |
| 133 | |
| 134 | for (const variant of decodeUrlPathVariants(cleanUrl)) { |
| 135 | const projectRelative = variant.startsWith("/") ? variant.slice(1) : variant; |
| 136 | const resolved = resolve(projectRoot, projectRelative); |
| 137 | if (isWithinProjectRoot(projectRoot, resolved)) { |
| 138 | addCandidate(candidates, resolved); |
| 139 | continue; |
| 140 | } |
| 141 | |
| 142 | const normalized = posix.normalize(projectRelative.replace(/\\/g, "/")); |
| 143 | const clamped = normalized.replace(/^(\.\.\/)+/, ""); |
| 144 | if (clamped && !clamped.startsWith("..")) { |
| 145 | addCandidate(candidates, resolve(projectRoot, clamped)); |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | return candidates; |
| 150 | } |
| 151 | |
| 152 | function resolveExistingLocalAsset( |
| 153 | projectDir: string, |
no test coverage detected