@gesslar/toolkit 0.0.12 → 0.0.13

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gesslar/toolkit",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "Get in, bitches, we're going toolkitting.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -63,10 +63,10 @@
63
63
  "devDependencies": {
64
64
  "@stylistic/eslint-plugin": "^5.4.0",
65
65
  "@types/node": "^24.5.2",
66
- "@typescript-eslint/eslint-plugin": "^8.44.1",
67
- "@typescript-eslint/parser": "^8.44.1",
66
+ "@typescript-eslint/eslint-plugin": "^8.44.0",
67
+ "@typescript-eslint/parser": "^8.44.0",
68
68
  "eslint": "^9.36.0",
69
- "eslint-plugin-jsdoc": "^60.3.0",
69
+ "eslint-plugin-jsdoc": "^60.1.0",
70
70
  "typedoc": "^0.28.13",
71
71
  "typedoc-plugin-markdown": "^4.9.0"
72
72
  }
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 File} from "./lib/File.js"
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,4 @@
1
- import File from "./File.js"
1
+ import File from "./FS.js"
2
2
  import FileObject from "./FileObject.js"
3
3
  import Sass from "./Sass.js"
4
4
 
@@ -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 File from "./File.js"
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
@@ -54,10 +57,12 @@ export default class DirectoryObject {
54
57
  * @param {string} directory - The directory path
55
58
  */
56
59
  constructor(directory) {
57
- const fixedDir = File.fixSlashes(directory ?? ".")
60
+ super(directory)
61
+
62
+ const fixedDir = FS.fixSlashes(directory ?? ".")
58
63
  const absolutePath = path.resolve(fixedDir)
59
- const fileUri = File.pathToUri(absolutePath)
60
- const filePath = File.uriToPath(fileUri)
64
+ const fileUri = FS.pathToUri(absolutePath)
65
+ const filePath = FS.uriToPath(fileUri)
61
66
  const baseName = path.basename(absolutePath) || "."
62
67
 
63
68
  this.#meta.supplied = fixedDir
@@ -112,7 +117,7 @@ export default class DirectoryObject {
112
117
  * @returns {Promise<boolean>} - A Promise that resolves to true or false
113
118
  */
114
119
  get exists() {
115
- return File.directoryExists(this)
120
+ return this.#directoryExists()
116
121
  }
117
122
 
118
123
  /**
@@ -186,4 +191,66 @@ export default class DirectoryObject {
186
191
  get isDirectory() {
187
192
  return this.#meta.isDirectory
188
193
  }
194
+
195
+ /**
196
+ * Check if a directory exists
197
+ *
198
+ * @returns {Promise<boolean>} Whether the directory exists
199
+ */
200
+ async #directoryExists() {
201
+ try {
202
+ (await fs.opendir(this.path)).close()
203
+
204
+ return true
205
+ } catch(_) {
206
+ return false
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Lists the contents of a directory.
212
+ *
213
+ * @param {DirectoryObject} directory - The directory to list.
214
+ * @returns {Promise<{files: Array<FileObject>, directories: Array<DirectoryObject>}>} The files and directories in the directory.
215
+ */
216
+ async read(directory) {
217
+ const found = await fs.readdir(directory.uri, {withFileTypes: true})
218
+ const results = await Promise.all(
219
+ found.map(async dirent => {
220
+ const fullPath = path.join(directory.uri, dirent.name)
221
+ const stat = await fs.stat(fullPath)
222
+
223
+ return {dirent, stat, fullPath}
224
+ }),
225
+ )
226
+
227
+ const files = results
228
+ .filter(({stat}) => stat.isFile())
229
+ .map(({fullPath}) => new FileObject(fullPath))
230
+
231
+ const directories = results
232
+ .filter(({stat}) => stat.isDirectory())
233
+ .map(({fullPath}) => new DirectoryObject(fullPath))
234
+
235
+ return {files, directories}
236
+ }
237
+
238
+ /**
239
+ * Ensures a directory exists, creating it if necessary
240
+ *
241
+ * @async
242
+ * @param {object} [options] - Any options to pass to mkdir
243
+ * @returns {Promise<void>}
244
+ * @throws {Sass} If directory creation fails
245
+ */
246
+ async assureExists(options = {}) {
247
+ if(await this.exists)
248
+ return
249
+
250
+ try {
251
+ await fs.mkdir(this.path, options)
252
+ } catch(e) {
253
+ throw Sass.new(`Unable to create directory '${this.path}': ${e.message}`)
254
+ }
255
+ }
189
256
  }
package/src/lib/FS.js ADDED
@@ -0,0 +1,187 @@
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 from = path1.split(sep).filter(Boolean)
127
+ const to = path2.split(sep).filter(Boolean)
128
+
129
+ // If they're the same, just return path1
130
+ if(to.length === from.length && from.every((f, i) => to[i] === f)) {
131
+ return path1
132
+ }
133
+
134
+ const overlapIndex = from.findLastIndex((curr, index) => {
135
+ const left = from.at(index)
136
+ const right = to.at(0)
137
+
138
+ return left === right
139
+ })
140
+
141
+ // If overlap is found, slice and join
142
+ if(overlapIndex !== -1) {
143
+ const prefix = from.slice(0, overlapIndex)
144
+
145
+ return path.join(...prefix, ...to)
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
+ // Strategy 1: If 'to' is absolute, it's standalone
176
+ if(path.isAbsolute(to))
177
+ return to
178
+
179
+ // Strategy 2: If 'to' contains relative navigation (./ or ../)
180
+ if(to.includes("./") || to.includes("../") || to.startsWith(".") || to.startsWith(".."))
181
+ return path.resolve(from, to)
182
+
183
+ // Strategy 3: Try overlap-based merging, which will default to a basic
184
+ // join if no overlap
185
+ return FS.mergeOverlappingPaths(from, to)
186
+ }
187
+ }
@@ -4,11 +4,16 @@
4
4
  * resolution and existence checks.
5
5
  */
6
6
 
7
+ import JSON5 from "json5"
8
+ import fs from "node:fs/promises"
7
9
  import path from "node:path"
8
10
  import util from "node:util"
11
+ import YAML from "yaml"
9
12
 
10
13
  import DirectoryObject from "./DirectoryObject.js"
11
- import File from "./File.js"
14
+ import FS from "./FS.js"
15
+ import Sass from "./Sass.js"
16
+ import Valid from "./Valid.js"
12
17
 
13
18
  /**
14
19
  * FileObject encapsulates metadata and operations for a file, including path
@@ -26,7 +31,7 @@ import File from "./File.js"
26
31
  * @property {Promise<boolean>} exists - Whether the file exists (async)
27
32
  */
28
33
 
29
- export default class FileObject {
34
+ export default class FileObject extends FS {
30
35
  /**
31
36
  * @type {object}
32
37
  * @private
@@ -59,9 +64,11 @@ export default class FileObject {
59
64
  * @param {DirectoryObject|string|null} [directory] - The parent directory (object or string)
60
65
  */
61
66
  constructor(fileName, directory=null) {
62
- const fixedFile = File.fixSlashes(fileName)
67
+ super(fileName, directory)
63
68
 
64
- const {dir,base,ext} = File.deconstructFilenameToParts(fixedFile)
69
+ const fixedFile = FS.fixSlashes(fileName)
70
+
71
+ const {dir,base,ext} = this.#deconstructFilenameToParts(fixedFile)
65
72
 
66
73
  if(!directory)
67
74
  directory = new DirectoryObject(dir)
@@ -71,7 +78,7 @@ export default class FileObject {
71
78
  : path.resolve(directory?.path ?? ".", fixedFile)
72
79
 
73
80
  const resolved = final
74
- const fileUri = File.pathToUri(resolved)
81
+ const fileUri = FS.pathToUri(resolved)
75
82
 
76
83
  this.#meta.supplied = fixedFile
77
84
  this.#meta.path = resolved
@@ -80,7 +87,7 @@ export default class FileObject {
80
87
  this.#meta.extension = ext
81
88
  this.#meta.module = path.basename(this.supplied, this.extension)
82
89
 
83
- const {dir: newDir} = File.deconstructFilenameToParts(this.path)
90
+ const {dir: newDir} = this.#deconstructFilenameToParts(this.path)
84
91
 
85
92
  this.#meta.directory = new DirectoryObject(newDir)
86
93
 
@@ -127,7 +134,7 @@ export default class FileObject {
127
134
  * @returns {Promise<boolean>} - A Promise that resolves to true or false
128
135
  */
129
136
  get exists() {
130
- return File.fileExists(this)
137
+ return this.#fileExists()
131
138
  }
132
139
 
133
140
  /**
@@ -219,4 +226,161 @@ export default class FileObject {
219
226
  get directory() {
220
227
  return this.#meta.directory
221
228
  }
229
+
230
+ /**
231
+ * Check if a file can be read. Returns true if the file can be read, false
232
+ *
233
+ * @returns {Promise<boolean>} Whether the file can be read
234
+ */
235
+ async canRead() {
236
+ try {
237
+ await fs.access(this.path, fs.constants.R_OK)
238
+
239
+ return true
240
+ } catch(_) {
241
+ return false
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Check if a file can be written. Returns true if the file can be written,
247
+ *
248
+ * @returns {Promise<boolean>} Whether the file can be written
249
+ */
250
+ async canWrite() {
251
+ try {
252
+ await fs.access(this.path, fs.constants.W_OK)
253
+
254
+ return true
255
+ } catch(_) {
256
+ return false
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Check if a file exists
262
+ *
263
+ * @returns {Promise<boolean>} Whether the file exists
264
+ */
265
+ async #fileExists() {
266
+ try {
267
+ await fs.access(this.path, fs.constants.F_OK)
268
+
269
+ return true
270
+ } catch(_) {
271
+ return false
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Determines the size of a file.
277
+ *
278
+ * @returns {Promise<number?>} - The size of the file or null, if it doesn't exist.
279
+ */
280
+ async size() {
281
+ try {
282
+ const stat = await fs.stat(this.path)
283
+
284
+ return stat.size
285
+ } catch(_) {
286
+ return null
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Gets the last modification time of a file.
292
+ * Used by the caching system to determine if cached data is still valid.
293
+ *
294
+ * @returns {Promise<Date?>} The last modification time, or null if file doesn't exist
295
+ */
296
+ async modified() {
297
+ try {
298
+ const stat = await fs.stat(this.path)
299
+
300
+ return stat.mtime
301
+ } catch(_) {
302
+ return null
303
+ }
304
+ }
305
+
306
+ /**
307
+ * @typedef {object} FileParts
308
+ * @property {string} base - The file name with extension
309
+ * @property {string} dir - The directory path
310
+ * @property {string} ext - The file extension (including dot)
311
+ */
312
+
313
+ /**
314
+ * Deconstruct a filename into parts
315
+ *
316
+ * @param {string} fileName - The filename to deconstruct
317
+ * @returns {FileParts} The filename parts
318
+ */
319
+ #deconstructFilenameToParts(fileName) {
320
+ Valid.assert(typeof fileName === "string" && fileName.length > 0,
321
+ "file must be a non-zero length string", 1)
322
+
323
+ return path.parse(fileName)
324
+ }
325
+
326
+ /**
327
+ * Reads the content of a file asynchronously.
328
+ *
329
+ * @param {string} [encoding] - The encoding to read the file as.
330
+ * @returns {Promise<string>} The file contents
331
+ */
332
+ async read(encoding="utf8") {
333
+ const filePath = this.path
334
+
335
+ if(!(await this.exists))
336
+ throw Sass.new(`No such file '${filePath}'`)
337
+
338
+ if(!filePath)
339
+ throw Sass.new("No absolute path in file map")
340
+
341
+ return await fs.readFile(filePath, encoding)
342
+ }
343
+
344
+ /**
345
+ * Writes content to a file synchronously.
346
+ *
347
+ * @param {string} content - The content to write
348
+ * @param {string} encoding - The encoding in which to write.
349
+ * @returns {Promise<void>}
350
+ */
351
+ async write(content, encoding="utf8") {
352
+ if(!this.path)
353
+ throw Sass.new("No absolute path in file")
354
+
355
+ await fs.writeFile(this.path, content, encoding)
356
+ }
357
+
358
+ /**
359
+ * Loads an object from JSON or YAML provided a fileMap
360
+ *
361
+ * @param {string} [type] - The expected type of data to parse.
362
+ * @param {string} [encoding] - The encoding to read the file as.
363
+ * @returns {object} The parsed data object.
364
+ */
365
+ async loadData(type="any", encoding="utf8") {
366
+ const content = await this.read(encoding)
367
+ const toTry = {
368
+ json5: [JSON5],
369
+ json: [JSON5],
370
+ yaml: [YAML],
371
+ any: [JSON5,YAML]
372
+ }[type.toLowerCase()]
373
+
374
+ for(const [format] of toTry) {
375
+ try {
376
+ const result = format.parse(content)
377
+
378
+ return result
379
+ } catch {
380
+ // nothing to see here
381
+ }
382
+ }
383
+
384
+ throw Sass.new(`Content is neither valid JSON5 nor valid YAML:\n'${this.path}'`)
385
+ }
222
386
  }
@@ -1,11 +1,21 @@
1
1
  // Implementation: ../lib/DirectoryObject.js
2
2
  // Type definitions for DirectoryObject
3
3
 
4
+ import FS from './FS.js'
5
+ import FileObject from './FileObject.js'
6
+
7
+ export interface DirectoryListing {
8
+ /** Array of FileObject instances */
9
+ files: Array<FileObject>
10
+ /** Array of DirectoryObject instances */
11
+ directories: Array<DirectoryObject>
12
+ }
13
+
4
14
  /**
5
15
  * DirectoryObject encapsulates metadata and operations for a directory,
6
16
  * including path resolution and existence checks.
7
17
  */
8
- export default class DirectoryObject {
18
+ export default class DirectoryObject extends FS {
9
19
  /**
10
20
  * Create a new DirectoryObject instance.
11
21
  * @param directory - The directory path
@@ -53,4 +63,10 @@ export default class DirectoryObject {
53
63
  isFile: boolean
54
64
  isDirectory: boolean
55
65
  }
56
- }
66
+
67
+ /** List the contents of this directory */
68
+ read(): Promise<DirectoryListing>
69
+
70
+ /** Ensure this directory exists, creating it if necessary */
71
+ assureExists(options?: any): Promise<void>
72
+ }
@@ -0,0 +1,31 @@
1
+ // Implementation: ../lib/FS.js
2
+ // Type definitions for FS utilities
3
+
4
+ import FileObject from './FileObject.js'
5
+ import DirectoryObject from './DirectoryObject.js'
6
+
7
+ /**
8
+ * Base filesystem utilities class. FileObject and DirectoryObject extend this class.
9
+ */
10
+ export default class FS {
11
+ /** Fix slashes in a path */
12
+ static fixSlashes(pathName: string): string
13
+
14
+ /** Convert a path to a URI */
15
+ static pathToUri(pathName: string): string
16
+
17
+ /** Convert a URI to a path */
18
+ static uriToPath(pathName: string): string
19
+
20
+ /** Retrieve files matching glob pattern(s) */
21
+ static getFiles(glob: string | Array<string>): Promise<Array<FileObject>>
22
+
23
+ /** Compute relative path between two file system objects */
24
+ static relativeOrAbsolutePath(from: FileObject | DirectoryObject, to: FileObject | DirectoryObject): string
25
+
26
+ /** Merge two paths by finding overlapping segments and combining them efficiently */
27
+ static mergeOverlappingPaths(path1: string, path2: string, sep?: string): string
28
+
29
+ /** Resolve a path relative to another path using various strategies. Handles absolute paths, relative navigation, and overlap-based merging */
30
+ static resolvePath(fromPath: string, toPath: string): string
31
+ }
@@ -1,6 +1,7 @@
1
1
  // Implementation: ../lib/FileObject.js
2
2
 
3
3
  import DirectoryObject from './DirectoryObject.js'
4
+ import FS from './FS.js'
4
5
 
5
6
  /**
6
7
  * FileObject encapsulates metadata and operations for a file, providing intelligent
@@ -60,7 +61,7 @@ import DirectoryObject from './DirectoryObject.js'
60
61
  *
61
62
  * for (const fileItem of files) {
62
63
  * console.log(`${fileItem.module}${fileItem.extension} -> ${fileItem.path}`)
63
- *
64
+ *
64
65
  * // Type-based processing
65
66
  * switch (fileItem.extension) {
66
67
  * case '.json':
@@ -99,7 +100,7 @@ import DirectoryObject from './DirectoryObject.js'
99
100
  * inherits the directory's resolved path, ensuring consistency in hierarchical
100
101
  * file operations.
101
102
  */
102
- export default class FileObject {
103
+ export default class FileObject extends FS {
103
104
  /**
104
105
  * Create a new FileObject instance with intelligent path resolution.
105
106
  *
@@ -142,8 +143,8 @@ export default class FileObject {
142
143
  * // Complex directory structures and nested files
143
144
  * const projectRoot = new DirectoryObject('./my-project')
144
145
  * const srcDir = new DirectoryObject('src', projectRoot)
145
- *
146
- * // Create files within nested directory structure
146
+ *
147
+ * // Create files within nested directory structure
147
148
  * const mainApp = new FileObject('App.tsx', srcDir)
148
149
  * const stylesheet = new FileObject('styles/main.css', srcDir)
149
150
  * const testFile = new FileObject('__tests__/App.test.tsx', srcDir)
@@ -151,7 +152,7 @@ export default class FileObject {
151
152
  * console.log('Main app:', mainApp.path) // /absolute/path/my-project/src/App.tsx
152
153
  * console.log('Stylesheet:', stylesheet.path) // /absolute/path/my-project/src/styles/main.css
153
154
  * console.log('Test file:', testFile.path) // /absolute/path/my-project/src/__tests__/App.test.tsx
154
- *
155
+ *
155
156
  * // All files share the same parent directory reference
156
157
  * console.log('Same parent?', mainApp.directory === srcDir) // true
157
158
  * ```
@@ -160,7 +161,7 @@ export default class FileObject {
160
161
 
161
162
  /**
162
163
  * The original user-supplied path string used during construction.
163
- *
164
+ *
164
165
  * Preserves the exact path string passed to the constructor, including
165
166
  * any relative path indicators (./, ../) or path separators. Useful
166
167
  * for debugging, logging, or when you need to recreate the original
@@ -170,7 +171,7 @@ export default class FileObject {
170
171
  * ```typescript
171
172
  * const file1 = new FileObject('./config.json')
172
173
  * const file2 = new FileObject('../package.json')
173
- *
174
+ *
174
175
  * console.log(file1.supplied) // './config.json'
175
176
  * console.log(file2.supplied) // '../package.json'
176
177
  * console.log(file1.path) // '/absolute/path/to/config.json'
@@ -181,7 +182,7 @@ export default class FileObject {
181
182
 
182
183
  /**
183
184
  * The fully resolved absolute file path with normalized separators.
184
- *
185
+ *
185
186
  * Automatically resolved during construction using Node.js path utilities.
186
187
  * Always uses forward slashes on Unix systems and backslashes on Windows.
187
188
  * This is the canonical path that should be used for all file operations.
@@ -189,9 +190,9 @@ export default class FileObject {
189
190
  * @example
190
191
  * ```typescript
191
192
  * // Different inputs, same resolved path
192
- * const file1 = new FileObject('./src/../config.json')
193
+ * const file1 = new FileObject('./src/../config.json')
193
194
  * const file2 = new FileObject('config.json')
194
- *
195
+ *
195
196
  * console.log(file1.path) // '/absolute/path/config.json'
196
197
  * console.log(file2.path) // '/absolute/path/config.json'
197
198
  * console.log(file1.path === file2.path) // true
@@ -201,7 +202,7 @@ export default class FileObject {
201
202
 
202
203
  /**
203
204
  * The file URI representation following RFC 3986 standard.
204
- *
205
+ *
205
206
  * Converts the absolute file path to a proper file:// URI scheme,
206
207
  * handling URL encoding for special characters and proper formatting
207
208
  * for cross-platform file URI access.
@@ -209,9 +210,9 @@ export default class FileObject {
209
210
  * @example
210
211
  * ```typescript
211
212
  * const file = new FileObject('./my project/config file.json')
212
- * console.log(file.uri)
213
+ * console.log(file.uri)
213
214
  * // 'file:///absolute/path/my%20project/config%20file.json'
214
- *
215
+ *
215
216
  * // Can be used with URL constructor or file:// handlers
216
217
  * const url = new URL(file.uri)
217
218
  * console.log(url.pathname) // '/absolute/path/my project/config file.json'
@@ -221,7 +222,7 @@ export default class FileObject {
221
222
 
222
223
  /**
223
224
  * The complete filename including extension.
224
- *
225
+ *
225
226
  * Extracted from the resolved path using Node.js path utilities.
226
227
  * Includes the file extension but excludes any directory components.
227
228
  *
@@ -229,7 +230,7 @@ export default class FileObject {
229
230
  * ```typescript
230
231
  * const jsFile = new FileObject('./src/components/Button.tsx')
231
232
  * const configFile = new FileObject('../.env.production')
232
- *
233
+ *
233
234
  * console.log(jsFile.name) // 'Button.tsx'
234
235
  * console.log(configFile.name) // '.env.production'
235
236
  * ```
@@ -238,7 +239,7 @@ export default class FileObject {
238
239
 
239
240
  /**
240
241
  * The filename without its extension, suitable for module identification.
241
- *
242
+ *
242
243
  * Useful for generating module names, import statements, or when you need
243
244
  * the base name without file type information. Handles complex extensions
244
245
  * and dotfiles appropriately.
@@ -247,7 +248,7 @@ export default class FileObject {
247
248
 
248
249
  /**
249
250
  * The file extension including the leading dot.
250
- *
251
+ *
251
252
  * Extracted using Node.js path utilities, always includes the dot prefix.
252
253
  * Returns an empty string for files without extensions. Handles multiple
253
254
  * extensions by returning only the last one.
@@ -262,7 +263,7 @@ export default class FileObject {
262
263
 
263
264
  /**
264
265
  * The parent DirectoryObject containing this file.
265
- *
266
+ *
266
267
  * Automatically created during FileObject construction based on the resolved
267
268
  * file path. Provides access to parent directory operations and maintains
268
269
  * the hierarchical relationship between files and directories.
@@ -271,7 +272,7 @@ export default class FileObject {
271
272
 
272
273
  /**
273
274
  * Promise that resolves to whether the file exists on the filesystem.
274
- *
275
+ *
275
276
  * Performs an asynchronous filesystem check to determine file existence.
276
277
  * The Promise will resolve to true if the file exists and is accessible,
277
278
  * false otherwise. Always await this property before using the result.
@@ -290,4 +291,25 @@ export default class FileObject {
290
291
  isDirectory: boolean
291
292
  directory: string | null
292
293
  }
294
+
295
+ /** Check if a file can be read */
296
+ canRead(): Promise<boolean>
297
+
298
+ /** Check if a file can be written */
299
+ canWrite(): Promise<boolean>
300
+
301
+ /** Get the size of a file */
302
+ size(): Promise<number | null>
303
+
304
+ /** Get the last modification time of a file */
305
+ modified(): Promise<Date | null>
306
+
307
+ /** Read the content of a file */
308
+ read(encoding?: string): Promise<string>
309
+
310
+ /** Write content to a file */
311
+ write(content: string, encoding?: string): Promise<void>
312
+
313
+ /** Load an object from JSON5 or YAML file with type specification */
314
+ loadData(type?: 'json' | 'json5' | 'yaml' | 'any', encoding?: string): Promise<any>
293
315
  }
@@ -2,7 +2,7 @@
2
2
  // Core file system abstractions
3
3
  export { default as FileObject } from './FileObject.js'
4
4
  export { default as DirectoryObject } from './DirectoryObject.js'
5
- export { default as File } from './File.js'
5
+ export { default as FS } from './FS.js'
6
6
 
7
7
  // Utility classes
8
8
  export { default as Cache } from './Cache.js'
@@ -15,4 +15,4 @@ export { default as Util } from './Util.js'
15
15
  export { default as Valid } from './Valid.js'
16
16
 
17
17
  // Type exports
18
- export type { FileParts } from './File.js'
18
+ export type { FileParts } from './FS.js'
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
- }