@gesslar/toolkit 0.0.12 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -15
- package/src/index.js +1 -1
- package/src/lib/Cache.js +3 -4
- package/src/lib/Data.js +31 -24
- package/src/lib/DirectoryObject.js +77 -7
- package/src/lib/FS.js +191 -0
- package/src/lib/FileObject.js +200 -11
- package/src/lib/Glog.js +15 -7
- package/src/lib/Type.js +20 -7
- package/src/lib/Util.js +11 -1
- package/src/types/Cache.d.ts +1 -1
- package/src/types/DirectoryObject.d.ts +18 -2
- package/src/types/FS.d.ts +31 -0
- package/src/types/FileObject.d.ts +55 -19
- package/src/types/index.d.ts +2 -2
- package/src/lib/File.js +0 -414
- package/src/types/File.d.ts +0 -83
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gesslar/toolkit",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Get in, bitches, we're going toolkitting.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -21,18 +21,11 @@
|
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"lint": "eslint src/",
|
|
24
|
-
"lint:docs": "cd docs && npm run lint",
|
|
25
24
|
"lint:fix": "eslint src/ --fix",
|
|
26
|
-
"lint:fix:docs": "cd docs && npm run lint:fix",
|
|
27
25
|
"submit": "npm publish --access public",
|
|
28
26
|
"update": "npx npm-check-updates -u && npm install",
|
|
29
|
-
"test": "node
|
|
30
|
-
"
|
|
31
|
-
"docs:build": "npm run docs:clean && npm run docs:generate && cd docs && npm run build",
|
|
32
|
-
"docs:serve": "cd docs && npm run serve",
|
|
33
|
-
"docs:clean": "cd docs && npm run docs:clean",
|
|
34
|
-
"docs:generate": "npx typedoc src/types/index.d.ts --out docs/docs/api --plugin typedoc-plugin-markdown --readme none --excludePrivate --excludeProtected --excludeInternal --includeVersion",
|
|
35
|
-
"docs:install": "cd docs && npm install"
|
|
27
|
+
"test": "node --test tests/unit/*.test.js",
|
|
28
|
+
"test:unit": "node --test tests/unit/*.test.js"
|
|
36
29
|
},
|
|
37
30
|
"repository": {
|
|
38
31
|
"type": "git",
|
|
@@ -63,11 +56,9 @@
|
|
|
63
56
|
"devDependencies": {
|
|
64
57
|
"@stylistic/eslint-plugin": "^5.4.0",
|
|
65
58
|
"@types/node": "^24.5.2",
|
|
66
|
-
"@typescript-eslint/eslint-plugin": "^8.44.
|
|
67
|
-
"@typescript-eslint/parser": "^8.44.
|
|
59
|
+
"@typescript-eslint/eslint-plugin": "^8.44.0",
|
|
60
|
+
"@typescript-eslint/parser": "^8.44.0",
|
|
68
61
|
"eslint": "^9.36.0",
|
|
69
|
-
"eslint-plugin-jsdoc": "^60.
|
|
70
|
-
"typedoc": "^0.28.13",
|
|
71
|
-
"typedoc-plugin-markdown": "^4.9.0"
|
|
62
|
+
"eslint-plugin-jsdoc": "^60.1.0"
|
|
72
63
|
}
|
|
73
64
|
}
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Core file system abstractions
|
|
2
2
|
export {default as FileObject} from "./lib/FileObject.js"
|
|
3
3
|
export {default as DirectoryObject} from "./lib/DirectoryObject.js"
|
|
4
|
-
export {default as
|
|
4
|
+
export {default as FS} from "./lib/FS.js"
|
|
5
5
|
|
|
6
6
|
// Utility classes
|
|
7
7
|
export {default as Cache} from "./lib/Cache.js"
|
package/src/lib/Cache.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import File from "./File.js"
|
|
2
1
|
import FileObject from "./FileObject.js"
|
|
3
2
|
import Sass from "./Sass.js"
|
|
4
3
|
|
|
@@ -41,11 +40,11 @@ export default class Cache {
|
|
|
41
40
|
* parallel processing.
|
|
42
41
|
*
|
|
43
42
|
* @param {FileObject} fileObject - The file object to load and cache
|
|
44
|
-
* @returns {Promise<
|
|
43
|
+
* @returns {Promise<unknown>} The parsed file data (JSON5 or YAML)
|
|
45
44
|
* @throws {Sass} If the file cannot be found or accessed
|
|
46
45
|
*/
|
|
47
46
|
async loadCachedData(fileObject) {
|
|
48
|
-
const lastModified = await
|
|
47
|
+
const lastModified = await fileObject.modified()
|
|
49
48
|
|
|
50
49
|
if(lastModified === null)
|
|
51
50
|
throw Sass.new(`Unable to find file '${fileObject.path}'`)
|
|
@@ -64,7 +63,7 @@ export default class Cache {
|
|
|
64
63
|
}
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
const data = await
|
|
66
|
+
const data = await fileObject.loadData()
|
|
68
67
|
|
|
69
68
|
this.#modifiedTimes.set(fileObject.path, lastModified)
|
|
70
69
|
this.#dataCache.set(fileObject.path, data)
|
package/src/lib/Data.js
CHANGED
|
@@ -20,6 +20,7 @@ export default class Data {
|
|
|
20
20
|
static primitives = Object.freeze([
|
|
21
21
|
// Primitives
|
|
22
22
|
"undefined",
|
|
23
|
+
"null",
|
|
23
24
|
"boolean",
|
|
24
25
|
"number",
|
|
25
26
|
"bigint",
|
|
@@ -196,10 +197,18 @@ export default class Data {
|
|
|
196
197
|
const result = {}
|
|
197
198
|
|
|
198
199
|
for(const [key, value] of Object.entries(obj)) {
|
|
199
|
-
if(Data.isType(value, "
|
|
200
|
+
if(Data.isType(value, "array")) {
|
|
201
|
+
// Clone arrays by mapping over them
|
|
202
|
+
result[key] = value.map(item =>
|
|
203
|
+
Data.isType(item, "object") || Data.isType(item, "array")
|
|
204
|
+
? Data.cloneObject(item)
|
|
205
|
+
: item
|
|
206
|
+
)
|
|
207
|
+
} else if(Data.isType(value, "object")) {
|
|
200
208
|
result[key] = Data.cloneObject(value)
|
|
201
|
-
else
|
|
209
|
+
} else {
|
|
202
210
|
result[key] = value
|
|
211
|
+
}
|
|
203
212
|
}
|
|
204
213
|
|
|
205
214
|
return freeze ? Object.freeze(result) : result
|
|
@@ -340,30 +349,16 @@ export default class Data {
|
|
|
340
349
|
return false
|
|
341
350
|
|
|
342
351
|
const valueType = Data.typeOf(value)
|
|
352
|
+
const normalizedType = type.toLowerCase()
|
|
343
353
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
return Array.isArray(value) // Native array check
|
|
347
|
-
case "string":
|
|
348
|
-
return valueType === "string"
|
|
349
|
-
case "boolean":
|
|
350
|
-
return valueType === "boolean"
|
|
354
|
+
// Special cases that need extra validation
|
|
355
|
+
switch(normalizedType) {
|
|
351
356
|
case "number":
|
|
352
357
|
return valueType === "number" && !isNaN(value) // Excludes NaN
|
|
353
358
|
case "object":
|
|
354
|
-
return
|
|
355
|
-
case "function":
|
|
356
|
-
return valueType === "function"
|
|
357
|
-
case "symbol":
|
|
358
|
-
return valueType === "symbol" // ES6 Symbol type
|
|
359
|
-
case "bigint":
|
|
360
|
-
return valueType === "bigint" // BigInt support
|
|
361
|
-
case "null":
|
|
362
|
-
return value === null // Explicit null check
|
|
363
|
-
case "undefined":
|
|
364
|
-
return valueType === "undefined" // Explicit undefined check
|
|
359
|
+
return valueType === "object" && value !== null && !Array.isArray(value) // Excludes arrays and null
|
|
365
360
|
default:
|
|
366
|
-
return
|
|
361
|
+
return valueType === normalizedType
|
|
367
362
|
}
|
|
368
363
|
}
|
|
369
364
|
|
|
@@ -374,7 +369,13 @@ export default class Data {
|
|
|
374
369
|
* @returns {string} The type of the value
|
|
375
370
|
*/
|
|
376
371
|
static typeOf(value) {
|
|
377
|
-
|
|
372
|
+
if(value === null)
|
|
373
|
+
return "null"
|
|
374
|
+
|
|
375
|
+
if(Array.isArray(value))
|
|
376
|
+
return "array"
|
|
377
|
+
|
|
378
|
+
return typeof value
|
|
378
379
|
}
|
|
379
380
|
|
|
380
381
|
/**
|
|
@@ -397,11 +398,16 @@ export default class Data {
|
|
|
397
398
|
* @returns {boolean} Whether the value is empty
|
|
398
399
|
*/
|
|
399
400
|
static isEmpty(value, checkForNothing = true) {
|
|
400
|
-
const type = Data.typeOf(value)
|
|
401
|
-
|
|
402
401
|
if(checkForNothing && Data.isNothing(value))
|
|
403
402
|
return true
|
|
404
403
|
|
|
404
|
+
// When checkForNothing is false, null/undefined should not be treated as empty
|
|
405
|
+
// They should be processed like regular values
|
|
406
|
+
if(!checkForNothing && Data.isNothing(value))
|
|
407
|
+
return false
|
|
408
|
+
|
|
409
|
+
const type = Data.typeOf(value)
|
|
410
|
+
|
|
405
411
|
if(!Data.emptyableTypes.includes(type))
|
|
406
412
|
return false
|
|
407
413
|
|
|
@@ -409,6 +415,7 @@ export default class Data {
|
|
|
409
415
|
case "array":
|
|
410
416
|
return value.length === 0
|
|
411
417
|
case "object":
|
|
418
|
+
// null was already handled above, so this should only be real objects
|
|
412
419
|
return Object.keys(value).length === 0
|
|
413
420
|
case "string":
|
|
414
421
|
return value.trim().length === 0
|
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* resolution and existence checks.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import fs from "node:fs/promises"
|
|
7
8
|
import path from "node:path"
|
|
8
9
|
import util from "node:util"
|
|
9
10
|
|
|
10
|
-
import
|
|
11
|
+
import FS from "./FS.js"
|
|
12
|
+
import FileObject from "./FileObject.js"
|
|
13
|
+
import Sass from "./Sass.js"
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* DirectoryObject encapsulates metadata and operations for a directory,
|
|
@@ -23,7 +26,7 @@ import File from "./File.js"
|
|
|
23
26
|
* @property {boolean} isDirectory - Always true
|
|
24
27
|
* @property {Promise<boolean>} exists - Whether the directory exists (async)
|
|
25
28
|
*/
|
|
26
|
-
export default class DirectoryObject {
|
|
29
|
+
export default class DirectoryObject extends FS {
|
|
27
30
|
/**
|
|
28
31
|
* @type {object}
|
|
29
32
|
* @private
|
|
@@ -45,7 +48,6 @@ export default class DirectoryObject {
|
|
|
45
48
|
extension: null,
|
|
46
49
|
isFile: false,
|
|
47
50
|
isDirectory: true,
|
|
48
|
-
directory: null,
|
|
49
51
|
})
|
|
50
52
|
|
|
51
53
|
/**
|
|
@@ -54,10 +56,12 @@ export default class DirectoryObject {
|
|
|
54
56
|
* @param {string} directory - The directory path
|
|
55
57
|
*/
|
|
56
58
|
constructor(directory) {
|
|
57
|
-
|
|
59
|
+
super()
|
|
60
|
+
|
|
61
|
+
const fixedDir = FS.fixSlashes(directory ?? ".")
|
|
58
62
|
const absolutePath = path.resolve(fixedDir)
|
|
59
|
-
const fileUri =
|
|
60
|
-
const filePath =
|
|
63
|
+
const fileUri = FS.pathToUri(absolutePath)
|
|
64
|
+
const filePath = FS.uriToPath(fileUri)
|
|
61
65
|
const baseName = path.basename(absolutePath) || "."
|
|
62
66
|
|
|
63
67
|
this.#meta.supplied = fixedDir
|
|
@@ -112,7 +116,7 @@ export default class DirectoryObject {
|
|
|
112
116
|
* @returns {Promise<boolean>} - A Promise that resolves to true or false
|
|
113
117
|
*/
|
|
114
118
|
get exists() {
|
|
115
|
-
return
|
|
119
|
+
return this.#directoryExists()
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
/**
|
|
@@ -186,4 +190,70 @@ export default class DirectoryObject {
|
|
|
186
190
|
get isDirectory() {
|
|
187
191
|
return this.#meta.isDirectory
|
|
188
192
|
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Check if a directory exists
|
|
196
|
+
*
|
|
197
|
+
* @returns {Promise<boolean>} Whether the directory exists
|
|
198
|
+
*/
|
|
199
|
+
async #directoryExists() {
|
|
200
|
+
try {
|
|
201
|
+
(await fs.opendir(this.path)).close()
|
|
202
|
+
|
|
203
|
+
return true
|
|
204
|
+
} catch(_) {
|
|
205
|
+
return false
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Lists the contents of a directory.
|
|
211
|
+
*
|
|
212
|
+
* @param {DirectoryObject} directory - The directory to list.
|
|
213
|
+
* @returns {Promise<{files: Array<FileObject>, directories: Array<DirectoryObject>}>} The files and directories in the directory.
|
|
214
|
+
*/
|
|
215
|
+
async read(directory) {
|
|
216
|
+
const found = await fs.readdir(
|
|
217
|
+
new URL(directory.uri),
|
|
218
|
+
{withFileTypes: true}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
const results = await Promise.all(
|
|
222
|
+
found.map(async dirent => {
|
|
223
|
+
const fullPath = path.join(directory.path, dirent.name)
|
|
224
|
+
const stat = await fs.stat(fullPath)
|
|
225
|
+
|
|
226
|
+
return {dirent, stat, fullPath}
|
|
227
|
+
}),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
const files = results
|
|
231
|
+
.filter(({stat}) => stat.isFile())
|
|
232
|
+
.map(({fullPath}) => new FileObject(fullPath))
|
|
233
|
+
|
|
234
|
+
const directories = results
|
|
235
|
+
.filter(({stat}) => stat.isDirectory())
|
|
236
|
+
.map(({fullPath}) => new DirectoryObject(fullPath))
|
|
237
|
+
|
|
238
|
+
return {files, directories}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Ensures a directory exists, creating it if necessary
|
|
243
|
+
*
|
|
244
|
+
* @async
|
|
245
|
+
* @param {object} [options] - Any options to pass to mkdir
|
|
246
|
+
* @returns {Promise<void>}
|
|
247
|
+
* @throws {Sass} If directory creation fails
|
|
248
|
+
*/
|
|
249
|
+
async assureExists(options = {}) {
|
|
250
|
+
if(await this.exists)
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
await fs.mkdir(this.path, options)
|
|
255
|
+
} catch(e) {
|
|
256
|
+
throw Sass.new(`Unable to create directory '${this.path}': ${e.message}`)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
189
259
|
}
|
package/src/lib/FS.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {globby} from "globby"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import url from "node:url"
|
|
4
|
+
|
|
5
|
+
import Data from "./Data.js"
|
|
6
|
+
import DirectoryObject from "./DirectoryObject.js"
|
|
7
|
+
import FileObject from "./FileObject.js"
|
|
8
|
+
import Sass from "./Sass.js"
|
|
9
|
+
import Valid from "./Valid.js"
|
|
10
|
+
|
|
11
|
+
export default class FS {
|
|
12
|
+
/**
|
|
13
|
+
* Fix slashes in a path
|
|
14
|
+
*
|
|
15
|
+
* @param {string} pathName - The path to fix
|
|
16
|
+
* @returns {string} The fixed path
|
|
17
|
+
*/
|
|
18
|
+
static fixSlashes(pathName) {
|
|
19
|
+
return pathName.replace(/\\/g, "/")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Convert a path to a URI
|
|
24
|
+
*
|
|
25
|
+
* @param {string} pathName - The path to convert
|
|
26
|
+
* @returns {string} The URI
|
|
27
|
+
*/
|
|
28
|
+
static pathToUri(pathName) {
|
|
29
|
+
try {
|
|
30
|
+
return url.pathToFileURL(pathName).href
|
|
31
|
+
} catch(e) {
|
|
32
|
+
void e // stfu linter
|
|
33
|
+
|
|
34
|
+
return pathName
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Convert a URI to a path
|
|
40
|
+
*
|
|
41
|
+
* @param {string} pathName - The URI to convert
|
|
42
|
+
* @returns {string} The path
|
|
43
|
+
*/
|
|
44
|
+
static uriToPath(pathName) {
|
|
45
|
+
try {
|
|
46
|
+
return url.fileURLToPath(pathName)
|
|
47
|
+
} catch(_) {
|
|
48
|
+
return pathName
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Retrieve all files matching a specific glob pattern.
|
|
54
|
+
*
|
|
55
|
+
* @param {string|Array<string>} glob - The glob pattern(s) to search.
|
|
56
|
+
* @returns {Promise<Array<FileObject>>} A promise that resolves to an array of file objects
|
|
57
|
+
* @throws {Sass} If the input is not a string or array of strings.
|
|
58
|
+
* @throws {Sass} If the glob pattern array is empty or for other search failures.
|
|
59
|
+
*/
|
|
60
|
+
static async getFiles(glob) {
|
|
61
|
+
Valid.assert(
|
|
62
|
+
(
|
|
63
|
+
(typeof glob === "string" && glob.length > 0) ||
|
|
64
|
+
(
|
|
65
|
+
Array.isArray(glob) && Data.uniformStringArray(glob) &&
|
|
66
|
+
glob.length > 0
|
|
67
|
+
)
|
|
68
|
+
),
|
|
69
|
+
"glob must be a non-empty string or array of strings.",
|
|
70
|
+
1
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const globbyArray = (
|
|
74
|
+
typeof glob === "string"
|
|
75
|
+
? glob
|
|
76
|
+
.split("|")
|
|
77
|
+
.map(g => g.trim())
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
: glob
|
|
80
|
+
).map(g => FS.fixSlashes(g))
|
|
81
|
+
|
|
82
|
+
if(
|
|
83
|
+
Array.isArray(globbyArray) &&
|
|
84
|
+
Data.uniformStringArray(globbyArray) &&
|
|
85
|
+
!globbyArray.length
|
|
86
|
+
)
|
|
87
|
+
throw Sass.new(
|
|
88
|
+
`Invalid glob pattern: Array cannot be empty. Got ${JSON.stringify(glob)}`,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
// Use Globby to fetch matching files
|
|
92
|
+
const filesArray = await globby(globbyArray)
|
|
93
|
+
const files = filesArray.map(file => new FileObject(file))
|
|
94
|
+
|
|
95
|
+
// Flatten the result and remove duplicates
|
|
96
|
+
return files
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Computes the relative path from one file or directory to another.
|
|
101
|
+
*
|
|
102
|
+
* If the target is outside the source (i.e., the relative path starts with ".."),
|
|
103
|
+
* returns the absolute path to the target instead.
|
|
104
|
+
*
|
|
105
|
+
* @param {FileObject|DirectoryObject} from - The source file or directory object
|
|
106
|
+
* @param {FileObject|DirectoryObject} to - The target file or directory object
|
|
107
|
+
* @returns {string} The relative path from `from` to `to`, or the absolute path if not reachable
|
|
108
|
+
*/
|
|
109
|
+
static relativeOrAbsolutePath(from, to) {
|
|
110
|
+
const relative = path.relative(from.path, to.path)
|
|
111
|
+
|
|
112
|
+
return relative.startsWith("..")
|
|
113
|
+
? to.path
|
|
114
|
+
: relative
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Merge two paths by finding overlapping segments and combining them efficiently
|
|
119
|
+
*
|
|
120
|
+
* @param {string} path1 - The first path
|
|
121
|
+
* @param {string} path2 - The second path to merge with the first
|
|
122
|
+
* @param {string} [sep] - The path separator to use (defaults to system separator)
|
|
123
|
+
* @returns {string} The merged path
|
|
124
|
+
*/
|
|
125
|
+
static mergeOverlappingPaths(path1, path2, sep=path.sep) {
|
|
126
|
+
const isAbsolutePath1 = path.isAbsolute(path1)
|
|
127
|
+
const from = path1.split(sep).filter(Boolean)
|
|
128
|
+
const to = path2.split(sep).filter(Boolean)
|
|
129
|
+
|
|
130
|
+
// If they're the same, just return path1
|
|
131
|
+
if(to.length === from.length && from.every((f, i) => to[i] === f)) {
|
|
132
|
+
return path1
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const overlapIndex = from.findLastIndex(curr => curr === to.at(0))
|
|
136
|
+
|
|
137
|
+
// If overlap is found, slice and join
|
|
138
|
+
if(overlapIndex !== -1) {
|
|
139
|
+
const prefix = from.slice(0, overlapIndex)
|
|
140
|
+
const result = path.join(...prefix, ...to)
|
|
141
|
+
|
|
142
|
+
// If original path1 was absolute, ensure result is also absolute
|
|
143
|
+
return isAbsolutePath1 && !path.isAbsolute(result)
|
|
144
|
+
? path.sep + result
|
|
145
|
+
: result
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// If no overlap, just join the paths
|
|
149
|
+
return path.join(path1, path2)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resolve a path relative to another path using various strategies
|
|
154
|
+
* Handles absolute paths, relative navigation, and overlap-based merging
|
|
155
|
+
*
|
|
156
|
+
* @param {string} fromPath - The base path to resolve from
|
|
157
|
+
* @param {string} toPath - The target path to resolve
|
|
158
|
+
* @returns {string} The resolved path
|
|
159
|
+
*/
|
|
160
|
+
static resolvePath(fromPath, toPath) {
|
|
161
|
+
// Normalize inputs
|
|
162
|
+
const from = fromPath?.trim() ?? ""
|
|
163
|
+
const to = toPath?.trim() ?? ""
|
|
164
|
+
|
|
165
|
+
// Handle empty cases
|
|
166
|
+
if(!from && !to)
|
|
167
|
+
return ""
|
|
168
|
+
|
|
169
|
+
if(!from)
|
|
170
|
+
return to
|
|
171
|
+
|
|
172
|
+
if(!to)
|
|
173
|
+
return from
|
|
174
|
+
|
|
175
|
+
const normalizedTo = /^\.\//.test(to)
|
|
176
|
+
? path.normalize(to)
|
|
177
|
+
: to
|
|
178
|
+
|
|
179
|
+
// Strategy 1: If 'to' is absolute, it's standalone
|
|
180
|
+
if(path.isAbsolute(normalizedTo))
|
|
181
|
+
return normalizedTo
|
|
182
|
+
|
|
183
|
+
// Strategy 2: If 'to' contains relative navigation
|
|
184
|
+
if(to.startsWith("../"))
|
|
185
|
+
return path.resolve(from, normalizedTo)
|
|
186
|
+
|
|
187
|
+
// Strategy 3: Try overlap-based merging, which will default to a basic
|
|
188
|
+
// join if no overlap
|
|
189
|
+
return FS.mergeOverlappingPaths(from, normalizedTo)
|
|
190
|
+
}
|
|
191
|
+
}
|