@gesslar/toolkit 0.3.0 → 0.4.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.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Get in, bitches, we're going toolkitting.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -26,7 +26,7 @@
26
26
  "update": "npx npm-check-updates -u && npm install",
27
27
  "test": "node --test tests/unit/*.test.js",
28
28
  "test:unit": "node --test tests/unit/*.test.js",
29
- "pr": "gt submit --cli --publish --restack --ai --merge-when-ready"
29
+ "pr": "gt submit --publish --restack --ai"
30
30
  },
31
31
  "repository": {
32
32
  "type": "git",
@@ -47,7 +47,7 @@ export default class BaseActionManager {
47
47
  }
48
48
 
49
49
  set hookManager(hookManager) {
50
- if (this.hookManager)
50
+ if(this.hookManager)
51
51
  throw new Error("Hook manager already set")
52
52
 
53
53
  this.#hookManager = hookManager
@@ -81,7 +81,7 @@ export default class BaseActionManager {
81
81
  /**
82
82
  * Initialize the action manager with the provided definition.
83
83
  * Override in subclasses to add specific validation or setup.
84
- *
84
+ *
85
85
  * @param {object} actionDefinition - Action definition
86
86
  * @protected
87
87
  */
@@ -92,10 +92,10 @@ export default class BaseActionManager {
92
92
 
93
93
  const {action, file, contract} = actionDefinition
94
94
 
95
- if (!action)
95
+ if(!action)
96
96
  throw new Error("Action is required")
97
97
 
98
- if (!contract)
98
+ if(!contract)
99
99
  throw new Error("Contract is required")
100
100
 
101
101
  this.#action = action
@@ -108,7 +108,7 @@ export default class BaseActionManager {
108
108
  /**
109
109
  * Attach hooks to the action instance.
110
110
  * Override in subclasses to customize hook attachment.
111
- *
111
+ *
112
112
  * @param {object} hookManager - Hook manager instance
113
113
  * @protected
114
114
  */
@@ -121,7 +121,7 @@ export default class BaseActionManager {
121
121
  /**
122
122
  * Setup the action by creating and configuring the runner.
123
123
  * Override setupActionInstance() in subclasses for custom setup logic.
124
- *
124
+ *
125
125
  * @returns {Promise<void>}
126
126
  */
127
127
  async setupAction() {
@@ -134,7 +134,7 @@ export default class BaseActionManager {
134
134
  /**
135
135
  * Setup the action instance and create the runner.
136
136
  * Override in subclasses to customize action setup.
137
- *
137
+ *
138
138
  * @protected
139
139
  */
140
140
  async #setupActionInstance() {
@@ -142,7 +142,7 @@ export default class BaseActionManager {
142
142
  const setup = actionInstance?.setup
143
143
 
144
144
  // Setup is required for actions.
145
- if (Data.typeOf(setup) === "Function") {
145
+ if(Data.typeOf(setup) === "Function") {
146
146
  const builder = new ActionBuilder(actionInstance)
147
147
  const configuredBuilder = setup(builder)
148
148
  const buildResult = configuredBuilder.build()
@@ -160,12 +160,12 @@ export default class BaseActionManager {
160
160
 
161
161
  /**
162
162
  * Run the action with the provided input.
163
- *
163
+ *
164
164
  * @param {unknown} result - Input to pass to the action
165
165
  * @returns {Promise<unknown>} Action result
166
166
  */
167
167
  async runAction(result) {
168
- if (!this.#runner)
168
+ if(!this.#runner)
169
169
  throw new Error("Action not set up. Call setupAction() first.")
170
170
 
171
171
  return await this.#runner.run(result)
@@ -173,7 +173,7 @@ export default class BaseActionManager {
173
173
 
174
174
  /**
175
175
  * Cleanup the action and hooks.
176
- *
176
+ *
177
177
  * @returns {Promise<void>}
178
178
  */
179
179
  async cleanupAction() {
@@ -186,7 +186,7 @@ export default class BaseActionManager {
186
186
  /**
187
187
  * Setup hooks if hook manager is present.
188
188
  * Override in subclasses to customize hook setup.
189
- *
189
+ *
190
190
  * @protected
191
191
  */
192
192
  async #setupHooks() {
@@ -195,10 +195,10 @@ export default class BaseActionManager {
195
195
  const type = Data.typeOf(setup)
196
196
 
197
197
  // No hooks attached.
198
- if (type === "Null" || type === "Undefined")
198
+ if(type === "Null" || type === "Undefined")
199
199
  return
200
200
 
201
- if (type !== "Function")
201
+ if(type !== "Function")
202
202
  throw Sass.new("Hook setup must be a function.")
203
203
 
204
204
  await setup.call(
@@ -213,13 +213,13 @@ export default class BaseActionManager {
213
213
  /**
214
214
  * Cleanup hooks if hook manager is present.
215
215
  * Override in subclasses to customize hook cleanup.
216
- *
216
+ *
217
217
  * @protected
218
218
  */
219
219
  async #cleanupHooks() {
220
220
  const cleanup = this.hookManager?.cleanup
221
221
 
222
- if (!cleanup)
222
+ if(!cleanup)
223
223
  return
224
224
 
225
225
  await cleanup.call(this.hookManager.hooks)
@@ -228,13 +228,13 @@ export default class BaseActionManager {
228
228
  /**
229
229
  * Cleanup the action instance.
230
230
  * Override in subclasses to add custom cleanup logic.
231
- *
231
+ *
232
232
  * @protected
233
233
  */
234
234
  async #cleanupActionInstance() {
235
235
  const cleanup = this.action?.cleanup
236
236
 
237
- if (!cleanup)
237
+ if(!cleanup)
238
238
  return
239
239
 
240
240
  await cleanup.call(this.action)
@@ -243,4 +243,4 @@ export default class BaseActionManager {
243
243
  toString() {
244
244
  return `${this.#file?.module || "UNDEFINED"} (${this.meta?.action || "UNDEFINED"})`
245
245
  }
246
- }
246
+ }
@@ -1,4 +1,4 @@
1
- import { setTimeout as timeoutPromise } from "timers/promises"
1
+ import {setTimeout as timeoutPromise} from "timers/promises"
2
2
  import Collection from "./Collection.js"
3
3
  import Data from "./Data.js"
4
4
  import Sass from "./Sass.js"
@@ -22,8 +22,8 @@ export default class BaseHookManager {
22
22
  * @param {string|object} config.action - Action identifier or instance
23
23
  * @param {object} config.hooksFile - File object containing hooks
24
24
  * @param {object} config.logger - Logger instance
25
- * @param {number} [config.timeOut=1000] - Hook execution timeout in milliseconds
26
- * @param {string[]} [config.allowedEvents=[]] - Array of allowed event types for validation
25
+ * @param {number} [config.timeOut] - Hook execution timeout in milliseconds
26
+ * @param {string[]} [config.allowedEvents] - Array of allowed event types for validation
27
27
  */
28
28
  constructor({action, hooksFile, logger, timeOut = 1000, allowedEvents = []}) {
29
29
  this.#action = action
@@ -68,7 +68,7 @@ export default class BaseHookManager {
68
68
  /**
69
69
  * Static factory method to create and initialize a hook manager.
70
70
  * Override loadHooks() in subclasses to customize hook loading logic.
71
- *
71
+ *
72
72
  * @param {object} config - Same as constructor config
73
73
  * @returns {Promise<BaseHookManager|null>} Initialized hook manager or null if no hooks found
74
74
  */
@@ -83,22 +83,23 @@ export default class BaseHookManager {
83
83
  debug("Loading hooks from `%s`", 2, hooksFile.uri)
84
84
 
85
85
  debug("Checking hooks file exists: %j", 2, hooksFile)
86
-
86
+
87
87
  try {
88
88
  const hooksFileContent = await import(hooksFile.uri)
89
+
89
90
  debug("Hooks file loaded successfully", 2)
90
91
 
91
- if (!hooksFileContent)
92
+ if(!hooksFileContent)
92
93
  throw new Error(`Hooks file is empty: ${hooksFile.uri}`)
93
94
 
94
95
  const hooks = await instance.loadHooks(hooksFileContent)
95
-
96
- if (Data.isEmpty(hooks))
96
+
97
+ if(Data.isEmpty(hooks))
97
98
  return null
98
99
 
99
100
  debug("Hooks found for action: `%s`", 2, instance.action)
100
101
 
101
- if (!hooks)
102
+ if(!hooks)
102
103
  return null
103
104
 
104
105
  // Attach common properties to hooks
@@ -109,8 +110,9 @@ export default class BaseHookManager {
109
110
  debug("Hooks loaded successfully for `%s`", 2, instance.action)
110
111
 
111
112
  return instance
112
- } catch (error) {
113
+ } catch(error) {
113
114
  debug("Failed to load hooks: %s", 1, error.message)
115
+
114
116
  return null
115
117
  }
116
118
  }
@@ -118,7 +120,7 @@ export default class BaseHookManager {
118
120
  /**
119
121
  * Load hooks from the imported hooks file content.
120
122
  * Override in subclasses to customize hook loading logic.
121
- *
123
+ *
122
124
  * @param {object} hooksFileContent - Imported hooks file content
123
125
  * @returns {Promise<object|null>} Loaded hooks object or null if no hooks found
124
126
  * @protected
@@ -126,7 +128,7 @@ export default class BaseHookManager {
126
128
  async loadHooks(hooksFileContent) {
127
129
  const hooks = hooksFileContent.default || hooksFileContent.Hooks
128
130
 
129
- if (!hooks)
131
+ if(!hooks)
130
132
  throw new Error(`\`${this.hooksFile.uri}\` contains no hooks.`)
131
133
 
132
134
  // Default implementation: look for hooks by action name
@@ -137,7 +139,7 @@ export default class BaseHookManager {
137
139
 
138
140
  /**
139
141
  * Trigger a hook by event name.
140
- *
142
+ *
141
143
  * @param {string} event - The type of hook to trigger
142
144
  * @param {object} args - The hook arguments as an object
143
145
  * @returns {Promise<unknown>} The result of the hook
@@ -147,21 +149,21 @@ export default class BaseHookManager {
147
149
 
148
150
  debug("Triggering hook for event `%s`", 4, event)
149
151
 
150
- if (!event)
152
+ if(!event)
151
153
  throw new Error("Event type is required for hook invocation")
152
154
 
153
155
  // Validate event type if allowed events are configured
154
- if (this.#allowedEvents.length > 0 && !this.#allowedEvents.includes(event))
156
+ if(this.#allowedEvents.length > 0 && !this.#allowedEvents.includes(event))
155
157
  throw new Error(`Invalid event type: ${event}. Allowed events: ${this.#allowedEvents.join(", ")}`)
156
158
 
157
159
  const hook = this.hooks?.[event]
158
160
 
159
- if (hook) {
161
+ if(hook) {
160
162
  Valid.type(hook, "function", `Hook "${event}" is not a function`)
161
163
 
162
164
  const hookExecution = hook.call(this.hooks, args)
163
165
  const hookTimeout = this.timeout
164
-
166
+
165
167
  const expireAsync = () =>
166
168
  timeoutPromise(
167
169
  hookTimeout,
@@ -170,7 +172,7 @@ export default class BaseHookManager {
170
172
 
171
173
  const result = await Promise.race([hookExecution, expireAsync()])
172
174
 
173
- if (result?.status === "error")
175
+ if(result?.status === "error")
174
176
  throw Sass.new(result.error)
175
177
 
176
178
  debug("Hook executed successfully for event: `%s`", 4, event)
@@ -178,13 +180,14 @@ export default class BaseHookManager {
178
180
  return result
179
181
  } else {
180
182
  debug("No hook found for event: `%s`", 4, event)
183
+
181
184
  return null
182
185
  }
183
186
  }
184
187
 
185
188
  /**
186
189
  * Check if a hook exists for the given event.
187
- *
190
+ *
188
191
  * @param {string} event - Event name to check
189
192
  * @returns {boolean} True if hook exists
190
193
  */
@@ -194,13 +197,13 @@ export default class BaseHookManager {
194
197
 
195
198
  /**
196
199
  * Get all available hook events.
197
- *
200
+ *
198
201
  * @returns {string[]} Array of available hook event names
199
202
  */
200
203
  getAvailableEvents() {
201
- return this.hooks ? Object.keys(this.hooks).filter(key =>
202
- typeof this.hooks[key] === 'function' &&
203
- !['setup', 'cleanup', 'log', 'timeout'].includes(key)
204
+ return this.hooks ? Object.keys(this.hooks).filter(key =>
205
+ typeof this.hooks[key] === "function" &&
206
+ !["setup", "cleanup", "log", "timeout"].includes(key)
204
207
  ) : []
205
208
  }
206
- }
209
+ }
@@ -48,6 +48,8 @@ export default class DirectoryObject extends FS {
48
48
  extension: null,
49
49
  isFile: false,
50
50
  isDirectory: true,
51
+ trail: null,
52
+ sep: null,
51
53
  })
52
54
 
53
55
  /**
@@ -63,6 +65,8 @@ export default class DirectoryObject extends FS {
63
65
  const fileUri = FS.pathToUri(absolutePath)
64
66
  const filePath = FS.uriToPath(fileUri)
65
67
  const baseName = path.basename(absolutePath) || "."
68
+ const trail = filePath.split(path.sep)
69
+ const sep = path.sep
66
70
 
67
71
  this.#meta.supplied = fixedDir
68
72
  this.#meta.path = filePath
@@ -70,6 +74,8 @@ export default class DirectoryObject extends FS {
70
74
  this.#meta.name = baseName
71
75
  this.#meta.extension = ""
72
76
  this.#meta.module = baseName
77
+ this.#meta.trail = trail
78
+ this.#meta.sep = sep
73
79
 
74
80
  Object.freeze(this.#meta)
75
81
  }
@@ -173,6 +179,27 @@ export default class DirectoryObject extends FS {
173
179
  return this.#meta.extension
174
180
  }
175
181
 
182
+ /**
183
+ * Returns the platform-specific path separator.
184
+ *
185
+ * @returns {string} The path separator ('/' on Unix, '\\' on Windows)
186
+ */
187
+ get sep() {
188
+ return this.#meta.sep
189
+ }
190
+
191
+ /**
192
+ * Returns the directory path split into segments.
193
+ *
194
+ * @returns {string[]} Array of path segments
195
+ * @example
196
+ * const dir = new DirectoryObject('/path/to/directory')
197
+ * console.log(dir.trail) // ['', 'path', 'to', 'directory']
198
+ */
199
+ get trail() {
200
+ return this.#meta.trail
201
+ }
202
+
176
203
  /**
177
204
  * Returns false. Because this is a directory.
178
205
  *
@@ -217,33 +244,29 @@ export default class DirectoryObject extends FS {
217
244
  {withFileTypes: true}
218
245
  )
219
246
 
220
- const results = await Promise.all(
221
- found.map(async dirent => {
222
- const fullPath = path.join(this.path, dirent.name)
223
- const stat = await fs.stat(fullPath)
224
-
225
- return {dirent, stat, fullPath}
226
- }),
227
- )
228
-
229
- const files = results
230
- .filter(({stat}) => stat.isFile())
231
- .map(({fullPath}) => new FileObject(fullPath))
247
+ const files = found
248
+ .filter(dirent => dirent.isFile())
249
+ .map(dirent => new FileObject(path.join(this.path, dirent.name)))
232
250
 
233
- const directories = results
234
- .filter(({stat}) => stat.isDirectory())
235
- .map(({fullPath}) => new DirectoryObject(fullPath))
251
+ const directories = found
252
+ .filter(dirent => dirent.isDirectory())
253
+ .map(dirent => new DirectoryObject(path.join(this.path, dirent.name)))
236
254
 
237
255
  return {files, directories}
238
256
  }
239
257
 
240
258
  /**
241
- * Ensures a directory exists, creating it if necessary
259
+ * Ensures a directory exists, creating it if necessary.
260
+ * Gracefully handles the case where the directory already exists.
242
261
  *
243
262
  * @async
244
- * @param {object} [options] - Any options to pass to mkdir
263
+ * @param {object} [options] - Options to pass to fs.mkdir (e.g., {recursive: true, mode: 0o755})
245
264
  * @returns {Promise<void>}
246
- * @throws {Sass} If directory creation fails
265
+ * @throws {Sass} If directory creation fails for reasons other than already existing
266
+ * @example
267
+ * // Create directory recursively
268
+ * const dir = new DirectoryObject('./build/output')
269
+ * await dir.assureExists({recursive: true})
247
270
  */
248
271
  async assureExists(options = {}) {
249
272
  if(await this.exists)
@@ -252,7 +275,60 @@ export default class DirectoryObject extends FS {
252
275
  try {
253
276
  await fs.mkdir(this.path, options)
254
277
  } catch(e) {
278
+ if(e.code === "EEXIST") {
279
+ // Directory already exists, ignore
280
+ return
281
+ }
282
+
255
283
  throw Sass.new(`Unable to create directory '${this.path}': ${e.message}`)
256
284
  }
257
285
  }
286
+
287
+ /**
288
+ * Private generator that walks up the directory tree.
289
+ *
290
+ * @private
291
+ * @generator
292
+ * @yields {DirectoryObject} Parent directory objects from current to root
293
+ */
294
+ *#walkUp() {
295
+ if(!Array.isArray(this.trail))
296
+ return
297
+
298
+ const curr = structuredClone(this.trail)
299
+
300
+ while(curr.length > 0) {
301
+ const joined = curr.join(this.sep)
302
+
303
+ // Stop if we've reached an empty path (which would resolve to CWD)
304
+ if(joined === "" || joined === this.sep) {
305
+ // Yield the root and stop
306
+ yield new DirectoryObject(this.sep)
307
+ break
308
+ }
309
+
310
+ yield new DirectoryObject(joined)
311
+ curr.pop()
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Generator that walks up the directory tree, yielding each parent directory.
317
+ * Starts from the current directory and yields each parent until reaching the root.
318
+ *
319
+ * @returns {object} Generator yielding parent DirectoryObject instances
320
+ * @example
321
+ * const dir = new DirectoryObject('/path/to/deep/directory')
322
+ * for(const parent of dir.walkUp) {
323
+ * console.log(parent.path)
324
+ * // /path/to/deep/directory
325
+ * // /path/to/deep
326
+ * // /path/to
327
+ * // /path
328
+ * // /
329
+ * }
330
+ */
331
+ get walkUp() {
332
+ return this.#walkUp()
333
+ }
258
334
  }
@@ -10,6 +10,7 @@ import path from "node:path"
10
10
  import util from "node:util"
11
11
  import YAML from "yaml"
12
12
 
13
+ import Data from "./Data.js"
13
14
  import DirectoryObject from "./DirectoryObject.js"
14
15
  import FS from "./FS.js"
15
16
  import Sass from "./Sass.js"
@@ -87,13 +88,18 @@ export default class FileObject extends FS {
87
88
 
88
89
  const {dir,base,ext} = this.#deconstructFilenameToParts(fixedFile)
89
90
 
90
- if(!directory) {
91
- directory = new DirectoryObject(dir)
92
- } else if(typeof directory === "string") {
93
- directory = new DirectoryObject(directory)
94
- }
91
+ const directoryObject = (() => {
92
+ switch(Data.typeOf(directory)) {
93
+ case "String":
94
+ return new DirectoryObject(directory)
95
+ case "DirectoryObject":
96
+ return directory
97
+ default:
98
+ return new DirectoryObject(dir)
99
+ }
100
+ })()
95
101
 
96
- const final = FS.resolvePath(directory?.path ?? ".", fixedFile)
102
+ const final = FS.resolvePath(directoryObject.path ?? ".", fixedFile)
97
103
 
98
104
  const resolved = final
99
105
  const fileUri = FS.pathToUri(resolved)
@@ -104,10 +110,7 @@ export default class FileObject extends FS {
104
110
  this.#meta.name = base
105
111
  this.#meta.extension = ext
106
112
  this.#meta.module = path.basename(this.supplied, this.extension)
107
-
108
- const {dir: newDir} = this.#deconstructFilenameToParts(this.path)
109
-
110
- this.#meta.directory = new DirectoryObject(newDir)
113
+ this.#meta.directory = directoryObject
111
114
 
112
115
  Object.freeze(this.#meta)
113
116
  }
@@ -363,29 +366,46 @@ export default class FileObject extends FS {
363
366
  }
364
367
 
365
368
  /**
366
- * Writes content to a file synchronously.
369
+ * Writes content to a file asynchronously.
370
+ * Validates that the parent directory exists before writing.
367
371
  *
368
372
  * @param {string} content - The content to write
369
- * @param {string} encoding - The encoding in which to write.
373
+ * @param {string} [encoding] - The encoding in which to write (default: "utf8")
370
374
  * @returns {Promise<void>}
375
+ * @throws {Sass} If the file path is invalid or the parent directory doesn't exist
376
+ * @example
377
+ * const file = new FileObject('./output/data.json')
378
+ * await file.write(JSON.stringify({key: 'value'}))
371
379
  */
372
380
  async write(content, encoding="utf8") {
373
381
  if(!this.path)
374
382
  throw Sass.new("No absolute path in file")
375
383
 
376
- await fs.writeFile(this.path, content, encoding)
384
+ if(await this.directory.exists)
385
+ await fs.writeFile(this.path, content, encoding)
386
+
387
+ else
388
+ throw Sass.new(`Invalid directory, ${this.directory.uri}`)
377
389
  }
378
390
 
379
391
  /**
380
- * Loads an object from JSON or YAML provided a fileMap
392
+ * Loads an object from JSON or YAML file.
393
+ * Attempts to parse content as JSON5 first, then falls back to YAML if specified.
381
394
  *
382
- * @param {string} [type] - The expected type of data to parse.
383
- * @param {string} [encoding] - The encoding to read the file as.
384
- * @returns {Promise<unknown>} The parsed data object.
395
+ * @param {string} [type] - The expected type of data to parse ("json", "json5", "yaml", or "any")
396
+ * @param {string} [encoding] - The encoding to read the file as (default: "utf8")
397
+ * @returns {Promise<unknown>} The parsed data object
398
+ * @throws {Sass} If the content cannot be parsed or type is unsupported
399
+ * @example
400
+ * const configFile = new FileObject('./config.json5')
401
+ * const config = await configFile.loadData('json5')
402
+ *
403
+ * // Auto-detect format
404
+ * const data = await configFile.loadData('any')
385
405
  */
386
406
  async loadData(type="any", encoding="utf8") {
387
407
  const content = await this.read(encoding)
388
- const normalizedType = type.toLocaleLowerCase()
408
+ const normalizedType = type.toLowerCase()
389
409
  const toTry = {
390
410
  json5: [JSON5],
391
411
  json: [JSON5],
@@ -394,7 +414,7 @@ export default class FileObject extends FS {
394
414
  }[normalizedType]
395
415
 
396
416
  if(!toTry) {
397
- throw Sass.new(`Unsupported data type '${type}'. Supported types: json, json5, yaml, any`)
417
+ throw Sass.new(`Unsupported data type '${type}'. Supported types: json, json5, yaml.`)
398
418
  }
399
419
 
400
420
  for(const format of toTry) {
@@ -40,6 +40,12 @@ export default class DirectoryObject extends FS {
40
40
  /** The directory extension (usually empty) */
41
41
  readonly extension: string
42
42
 
43
+ /** The platform-specific path separator (e.g., '/' on Unix, '\\' on Windows) */
44
+ readonly sep: string
45
+
46
+ /** Array of directory path segments split by separator */
47
+ readonly trail: string[]
48
+
43
49
  /** Always false for directories */
44
50
  readonly isFile: false
45
51
 
@@ -49,6 +55,25 @@ export default class DirectoryObject extends FS {
49
55
  /** Whether the directory exists (async) */
50
56
  readonly exists: Promise<boolean>
51
57
 
58
+ /**
59
+ * Generator that walks up the directory tree, yielding parent directories.
60
+ * Starts from the current directory and yields each parent until reaching the root.
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * const dir = new DirectoryObject('/path/to/deep/directory')
65
+ * for (const parent of dir.walkUp) {
66
+ * console.log(parent.path)
67
+ * // /path/to/deep/directory
68
+ * // /path/to/deep
69
+ * // /path/to
70
+ * // /path
71
+ * // /
72
+ * }
73
+ * ```
74
+ */
75
+ readonly walkUp: Generator<DirectoryObject, void, unknown>
76
+
52
77
  /** Returns a string representation of the DirectoryObject */
53
78
  toString(): string
54
79
 
@@ -64,9 +89,47 @@ export default class DirectoryObject extends FS {
64
89
  isDirectory: boolean
65
90
  }
66
91
 
67
- /** List the contents of this directory */
92
+ /**
93
+ * Lists the contents of this directory.
94
+ * Returns FileObject instances for files and DirectoryObject instances for subdirectories.
95
+ *
96
+ * @returns Promise resolving to object with files and directories arrays
97
+ * @throws {Error} If directory cannot be read
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * const dir = new DirectoryObject('./src')
102
+ * const {files, directories} = await dir.read()
103
+ *
104
+ * console.log(`Found ${files.length} files`)
105
+ * files.forEach(file => console.log(file.name))
106
+ *
107
+ * console.log(`Found ${directories.length} subdirectories`)
108
+ * directories.forEach(subdir => console.log(subdir.name))
109
+ * ```
110
+ */
68
111
  read(): Promise<DirectoryListing>
69
112
 
70
- /** Ensure this directory exists, creating it if necessary */
113
+ /**
114
+ * Ensures this directory exists, creating it if necessary.
115
+ * Gracefully handles the case where the directory already exists (EEXIST error).
116
+ * Pass options to control directory creation behavior (e.g., recursive, mode).
117
+ *
118
+ * @param options - Options to pass to fs.mkdir (e.g., {recursive: true, mode: 0o755})
119
+ * @returns Promise that resolves when directory exists or has been created
120
+ * @throws {Sass} If directory creation fails for reasons other than already existing
121
+ *
122
+ * @example
123
+ * ```typescript
124
+ * const dir = new DirectoryObject('./build/output')
125
+ *
126
+ * // Create directory recursively
127
+ * await dir.assureExists({recursive: true})
128
+ *
129
+ * // Now safe to write files
130
+ * const file = new FileObject('result.json', dir)
131
+ * await file.write(JSON.stringify(data))
132
+ * ```
133
+ */
71
134
  assureExists(options?: any): Promise<void>
72
135
  }
@@ -321,10 +321,46 @@ export default class FileObject extends FS {
321
321
  /** Read the content of a file */
322
322
  read(encoding?: string): Promise<string>
323
323
 
324
- /** Write content to a file */
324
+ /**
325
+ * Write content to a file asynchronously.
326
+ * Validates that the parent directory exists before writing.
327
+ *
328
+ * @param content - The content to write
329
+ * @param encoding - The encoding in which to write (default: "utf8")
330
+ * @throws {Sass} If the file path is invalid or the parent directory doesn't exist
331
+ *
332
+ * @example
333
+ * ```typescript
334
+ * const file = new FileObject('./output/data.json')
335
+ * await file.write(JSON.stringify({key: 'value'}))
336
+ *
337
+ * // With custom encoding
338
+ * await file.write('content', 'utf16le')
339
+ * ```
340
+ */
325
341
  write(content: string, encoding?: string): Promise<void>
326
342
 
327
- /** Load an object from JSON5 or YAML file with type specification */
343
+ /**
344
+ * Load and parse data from JSON5 or YAML file.
345
+ * Attempts to parse content as JSON5 first, then falls back to YAML if type is "any".
346
+ *
347
+ * @param type - The expected data format: "json", "json5", "yaml", or "any" (default: "any")
348
+ * @param encoding - The file encoding (default: "utf8")
349
+ * @returns The parsed data object
350
+ * @throws {Sass} If the content cannot be parsed or type is unsupported
351
+ *
352
+ * @example
353
+ * ```typescript
354
+ * // Load JSON5 config
355
+ * const config = await configFile.loadData('json5')
356
+ *
357
+ * // Auto-detect format (tries JSON5, then YAML)
358
+ * const data = await dataFile.loadData('any')
359
+ *
360
+ * // Load YAML explicitly
361
+ * const yaml = await yamlFile.loadData('yaml')
362
+ * ```
363
+ */
328
364
  loadData(type?: 'json' | 'json5' | 'yaml' | 'any', encoding?: string): Promise<unknown>
329
365
 
330
366
  /**