@gesslar/toolkit 0.0.2 → 0.0.4

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.2",
3
+ "version": "0.0.4",
4
4
  "description": "Get in, bitches, we're going toolkitting.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -4,8 +4,10 @@ export {default as DirectoryObject} from "./lib/DirectoryObject.js"
4
4
  export {default as File} from "./lib/File.js"
5
5
 
6
6
  // Utility classes
7
+ export {default as Cache} from "./lib/Cache.js"
7
8
  export {default as Data} from "./lib/Data.js"
8
9
  export {default as Sass} from "./lib/Sass.js"
9
10
  export {default as Term} from "./lib/Term.js"
10
11
  export {default as Type} from "./lib/Type.js"
12
+ export {default as Util} from "./lib/Util.js"
11
13
  export {default as Valid} from "./lib/Valid.js"
@@ -0,0 +1,74 @@
1
+ import File from "./File.js"
2
+ import FileObject from "./FileObject.js"
3
+ import Sass from "./Sass.js"
4
+
5
+ /**
6
+ * File system cache with automatic invalidation based on modification time.
7
+ * Provides intelligent caching of parsed JSON5/YAML files with mtime-based
8
+ * cache invalidation to optimize performance for repeated file access.
9
+ *
10
+ * The cache eliminates redundant file reads and parsing when multiple processes
11
+ * access the same dependency files, while ensuring data freshness through
12
+ * modification time checking.
13
+ */
14
+ export default class Cache {
15
+ /** @type {Map<string, Date>} Map of file paths to last modification times */
16
+ #modifiedTimes = new Map()
17
+ /** @type {Map<string, object>} Map of file paths to parsed file data */
18
+ #dataCache = new Map()
19
+
20
+ /**
21
+ * Removes cached data for a specific file from both time and data maps.
22
+ * Used when files are modified or when cache consistency needs to be
23
+ * maintained.
24
+ *
25
+ * @private
26
+ * @param {FileObject} file - The file object to remove from cache
27
+ * @returns {void}
28
+ */
29
+ #cleanup(file) {
30
+ this.#modifiedTimes.delete(file.path)
31
+ this.#dataCache.delete(file.path)
32
+ }
33
+
34
+ /**
35
+ * Loads and caches parsed file data with automatic invalidation based on
36
+ * modification time.
37
+ *
38
+ * Implements a sophisticated caching strategy that checks file modification
39
+ * times to determine whether cached data is still valid, ensuring data
40
+ * freshness while optimizing performance for repeated file access during
41
+ * parallel processing.
42
+ *
43
+ * @param {FileObject} fileObject - The file object to load and cache
44
+ * @returns {Promise<object>} The parsed file data (JSON5 or YAML)
45
+ * @throws {Sass} If the file cannot be found or accessed
46
+ */
47
+ async loadCachedData(fileObject) {
48
+ const lastModified = await File.fileModified(fileObject)
49
+
50
+ if(lastModified === null)
51
+ throw Sass.new(`Unable to find file '${fileObject.path}'`)
52
+
53
+ if(this.#modifiedTimes.has(fileObject.path)) {
54
+ const lastCached = this.#modifiedTimes.get(fileObject.path)
55
+
56
+ if(lastModified > lastCached) {
57
+ this.#cleanup(fileObject)
58
+ } else {
59
+ if(!(this.#dataCache.has(fileObject.path)))
60
+ this.#cleanup(fileObject)
61
+ else {
62
+ return this.#dataCache.get(fileObject.path)
63
+ }
64
+ }
65
+ }
66
+
67
+ const data = await File.loadDataFile(fileObject)
68
+
69
+ this.#modifiedTimes.set(fileObject.path, lastModified)
70
+ this.#dataCache.set(fileObject.path, data)
71
+
72
+ return data
73
+ }
74
+ }
package/src/lib/Data.js CHANGED
@@ -3,8 +3,8 @@
3
3
  * Provides comprehensive utilities for working with JavaScript data types and structures.
4
4
  */
5
5
 
6
- import TypeSpec from "./Type.js"
7
6
  import Sass from "./Sass.js"
7
+ import TypeSpec from "./Type.js"
8
8
  import Valid from "./Valid.js"
9
9
 
10
10
  export default class Data {
package/src/lib/File.js CHANGED
@@ -11,10 +11,10 @@ import path from "node:path"
11
11
  import url from "node:url"
12
12
  import YAML from "yaml"
13
13
 
14
- import Sass from "./Sass.js"
15
14
  import Data from "./Data.js"
16
15
  import DirectoryObject from "./DirectoryObject.js"
17
16
  import FileObject from "./FileObject.js"
17
+ import Sass from "./Sass.js"
18
18
  import Valid from "./Valid.js"
19
19
 
20
20
  export default class File {
@@ -37,7 +37,7 @@ export default class File {
37
37
  static pathToUri(pathName) {
38
38
  try {
39
39
  return url.pathToFileURL(pathName).href
40
- } catch (e) {
40
+ } catch(e) {
41
41
  void e // stfu linter
42
42
 
43
43
  return pathName
@@ -55,7 +55,7 @@ export default class File {
55
55
  await fs.access(file.path, fs.constants.R_OK)
56
56
 
57
57
  return true
58
- } catch (_) {
58
+ } catch(_) {
59
59
  return false
60
60
  }
61
61
  }
@@ -71,7 +71,7 @@ export default class File {
71
71
  await fs.access(file.path, fs.constants.W_OK)
72
72
 
73
73
  return true
74
- } catch (_error) {
74
+ } catch(_) {
75
75
  return false
76
76
  }
77
77
  }
@@ -84,10 +84,10 @@ export default class File {
84
84
  */
85
85
  static async fileExists(file) {
86
86
  try {
87
- await fs.access(file.path, fs.constants.R_OK)
87
+ await fs.access(file.path, fs.constants.F_OK)
88
88
 
89
89
  return true
90
- } catch (_) {
90
+ } catch(_) {
91
91
  return false
92
92
  }
93
93
  }
@@ -103,7 +103,7 @@ export default class File {
103
103
  const stat = await fs.stat(file.path)
104
104
 
105
105
  return stat.size
106
- } catch (_) {
106
+ } catch(_) {
107
107
  return null
108
108
  }
109
109
  }
@@ -120,7 +120,7 @@ export default class File {
120
120
  const stat = await fs.stat(file.path)
121
121
 
122
122
  return stat.mtime
123
- } catch (_) {
123
+ } catch(_) {
124
124
  return null
125
125
  }
126
126
  }
@@ -136,7 +136,7 @@ export default class File {
136
136
  (await fs.opendir(dirObject.path)).close()
137
137
 
138
138
  return true
139
- } catch (_) {
139
+ } catch(_) {
140
140
  return false
141
141
  }
142
142
  }
@@ -150,7 +150,7 @@ export default class File {
150
150
  static uriToPath(pathName) {
151
151
  try {
152
152
  return url.fileURLToPath(pathName)
153
- } catch (_) {
153
+ } catch(_) {
154
154
  return pathName
155
155
  }
156
156
  }
@@ -211,11 +211,10 @@ export default class File {
211
211
  !globbyArray.length
212
212
  )
213
213
  throw Sass.new(
214
- `Invalid glob pattern: Array must contain only strings. Got ${JSON.stringify(glob)}`,
214
+ `Invalid glob pattern: Array cannot be empty. Got ${JSON.stringify(glob)}`,
215
215
  )
216
216
 
217
217
  // Use Globby to fetch matching files
218
-
219
218
  const filesArray = await globby(globbyArray)
220
219
  const files = filesArray.map(file => new FileObject(file))
221
220
 
@@ -310,20 +309,18 @@ export default class File {
310
309
  * @async
311
310
  * @param {DirectoryObject} dirObject - The path or DirMap of the directory to assure exists
312
311
  * @param {object} [options] - Any options to pass to mkdir
313
- * @returns {Promise<boolean>} True if directory exists, false otherwise
312
+ * @returns {Promise<void>}
314
313
  * @throws {Sass} If directory creation fails
315
314
  */
316
315
  static async assureDirectory(dirObject, options = {}) {
317
316
  if(await dirObject.exists)
318
- return true
317
+ return
319
318
 
320
319
  try {
321
320
  await fs.mkdir(dirObject.path, options)
322
- } catch (e) {
321
+ } catch(e) {
323
322
  throw Sass.new(`Unable to create directory '${dirObject.path}': ${e.message}`)
324
323
  }
325
-
326
- return dirObject.exists
327
324
  }
328
325
 
329
326
  /**
@@ -66,13 +66,9 @@ export default class FileObject {
66
66
  if(!directory)
67
67
  directory = new DirectoryObject(dir)
68
68
 
69
- let final
70
-
71
- if(path.isAbsolute(fixedFile)) {
72
- final = fixedFile
73
- } else {
74
- final = path.resolve(directory.path, fixedFile)
75
- }
69
+ const final = path.isAbsolute(fixedFile)
70
+ ? fixedFile
71
+ : path.resolve(directory.path, fixedFile)
76
72
 
77
73
  const resolved = final
78
74
  const fileUri = File.pathToUri(resolved)
@@ -0,0 +1,134 @@
1
+ import {createHash} from "node:crypto"
2
+ import {performance} from "node:perf_hooks"
3
+
4
+ /**
5
+ * Utility class providing common helper functions for string manipulation,
6
+ * timing, hashing, and option parsing.
7
+ */
8
+ export default class Util {
9
+ /**
10
+ * Capitalizes the first letter of a string.
11
+ *
12
+ * @param {string} text - The text to capitalize
13
+ * @returns {string} Text with first letter capitalized
14
+ */
15
+ static capitalize(text) {
16
+ return typeof text === "string"
17
+ && `${text.slice(0,1).toUpperCase()}${text.slice(1)}`
18
+ || text
19
+ }
20
+
21
+ /**
22
+ * Measure wall-clock time for an async function.
23
+ *
24
+ * @template T
25
+ * @param {() => Promise<T>} fn - Thunk returning a promise.
26
+ * @returns {Promise<{result: T, cost: number}>} Object containing result and elapsed ms (number, 1 decimal).
27
+ */
28
+ static async time(fn) {
29
+ const t0 = performance.now()
30
+ const result = await fn()
31
+ const cost = Math.round((performance.now() - t0) * 10) / 10
32
+
33
+ return {result, cost}
34
+ }
35
+
36
+ /**
37
+ * Right-align a string inside a fixed width (left pad with spaces).
38
+ * If the string exceeds width it is returned unchanged.
39
+ *
40
+ * @param {string|number} text - Text to align.
41
+ * @param {number} width - Target field width (default 80).
42
+ * @returns {string} Padded string.
43
+ */
44
+ static rightAlignText(text, width=80) {
45
+ const work = String(text)
46
+
47
+ if(work.length > width)
48
+ return work
49
+
50
+ const diff = width-work.length
51
+
52
+ return `${" ".repeat(diff)}${work}`
53
+ }
54
+
55
+ /**
56
+ * Compute sha256 hash (hex) of the provided string.
57
+ *
58
+ * @param {string} s - Input string.
59
+ * @returns {string} 64-char hexadecimal digest.
60
+ */
61
+ static hashOf(s) {
62
+ return createHash("sha256").update(s).digest("hex")
63
+ }
64
+
65
+ /**
66
+ * Extracts canonical option names from a Commander-style options object.
67
+ *
68
+ * Each key in the input object is a string containing one or more option
69
+ * forms, separated by commas (e.g. "-w, --watch"). This function splits each
70
+ * key, trims whitespace, and parses out the long option name (e.g. "watch")
71
+ * for each entry. If no long option ("--") is present, the short option (e.g.
72
+ * "v" from "-v") will be included in the result array. If both are present,
73
+ * the long option is preferred.
74
+ *
75
+ * Example:
76
+ * generateOptionNames({"-w, --watch": "desc", "-v": "desc"})
77
+ * → ["watch", "v"]
78
+ *
79
+ * Edge cases:
80
+ * - If a key contains only a short option ("-v"), that short name will be
81
+ * included in the result.
82
+ * - If multiple long options are present, only the first is used.
83
+ * - If the option string is malformed, may return undefined for that entry
84
+ * (filtered out).
85
+ *
86
+ * @param {object} object - Mapping of option strings to descriptions.
87
+ * @returns {string[]} Array of canonical option names (long preferred, short if no long present).
88
+ */
89
+ static generateOptionNames(object) {
90
+ return Object.keys(object)
91
+ .map(key => {
92
+ return key
93
+ .split(",")
94
+ .map(o => o.trim())
95
+ .map(o => o.match(/^(?<sign>--?)(?<option>[\w-]+)/)?.groups ?? {})
96
+ .reduce((acc, curr) => acc.sign === "--" ? acc : curr, {})
97
+ ?.option
98
+ })
99
+ .filter(Boolean)
100
+ }
101
+
102
+ /**
103
+ * Asynchronously awaits all promises in parallel.
104
+ * Wrapper around Promise.all for consistency with other utility methods.
105
+ *
106
+ * @param {Promise[]} promises - Array of promises to await
107
+ * @returns {Promise<unknown[]>} Results of all promises
108
+ */
109
+ static async awaitAll(promises) {
110
+ return await Promise.all(promises)
111
+ }
112
+
113
+ /**
114
+ * Settles all promises (both fulfilled and rejected) in parallel.
115
+ * Wrapper around Promise.allSettled for consistency with other utility methods.
116
+ *
117
+ * @param {Promise[]} promises - Array of promises to settle
118
+ * @returns {Promise<Array>} Results of all settled promises with status and value/reason
119
+ */
120
+ static async settleAll(promises) {
121
+ return await Promise.allSettled(promises)
122
+ }
123
+
124
+ /**
125
+ * Returns the first promise to resolve or reject from an array of promises.
126
+ * Wrapper around Promise.race for consistency with other utility methods.
127
+ *
128
+ * @param {Promise[]} promises - Array of promises to race
129
+ * @returns {Promise<unknown>} Result of the first settled promise
130
+ */
131
+ static async race(promises) {
132
+ return await Promise.race(promises)
133
+ }
134
+ }
@@ -0,0 +1,29 @@
1
+ import FileObject from '../lib/FileObject.js'
2
+
3
+ /**
4
+ * File system cache for theme compilation data with automatic invalidation.
5
+ * Provides intelligent caching of parsed JSON5/YAML files with mtime-based
6
+ * cache invalidation to optimize parallel theme compilation performance.
7
+ *
8
+ * The cache eliminates redundant file reads and parsing when multiple themes
9
+ * import the same dependency files, while ensuring data freshness through
10
+ * modification time checking.
11
+ */
12
+ declare class Cache {
13
+ /**
14
+ * Loads and caches parsed file data with automatic invalidation based on
15
+ * modification time.
16
+ *
17
+ * Implements a sophisticated caching strategy that checks file modification
18
+ * times to determine whether cached data is still valid, ensuring data
19
+ * freshness while optimizing performance for repeated file access during
20
+ * parallel theme compilation.
21
+ *
22
+ * @param fileObject - The file object to load and cache
23
+ * @returns The parsed file data (JSON5 or YAML)
24
+ * @throws If the file cannot be found or accessed
25
+ */
26
+ loadCachedData(fileObject: FileObject): Promise<object>
27
+ }
28
+
29
+ export default Cache
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Utility class providing common helper functions for string manipulation,
3
+ * timing, hashing, and option parsing.
4
+ */
5
+ declare class Util {
6
+ /**
7
+ * Capitalizes the first letter of a string.
8
+ *
9
+ * @param text - The text to capitalize
10
+ * @returns Text with first letter capitalized
11
+ */
12
+ static capitalize(text: string): string
13
+
14
+ /**
15
+ * Measure wall-clock time for an async function.
16
+ *
17
+ * @template T
18
+ * @param fn - Thunk returning a promise.
19
+ * @returns Object containing result and elapsed ms (number, 1 decimal).
20
+ */
21
+ static time<T>(fn: () => Promise<T>): Promise<{result: T, cost: number}>
22
+
23
+ /**
24
+ * Right-align a string inside a fixed width (left pad with spaces).
25
+ * If the string exceeds width it is returned unchanged.
26
+ *
27
+ * @param text - Text to align.
28
+ * @param width - Target field width (default 80).
29
+ * @returns Padded string.
30
+ */
31
+ static rightAlignText(text: string | number, width?: number): string
32
+
33
+ /**
34
+ * Compute sha256 hash (hex) of the provided string.
35
+ *
36
+ * @param s - Input string.
37
+ * @returns 64-char hexadecimal digest.
38
+ */
39
+ static hashOf(s: string): string
40
+
41
+ /**
42
+ * Extracts canonical option names from a Commander-style options object.
43
+ *
44
+ * Each key in the input object is a string containing one or more option
45
+ * forms, separated by commas (e.g. "-w, --watch"). This function splits each
46
+ * key, trims whitespace, and parses out the long option name (e.g. "watch")
47
+ * for each entry. If no long option ("--") is present, the short option (e.g.
48
+ * "v" from "-v") will be included in the result array. If both are present,
49
+ * the long option is preferred.
50
+ *
51
+ * @param object - Mapping of option strings to descriptions.
52
+ * @returns Array of canonical option names (long preferred, short if no long present).
53
+ */
54
+ static generateOptionNames(object: Record<string, any>): string[]
55
+
56
+ /**
57
+ * Asynchronously awaits all promises in parallel.
58
+ * Wrapper around Promise.all for consistency with other utility methods.
59
+ *
60
+ * @param promises - Array of promises to await
61
+ * @returns Results of all promises
62
+ */
63
+ static awaitAll<T>(promises: Promise<T>[]): Promise<T[]>
64
+
65
+ /**
66
+ * Settles all promises (both fulfilled and rejected) in parallel.
67
+ * Wrapper around Promise.allSettled for consistency with other utility methods.
68
+ *
69
+ * @param promises - Array of promises to settle
70
+ * @returns Results of all settled promises with status and value/reason
71
+ */
72
+ static settleAll<T>(promises: Promise<T>[]): Promise<PromiseSettledResult<T>[]>
73
+
74
+ /**
75
+ * Returns the first promise to resolve or reject from an array of promises.
76
+ * Wrapper around Promise.race for consistency with other utility methods.
77
+ *
78
+ * @param promises - Array of promises to race
79
+ * @returns Result of the first settled promise
80
+ */
81
+ static race<T>(promises: Promise<T>[]): Promise<T>
82
+ }
83
+
84
+ export default Util
@@ -4,10 +4,12 @@ export { default as DirectoryObject } from './DirectoryObject.js'
4
4
  export { default as File } from './File.js'
5
5
 
6
6
  // Utility classes
7
+ export { default as Cache } from './Cache.js'
7
8
  export { default as Data } from './Data.js'
8
9
  export { default as Sass } from './Sass.js'
9
10
  export { default as Term } from './Term.js'
10
11
  export { default as Type } from './Type.js'
12
+ export { default as Util } from './Util.js'
11
13
  export { default as Valid } from './Valid.js'
12
14
 
13
15
  // Type exports