@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/src/lib/File.js DELETED
@@ -1,414 +0,0 @@
1
- /**
2
- * @file File system utilities for reading, writing, and manipulating files and directories.
3
- * Provides comprehensive file operations including data file loading (JSON5/YAML),
4
- * path resolution, and file system navigation with support for both files and directories.
5
- */
6
-
7
- import {globby} from "globby"
8
- import JSON5 from "json5"
9
- import fs from "node:fs/promises"
10
- import path from "node:path"
11
- import url from "node:url"
12
- import YAML from "yaml"
13
-
14
- import Data from "./Data.js"
15
- import DirectoryObject from "./DirectoryObject.js"
16
- import FileObject from "./FileObject.js"
17
- import Sass from "./Sass.js"
18
- import Valid from "./Valid.js"
19
-
20
- export default class File {
21
- /**
22
- * Fix slashes in a path
23
- *
24
- * @param {string} pathName - The path to fix
25
- * @returns {string} The fixed path
26
- */
27
- static fixSlashes(pathName) {
28
- return pathName.replace(/\\/g, "/")
29
- }
30
-
31
- /**
32
- * Convert a path to a URI
33
- *
34
- * @param {string} pathName - The path to convert
35
- * @returns {string} The URI
36
- */
37
- static pathToUri(pathName) {
38
- try {
39
- return url.pathToFileURL(pathName).href
40
- } catch(e) {
41
- void e // stfu linter
42
-
43
- return pathName
44
- }
45
- }
46
-
47
- /**
48
- * Check if a file can be read. Returns true if the file can be read, false
49
- *
50
- * @param {FileObject} file - The file map to check
51
- * @returns {Promise<boolean>} Whether the file can be read
52
- */
53
- static async canReadFile(file) {
54
- try {
55
- await fs.access(file.path, fs.constants.R_OK)
56
-
57
- return true
58
- } catch(_) {
59
- return false
60
- }
61
- }
62
-
63
- /**
64
- * Check if a file can be written. Returns true if the file can be written,
65
- *
66
- * @param {FileObject} file - The file map to check
67
- * @returns {Promise<boolean>} Whether the file can be written
68
- */
69
- static async canWriteFile(file) {
70
- try {
71
- await fs.access(file.path, fs.constants.W_OK)
72
-
73
- return true
74
- } catch(_) {
75
- return false
76
- }
77
- }
78
-
79
- /**
80
- * Check if a file exists
81
- *
82
- * @param {FileObject} file - The file map to check
83
- * @returns {Promise<boolean>} Whether the file exists
84
- */
85
- static async fileExists(file) {
86
- try {
87
- await fs.access(file.path, fs.constants.F_OK)
88
-
89
- return true
90
- } catch(_) {
91
- return false
92
- }
93
- }
94
-
95
- /**
96
- * Determines the size of a file.
97
- *
98
- * @param {FileObject} file - The file object to test
99
- * @returns {Promise<number?>} - The size of the file or null, if it doesn't exist.
100
- */
101
- static async fileSize(file) {
102
- try {
103
- const stat = await fs.stat(file.path)
104
-
105
- return stat.size
106
- } catch(_) {
107
- return null
108
- }
109
- }
110
-
111
- /**
112
- * Gets the last modification time of a file.
113
- * Used by the caching system to determine if cached data is still valid.
114
- *
115
- * @param {FileObject} file - The file object to check
116
- * @returns {Promise<Date|null>} The last modification time, or null if file doesn't exist
117
- */
118
- static async fileModified(file) {
119
- try {
120
- const stat = await fs.stat(file.path)
121
-
122
- return stat.mtime
123
- } catch(_) {
124
- return null
125
- }
126
- }
127
-
128
- /**
129
- * Check if a directory exists
130
- *
131
- * @param {DirectoryObject} dirObject - The directory map to check
132
- * @returns {Promise<boolean>} Whether the directory exists
133
- */
134
- static async directoryExists(dirObject) {
135
- try {
136
- (await fs.opendir(dirObject.path)).close()
137
-
138
- return true
139
- } catch(_) {
140
- return false
141
- }
142
- }
143
-
144
- /**
145
- * Convert a URI to a path
146
- *
147
- * @param {string} pathName - The URI to convert
148
- * @returns {string} The path
149
- */
150
- static uriToPath(pathName) {
151
- try {
152
- return url.fileURLToPath(pathName)
153
- } catch(_) {
154
- return pathName
155
- }
156
- }
157
-
158
- /**
159
- * @typedef {object} FileParts
160
- * @property {string} base - The file name with extension
161
- * @property {string} dir - The directory path
162
- * @property {string} ext - The file extension (including dot)
163
- */
164
-
165
- /**
166
- * Deconstruct a filename into parts
167
- *
168
- * @param {string} fileName - The filename to deconstruct
169
- * @returns {FileParts} The filename parts
170
- */
171
- static deconstructFilenameToParts(fileName) {
172
- Valid.assert(typeof fileName === "string" && fileName.length > 0,
173
- "file must be a non-zero length string", 1)
174
-
175
- return path.parse(fileName)
176
- }
177
-
178
- /**
179
- * Retrieve all files matching a specific glob pattern.
180
- *
181
- * @param {string|Array<string>} glob - The glob pattern(s) to search.
182
- * @returns {Promise<Array<FileObject>>} A promise that resolves to an array of file objects
183
- * @throws {Sass} If the input is not a string or array of strings.
184
- * @throws {Sass} If the glob pattern array is empty or for other search failures.
185
- */
186
- static async getFiles(glob) {
187
- Valid.assert(
188
- (
189
- (typeof glob === "string" && glob.length > 0) ||
190
- (
191
- Array.isArray(glob) && Data.uniformStringArray(glob) &&
192
- glob.length > 0
193
- )
194
- ),
195
- "glob must be a non-empty string or array of strings.",
196
- 1
197
- )
198
-
199
- const globbyArray = (
200
- typeof glob === "string"
201
- ? glob
202
- .split("|")
203
- .map(g => g.trim())
204
- .filter(Boolean)
205
- : glob
206
- ).map(g => File.fixSlashes(g))
207
-
208
- if(
209
- Array.isArray(globbyArray) &&
210
- Data.uniformStringArray(globbyArray) &&
211
- !globbyArray.length
212
- )
213
- throw Sass.new(
214
- `Invalid glob pattern: Array cannot be empty. Got ${JSON.stringify(glob)}`,
215
- )
216
-
217
- // Use Globby to fetch matching files
218
- const filesArray = await globby(globbyArray)
219
- const files = filesArray.map(file => new FileObject(file))
220
-
221
- // Flatten the result and remove duplicates
222
- return files
223
- }
224
-
225
- /**
226
- * Lists the contents of a directory.
227
- *
228
- * @param {DirectoryObject} directory - The directory to list.
229
- * @returns {Promise<{files: Array<FileObject>, directories: Array<DirectoryObject>}>} The files and
230
- * directories in the directory.
231
- */
232
- static async ls(directory) {
233
- const found = await fs.readdir(directory.uri, {withFileTypes: true})
234
- const results = await Promise.all(
235
- found.map(async dirent => {
236
- const fullPath = path.join(directory.uri, dirent.name)
237
- const stat = await fs.stat(fullPath)
238
-
239
- return {dirent, stat, fullPath}
240
- }),
241
- )
242
-
243
- const files = results
244
- .filter(({stat}) => stat.isFile())
245
- .map(({fullPath}) => new FileObject(fullPath))
246
-
247
- const directories = results
248
- .filter(({stat}) => stat.isDirectory())
249
- .map(({fullPath}) => new DirectoryObject(fullPath))
250
-
251
- return {files, directories}
252
- }
253
-
254
- /**
255
- * Reads the content of a file asynchronously.
256
- *
257
- * @param {FileObject} fileObject - The file map containing the file path
258
- * @returns {Promise<string>} The file contents
259
- */
260
- static async readFile(fileObject) {
261
- const filePath = fileObject.path
262
-
263
- if(!(await fileObject.exists))
264
- throw Sass.new(`No such file '${filePath}'`)
265
-
266
- if(!filePath)
267
- throw Sass.new("No absolute path in file map")
268
-
269
- return await fs.readFile(filePath, "utf8")
270
- }
271
-
272
- /**
273
- * Writes content to a file synchronously.
274
- *
275
- * @param {FileObject} fileObject - The file map containing the file path
276
- * @param {string} content - The content to write
277
- */
278
- static async writeFile(fileObject, content) {
279
- if(!fileObject.path)
280
- throw Sass.new("No absolute path in file")
281
-
282
- await fs.writeFile(fileObject.path, content, "utf8")
283
- }
284
-
285
- /**
286
- * Loads an object from JSON or YAML provided a fileMap
287
- *
288
- * @param {FileObject} fileObject - The FileObj file to load containing
289
- * JSON or YAML text.
290
- * @returns {object} The parsed data object.
291
- */
292
- static async loadDataFile(fileObject) {
293
- const content = await File.readFile(fileObject)
294
-
295
- try {
296
- return JSON5.parse(content)
297
- } catch {
298
- try {
299
- return YAML.parse(content)
300
- } catch {
301
- throw Sass.new(`Content is neither valid JSON nor valid YAML:\n'${fileObject.path}'`)
302
- }
303
- }
304
- }
305
-
306
- /**
307
- * Ensures a directory exists, creating it if necessary
308
- *
309
- * @async
310
- * @param {DirectoryObject} dirObject - The path or DirMap of the directory to assure exists
311
- * @param {object} [options] - Any options to pass to mkdir
312
- * @returns {Promise<void>}
313
- * @throws {Sass} If directory creation fails
314
- */
315
- static async assureDirectory(dirObject, options = {}) {
316
- if(await dirObject.exists)
317
- return
318
-
319
- try {
320
- await fs.mkdir(dirObject.path, options)
321
- } catch(e) {
322
- throw Sass.new(`Unable to create directory '${dirObject.path}': ${e.message}`)
323
- }
324
- }
325
-
326
- /**
327
- * Computes the relative path from one file or directory to another.
328
- *
329
- * If the target is outside the source (i.e., the relative path starts with ".."),
330
- * returns the absolute path to the target instead.
331
- *
332
- * @param {FileObject|DirectoryObject} from - The source file or directory object
333
- * @param {FileObject|DirectoryObject} to - The target file or directory object
334
- * @returns {string} The relative path from `from` to `to`, or the absolute path if not reachable
335
- */
336
- static relativeOrAbsolutePath(from, to) {
337
- const relative = path.relative(from.path, to.path)
338
-
339
- return relative.startsWith("..")
340
- ? to.path
341
- : relative
342
- }
343
-
344
- /**
345
- * Merge two paths by finding overlapping segments and combining them efficiently
346
- *
347
- * @param {string} path1 - The first path
348
- * @param {string} path2 - The second path to merge with the first
349
- * @param {string} [sep] - The path separator to use (defaults to system separator)
350
- * @returns {string} The merged path
351
- */
352
- static mergeOverlappingPaths(path1, path2, sep=path.sep) {
353
- const from = path1.split(sep).filter(Boolean)
354
- const to = path2.split(sep).filter(Boolean)
355
-
356
- // If they're the same, just return path1
357
- if(to.length === from.length && from.every((f, i) => to[i] === f)) {
358
- return path1
359
- }
360
-
361
- const overlapIndex = from.findLastIndex((curr, index) => {
362
- const left = from.at(index)
363
- const right = to.at(0)
364
-
365
- return left === right
366
- })
367
-
368
- // If overlap is found, slice and join
369
- if(overlapIndex !== -1) {
370
- const prefix = from.slice(0, overlapIndex)
371
-
372
- return path.join(...prefix, ...to)
373
- }
374
-
375
- // If no overlap, just join the paths
376
- return path.join(path1, path2)
377
- }
378
-
379
- /**
380
- * Resolve a path relative to another path using various strategies
381
- * Handles absolute paths, relative navigation, and overlap-based merging
382
- *
383
- * @param {string} fromPath - The base path to resolve from
384
- * @param {string} toPath - The target path to resolve
385
- * @returns {string} The resolved path
386
- */
387
- static resolvePath(fromPath, toPath) {
388
- // Normalize inputs
389
- const from = fromPath.trim()
390
- const to = toPath.trim()
391
-
392
- // Handle empty cases
393
- if(!from && !to)
394
- return ""
395
-
396
- if(!from)
397
- return to
398
-
399
- if(!to)
400
- return from
401
-
402
- // Strategy 1: If 'to' is absolute, it's standalone
403
- if(path.isAbsolute(to))
404
- return to
405
-
406
- // Strategy 2: If 'to' contains relative navigation (./ or ../)
407
- if(to.includes("./") || to.includes("../") || to.startsWith(".") || to.startsWith(".."))
408
- return path.resolve(from, to)
409
-
410
- // Strategy 3: Try overlap-based merging, which will default to a basic
411
- // join if no overlap
412
- return File.mergeOverlappingPaths(from, to)
413
- }
414
- }
@@ -1,83 +0,0 @@
1
- // Implementation: ../lib/File.js
2
- // Type definitions for File utilities
3
-
4
- import FileObject from './FileObject.js'
5
- import DirectoryObject from './DirectoryObject.js'
6
-
7
- export interface FileParts {
8
- /** The file name with extension */
9
- base: string
10
- /** The directory path */
11
- dir: string
12
- /** The file extension (including dot) */
13
- ext: string
14
- }
15
-
16
- export interface DirectoryListing {
17
- /** Array of FileObject instances */
18
- files: Array<FileObject>
19
- /** Array of DirectoryObject instances */
20
- directories: Array<DirectoryObject>
21
- }
22
-
23
- /**
24
- * File system utilities for reading, writing, and manipulating files and directories.
25
- */
26
- export default class File {
27
- /** Fix slashes in a path */
28
- static fixSlashes(pathName: string): string
29
-
30
- /** Convert a path to a URI */
31
- static pathToUri(pathName: string): string
32
-
33
- /** Convert a URI to a path */
34
- static uriToPath(pathName: string): string
35
-
36
- /** Check if a file can be read */
37
- static canReadFile(file: FileObject): Promise<boolean>
38
-
39
- /** Check if a file can be written */
40
- static canWriteFile(file: FileObject): Promise<boolean>
41
-
42
- /** Check if a file exists */
43
- static fileExists(file: FileObject): Promise<boolean>
44
-
45
- /** Get the size of a file */
46
- static fileSize(file: FileObject): Promise<number | null>
47
-
48
- /** Get the last modification time of a file */
49
- static fileModified(file: FileObject): Promise<Date | null>
50
-
51
- /** Check if a directory exists */
52
- static directoryExists(dirObject: DirectoryObject): Promise<boolean>
53
-
54
- /** Deconstruct a filename into parts */
55
- static deconstructFilenameToParts(fileName: string): FileParts
56
-
57
- /** Retrieve files matching glob pattern(s) */
58
- static getFiles(glob: string | Array<string>): Promise<Array<FileObject>>
59
-
60
- /** List the contents of a directory */
61
- static ls(directory: DirectoryObject): Promise<DirectoryListing>
62
-
63
- /** Read the content of a file */
64
- static readFile(fileObject: FileObject): Promise<string>
65
-
66
- /** Write content to a file */
67
- static writeFile(fileObject: FileObject, content: string): Promise<void>
68
-
69
- /** Load an object from JSON or YAML file */
70
- static loadDataFile(fileObject: FileObject): Promise<any>
71
-
72
- /** Ensure a directory exists, creating it if necessary */
73
- static assureDirectory(dirObject: DirectoryObject, options?: any): Promise<boolean>
74
-
75
- /** Compute relative path between two file system objects */
76
- static relativeOrAbsolutePath(from: FileObject | DirectoryObject, to: FileObject | DirectoryObject): string
77
-
78
- /** Merge two paths by finding overlapping segments and combining them efficiently */
79
- static mergeOverlappingPaths(path1: string, path2: string, sep?: string): string
80
-
81
- /** Resolve a path relative to another path using various strategies. Handles absolute paths, relative navigation, and overlap-based merging */
82
- static resolvePath(fromPath: string, toPath: string): string
83
- }