@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gesslar/toolkit",
3
- "version": "0.0.12",
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 examples/FileSystem/index.js",
30
- "docs:dev": "npm run docs:build && cd docs && npm run start",
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.1",
67
- "@typescript-eslint/parser": "^8.44.1",
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.3.0",
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 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,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<object>} The parsed file data (JSON5 or YAML)
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 File.fileModified(fileObject)
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 File.loadDataFile(fileObject)
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, "object"))
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
- switch(type.toLowerCase()) {
345
- case "array":
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 value !== null && valueType === "object" && !Array.isArray(value) // Excludes arrays and null
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 false // Unknown type
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
- return Array.isArray(value) ? "array" : typeof value
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 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
@@ -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
- const fixedDir = File.fixSlashes(directory ?? ".")
59
+ super()
60
+
61
+ const fixedDir = FS.fixSlashes(directory ?? ".")
58
62
  const absolutePath = path.resolve(fixedDir)
59
- const fileUri = File.pathToUri(absolutePath)
60
- const filePath = File.uriToPath(fileUri)
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 File.directoryExists(this)
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
+ }