fork of hey-api/openapi-ts because I need some additional things
at feat/skip-token 265 lines 7.7 kB view raw
1import path, { join, win32 } from 'node:path'; 2 3import convertPathToPosix from './convert-path-to-posix'; 4import { isWindows } from './is-windows'; 5 6const forwardSlashPattern = /\//g; 7const protocolPattern = /^(\w{2,}):\/\//i; 8 9// RegExp patterns to URL-encode special characters in local filesystem paths 10const urlEncodePatterns = [ 11 [/\?/g, '%3F'], 12 [/#/g, '%23'], 13] as [RegExp, string][]; 14 15// RegExp patterns to URL-decode special characters for local filesystem paths 16const urlDecodePatterns = [/%23/g, '#', /%24/g, '$', /%26/g, '&', /%2C/g, ',', /%40/g, '@']; 17 18/** 19 * Returns resolved target URL relative to a base URL in a manner similar to that of a Web browser resolving an anchor tag HREF. 20 * 21 * @returns 22 */ 23export function resolve(from: string, to: string) { 24 const fromUrl = new URL(convertPathToPosix(from), 'resolve://'); 25 const resolvedUrl = new URL(convertPathToPosix(to), fromUrl); 26 const endSpaces = to.match(/(\s*)$/)?.[1] || ''; 27 if (resolvedUrl.protocol === 'resolve:') { 28 // `from` is a relative URL. 29 const { hash, pathname, search } = resolvedUrl; 30 return pathname + search + hash + endSpaces; 31 } 32 return resolvedUrl.toString() + endSpaces; 33} 34 35/** 36 * Returns the current working directory (in Node) or the current page URL (in browsers). 37 * 38 * @returns 39 */ 40export function cwd() { 41 if (typeof window !== 'undefined') { 42 return location.href; 43 } 44 45 const path = process.cwd(); 46 47 const lastChar = path.slice(-1); 48 if (lastChar === '/' || lastChar === '\\') { 49 return path; 50 } else { 51 return path + '/'; 52 } 53} 54 55/** 56 * Returns the protocol of the given URL, or `undefined` if it has no protocol. 57 * 58 * @param path 59 * @returns 60 */ 61export function getProtocol(path: string | undefined) { 62 const match = protocolPattern.exec(path || ''); 63 if (match) { 64 return match[1]!.toLowerCase(); 65 } 66 return undefined; 67} 68 69/** 70 * Returns the lowercased file extension of the given URL, 71 * or an empty string if it has no extension. 72 * 73 * @param path 74 * @returns 75 */ 76export function getExtension(path: any) { 77 const lastDot = path.lastIndexOf('.'); 78 if (lastDot > -1) { 79 return stripQuery(path.substr(lastDot).toLowerCase()); 80 } 81 return ''; 82} 83 84/** 85 * Removes the query, if any, from the given path. 86 * 87 * @param path 88 * @returns 89 */ 90export function stripQuery(path: any) { 91 const queryIndex = path.indexOf('?'); 92 if (queryIndex > -1) { 93 path = path.substr(0, queryIndex); 94 } 95 return path; 96} 97 98/** 99 * Returns the hash (URL fragment), of the given path. 100 * If there is no hash, then the root hash ("#") is returned. 101 * 102 * @param path 103 * @returns 104 */ 105export function getHash(path: undefined | string) { 106 if (!path) { 107 return '#'; 108 } 109 const hashIndex = path.indexOf('#'); 110 if (hashIndex > -1) { 111 return path.substring(hashIndex); 112 } 113 return '#'; 114} 115 116/** 117 * Removes the hash (URL fragment), if any, from the given path. 118 * 119 * @param path 120 * @returns 121 */ 122export function stripHash(path?: string | undefined) { 123 if (!path) { 124 return ''; 125 } 126 const hashIndex = path.indexOf('#'); 127 if (hashIndex > -1) { 128 path = path.substring(0, hashIndex); 129 } 130 return path; 131} 132 133/** 134 * Determines whether the given path is a filesystem path. 135 * This includes "file://" URLs. 136 * 137 * @param path 138 * @returns 139 */ 140export function isFileSystemPath(path: string | undefined) { 141 // @ts-ignore 142 if (typeof window !== 'undefined' || (typeof process !== 'undefined' && process.browser)) { 143 // We're running in a browser, so assume that all paths are URLs. 144 // This way, even relative paths will be treated as URLs rather than as filesystem paths 145 return false; 146 } 147 148 const protocol = getProtocol(path); 149 return protocol === undefined || protocol === 'file'; 150} 151 152/** 153 * Converts a filesystem path to a properly-encoded URL. 154 * 155 * This is intended to handle situations where JSON Schema $Ref Parser is called 156 * with a filesystem path that contains characters which are not allowed in URLs. 157 * 158 * @example 159 * The following filesystem paths would be converted to the following URLs: 160 * 161 * <"!@#$%^&*+=?'>.json ==> %3C%22!@%23$%25%5E&*+=%3F\'%3E.json 162 * C:\\My Documents\\File (1).json ==> C:/My%20Documents/File%20(1).json 163 * file://Project #42/file.json ==> file://Project%20%2342/file.json 164 * 165 * @param path 166 * @returns 167 */ 168export function fromFileSystemPath(path: string) { 169 // Step 1: On Windows, replace backslashes with forward slashes, 170 // rather than encoding them as "%5C" 171 if (isWindows()) { 172 const projectDir = cwd(); 173 const upperPath = path.toUpperCase(); 174 const projectDirPosixPath = convertPathToPosix(projectDir); 175 const posixUpper = projectDirPosixPath.toUpperCase(); 176 const hasProjectDir = upperPath.includes(posixUpper); 177 const hasProjectUri = upperPath.includes(posixUpper); 178 const isAbsolutePath = 179 win32.isAbsolute(path) || 180 path.startsWith('http://') || 181 path.startsWith('https://') || 182 path.startsWith('file://'); 183 184 if (!(hasProjectDir || hasProjectUri || isAbsolutePath) && !projectDir.startsWith('http')) { 185 path = join(projectDir, path); 186 } 187 path = convertPathToPosix(path); 188 } 189 190 // Step 2: `encodeURI` will take care of MOST characters 191 path = encodeURI(path); 192 193 // Step 3: Manually encode characters that are not encoded by `encodeURI`. 194 // This includes characters such as "#" and "?", which have special meaning in URLs, 195 // but are just normal characters in a filesystem path. 196 for (const pattern of urlEncodePatterns) { 197 path = path.replace(pattern[0], pattern[1]); 198 } 199 200 return path; 201} 202 203/** 204 * Converts a URL to a local filesystem path. 205 */ 206export function toFileSystemPath(path: string | undefined, keepFileProtocol?: boolean): string { 207 // Step 1: `decodeURI` will decode characters such as Cyrillic characters, spaces, etc. 208 path = decodeURI(path!); 209 210 // Step 2: Manually decode characters that are not decoded by `decodeURI`. 211 // This includes characters such as "#" and "?", which have special meaning in URLs, 212 // but are just normal characters in a filesystem path. 213 for (let i = 0; i < urlDecodePatterns.length; i += 2) { 214 path = path.replace(urlDecodePatterns[i]!, urlDecodePatterns[i + 1] as string); 215 } 216 217 // Step 3: If it's a "file://" URL, then format it consistently 218 // or convert it to a local filesystem path 219 let isFileUrl = path.substr(0, 7).toLowerCase() === 'file://'; 220 if (isFileUrl) { 221 // Strip-off the protocol, and the initial "/", if there is one 222 path = path[7] === '/' ? path.substr(8) : path.substr(7); 223 224 // insert a colon (":") after the drive letter on Windows 225 if (isWindows() && path[1] === '/') { 226 path = path[0] + ':' + path.substr(1); 227 } 228 229 if (keepFileProtocol) { 230 // Return the consistently-formatted "file://" URL 231 path = 'file:///' + path; 232 } else { 233 // Convert the "file://" URL to a local filesystem path. 234 // On Windows, it will start with something like "C:/". 235 // On Posix, it will start with "/" 236 isFileUrl = false; 237 path = isWindows() ? path : '/' + path; 238 } 239 } 240 241 // Step 4: Normalize Windows paths (unless it's a "file://" URL) 242 if (isWindows() && !isFileUrl) { 243 // Replace forward slashes with backslashes 244 path = path.replace(forwardSlashPattern, '\\'); 245 246 // Capitalize the drive letter 247 if (path.substr(1, 2) === ':\\') { 248 path = path[0]!.toUpperCase() + path.substr(1); 249 } 250 } 251 252 return path; 253} 254 255export function relative(from: string, to: string) { 256 if (!isFileSystemPath(from) || !isFileSystemPath(to)) { 257 return resolve(from, to); 258 } 259 260 const fromDir = path.dirname(stripHash(from)); 261 const toPath = stripHash(to); 262 263 const result = path.relative(fromDir, toPath); 264 return result + getHash(to); 265}