@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 +2 -2
- package/src/lib/BaseActionManager.js +19 -19
- package/src/lib/BaseHookManager.js +27 -24
- package/src/lib/DirectoryObject.js +94 -18
- package/src/lib/FileObject.js +39 -19
- package/src/types/DirectoryObject.d.ts +65 -2
- package/src/types/FileObject.d.ts +38 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gesslar/toolkit",
|
|
3
|
-
"version": "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 --
|
|
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
|
|
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
|
|
95
|
+
if(!action)
|
|
96
96
|
throw new Error("Action is required")
|
|
97
97
|
|
|
98
|
-
if
|
|
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
|
|
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
|
|
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
|
|
198
|
+
if(type === "Null" || type === "Undefined")
|
|
199
199
|
return
|
|
200
200
|
|
|
201
|
-
if
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
26
|
-
* @param {string[]} [config.allowedEvents
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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] ===
|
|
203
|
-
![
|
|
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
|
|
221
|
-
|
|
222
|
-
|
|
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 =
|
|
234
|
-
.filter(
|
|
235
|
-
.map(
|
|
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] -
|
|
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
|
}
|
package/src/lib/FileObject.js
CHANGED
|
@@ -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
|
-
|
|
91
|
-
directory
|
|
92
|
-
|
|
93
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
/**
|