fork of hey-api/openapi-ts because I need some additional things
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}