@gesslar/bedoc 1.0.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.
@@ -0,0 +1,322 @@
1
+ import fs from "fs"
2
+ import {globby} from "globby"
3
+ import path from "node:path"
4
+ import process from "node:process"
5
+ import {fileURLToPath, pathToFileURL} from "node:url"
6
+
7
+ import * as DataUtil from "./DataUtil.js"
8
+ import * as ValidUtil from "./ValidUtil.js"
9
+
10
+ const {isArrayUniform, isType, allocateObject} = DataUtil
11
+ const {validType} = ValidUtil
12
+
13
+ const freeze = (ob) => Object.freeze(ob)
14
+
15
+ const fdTypes = freeze(["file", "directory"])
16
+ const upperFdTypes = freeze(fdTypes.map((type) => type.toUpperCase()))
17
+ const fdType = freeze(await allocateObject(upperFdTypes, fdTypes))
18
+
19
+ /**
20
+ * Fix slashes in a path
21
+ *
22
+ * @param {string} pathName - The path to fix
23
+ * @returns {string} The fixed path
24
+ */
25
+ function fixSlashes(pathName) {
26
+ return pathName.replace(/\\/g, "/")
27
+ }
28
+
29
+ /**
30
+ * Convert a path to a URI
31
+ *
32
+ * @param {string} pathName - The path to convert
33
+ * @returns {string} The URI
34
+ * @throws {Error} If the path is not a valid file path
35
+ */
36
+ function pathToUri(pathName) {
37
+ try {
38
+ return pathToFileURL(pathName).href
39
+ } catch(e) {
40
+ void e // stfu linter
41
+ return pathName
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Convert a URI to a path
47
+ *
48
+ * @param {string} pathName - The URI to convert
49
+ * @returns {string} The path
50
+ * @throws {Error} If the URI is not a valid file URL
51
+ */
52
+ function uriToPath(pathName) {
53
+ try {
54
+ return fileURLToPath(pathName)
55
+ } catch(e) {
56
+ void e // did you hear me?? i said stfu!
57
+ return pathName
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Resolves a file to an absolute path
63
+ *
64
+ * @param {string} fileName - The file to resolve
65
+ * @param {object} [directoryObject] - The directory object to resolve the
66
+ * file in
67
+ * @returns {object} A file object (validated)
68
+ * @throws {Error}
69
+ */
70
+ function resolveFilename(fileName, directoryObject = null) {
71
+ validType(fileName, "string", {allowEmpty: false}, 1)
72
+
73
+ fileName = uriToPath(fileName)
74
+ const fixedFileName = fixSlashes(fileName)
75
+ const directoryNamePart = fixedFileName.split("/").slice(0, -1).join("/")
76
+ const fileNamePart = fixedFileName.split("/").pop()
77
+ if(!directoryObject)
78
+ directoryObject = resolveDirectory(directoryNamePart)
79
+
80
+ const fileObject = composeFilename(directoryObject, fileNamePart)
81
+ try {
82
+ fs.opendirSync(directoryObject.absolutePath).closeSync()
83
+ } catch(e) {
84
+ void e
85
+ throw new Error(
86
+ `Failed to resolve directory: ${directoryObject.absolutePath}, looking for file: ${fileNamePart}`,
87
+ )
88
+ }
89
+
90
+ return {
91
+ ...fileObject,
92
+ directory: directoryObject,
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Compose a file path from a directory and a file
98
+ *
99
+ * @param {string|object} directoryNameorObject - The directory
100
+ * @param {string} fileName - The file
101
+ * @returns {object} A file object
102
+ */
103
+ function composeFilename(directoryNameorObject, fileName) {
104
+ let dirObject
105
+
106
+ if(isType(directoryNameorObject, "string|string[]")) {
107
+ dirObject = composeDirectory(directoryNameorObject)
108
+ } else {
109
+ if(!directoryNameorObject.isDirectory)
110
+ throw new Error("Directory object is not a directory")
111
+
112
+ dirObject = directoryNameorObject
113
+ }
114
+
115
+ fileName = path.resolve(dirObject.path, fileName)
116
+
117
+ return mapFilename(fileName)
118
+ }
119
+
120
+ /**
121
+ * Map a file to a FileMap
122
+ *
123
+ * @param {string} fileName - The file to map
124
+ * @returns {object} A file object
125
+ */
126
+ function mapFilename(fileName) {
127
+ return {
128
+ path: fileName,
129
+ uri: pathToUri(fileName),
130
+ absolutePath: path.resolve(process.cwd(), fileName),
131
+ absoluteUri: pathToUri(path.resolve(process.cwd(), fileName)),
132
+ name: path.basename(fileName),
133
+ module: path.basename(fileName, path.extname(fileName)),
134
+ extension: path.extname(fileName),
135
+ isFile: true,
136
+ isDirectory: false,
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Map a directory to a DirMap
142
+ *
143
+ * @param {string} directoryName - The directory to map
144
+ * @returns {object} A directory object
145
+ */
146
+ function mapDirectory(directoryName) {
147
+ return {
148
+ path: directoryName,
149
+ uri: pathToUri(directoryName),
150
+ absolutePath: path.resolve(process.cwd(), directoryName),
151
+ absoluteUri: pathToUri(path.resolve(process.cwd(), directoryName)),
152
+ name: path.basename(directoryName),
153
+ separator: path.sep,
154
+ isFile: false,
155
+ isDirectory: true,
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Deconstruct a filename into parts
161
+ *
162
+ * @param {string} fileName - The filename to deconstruct
163
+ * @returns {object} The filename parts
164
+ */
165
+ function deconstructFilenameToParts(fileName) {
166
+ const {basename, dirname, extname} = path.parse(fileName)
167
+
168
+ return {basename, dirname, extname}
169
+ }
170
+
171
+ /**
172
+ * Retrieve all files matching a specific glob pattern.
173
+ *
174
+ * @param {string|string[]} globPattern - The glob pattern(s) to search.
175
+ * @returns {Promise<object[]>} An array of file objects
176
+ * @throws {Error} Throws an error for invalid input or search failure.
177
+ */
178
+ async function getFiles(globPattern) {
179
+ // Validate input
180
+ validType(globPattern, "string|string[]", {allowEmpty: false})
181
+
182
+ const globbyArray = (
183
+ isType(globPattern, "array")
184
+ ? globPattern
185
+ : globPattern
186
+ .split("|")
187
+ .map((g) => g.trim())
188
+ .filter(Boolean)
189
+ ).map((g) => fixSlashes(g))
190
+
191
+ if(
192
+ Array.isArray(globbyArray) &&
193
+ isArrayUniform(globbyArray, "string", true) &&
194
+ !globbyArray.length
195
+ )
196
+ throw new Error(
197
+ "[getFiles] Invalid glob pattern: Array must contain only strings.",
198
+ )
199
+
200
+ // Use Globby to fetch matching files
201
+ const filesArray = await globby(globbyArray)
202
+ const files = filesArray.map((file) => mapFilename(file))
203
+
204
+ // Flatten the result and remove duplicates
205
+ return files
206
+ }
207
+
208
+ /**
209
+ * Resolves a path to an absolute path
210
+ *
211
+ * @param {string} directoryName - The path to resolve
212
+ * @returns {object} The directory object
213
+ * @throws {Error}
214
+ */
215
+ function resolveDirectory(directoryName) {
216
+ validType(directoryName, "string", true)
217
+
218
+ const directoryObject = mapDirectory(directoryName)
219
+
220
+ try {
221
+ fs.opendirSync(directoryObject.absolutePath).closeSync()
222
+ } catch(e) {
223
+ throw new Error(
224
+ `Failed to resolve directory: ${directoryObject.absolutePath}, looking for file: ${directoryName}\n${e.message}`,
225
+ )
226
+ }
227
+
228
+ fs.opendirSync(directoryObject.absolutePath).closeSync()
229
+
230
+ return directoryObject
231
+ }
232
+
233
+ /**
234
+ * Compose a directory map from a path
235
+ *
236
+ * @param {string} directory - The directory
237
+ * @returns {object} A directory object
238
+ */
239
+ function composeDirectory(directory) {
240
+ return mapDirectory(directory)
241
+ }
242
+
243
+ /**
244
+ * Lists the contents of a directory.
245
+ *
246
+ * @param {string} directory - The directory to list.
247
+ * @returns {Promise<{files: object[], directories: object[]}>} The files and
248
+ * directories in the directory.
249
+ */
250
+ async function ls(directory) {
251
+ const found = await fs.promises.readdir(directory, {withFileTypes: true})
252
+ const results = await Promise.all(
253
+ found.map(async(dirent) => {
254
+ const fullPath = path.join(directory, dirent.name)
255
+ const stat = await fs.promises.stat(fullPath)
256
+ return {dirent, stat, fullPath}
257
+ }),
258
+ )
259
+
260
+ const files = results
261
+ .filter(({stat}) => stat.isFile())
262
+ .map(({fullPath}) => mapFilename(fullPath))
263
+
264
+ const directories = results
265
+ .filter(({stat}) => stat.isDirectory())
266
+ .map(({fullPath}) => mapDirectory(fullPath))
267
+
268
+ return {files, directories}
269
+ }
270
+
271
+ /**
272
+ * Reads the content of a file synchronously.
273
+ *
274
+ * @param {object} fileObject - The file map containing the file path
275
+ * @returns {Promise<string>} The file contents
276
+ */
277
+ function readFile(fileObject) {
278
+ const {absolutePath} = fileObject
279
+
280
+ if(!absolutePath)
281
+ throw new Error("No absolute path in file map")
282
+
283
+ const content = fs.readFileSync(absolutePath, "utf8")
284
+
285
+ return content
286
+ }
287
+
288
+ /**
289
+ * Writes content to a file synchronously.
290
+ *
291
+ * @param {object} fileObject - The file map containing the file path
292
+ * @param {string} content - The content to write
293
+ */
294
+ function writeFile(fileObject, content) {
295
+ const absolutePath = fileObject.absolutePath
296
+
297
+ if(!absolutePath)
298
+ throw new Error("No absolute path in file map")
299
+
300
+ fs.writeFileSync(absolutePath, content, "utf8")
301
+ }
302
+
303
+ export {
304
+ // Constants
305
+ fdType,
306
+ fdTypes,
307
+ // Functions
308
+ composeDirectory,
309
+ composeFilename,
310
+ deconstructFilenameToParts,
311
+ fixSlashes,
312
+ getFiles,
313
+ ls,
314
+ mapDirectory,
315
+ mapFilename,
316
+ pathToUri,
317
+ readFile,
318
+ resolveDirectory,
319
+ resolveFilename,
320
+ uriToPath,
321
+ writeFile,
322
+ }
@@ -0,0 +1,39 @@
1
+ import {createRequire} from "module"
2
+ import FDUtil from "./FDUtil.js"
3
+
4
+ export default class ModuleUtil {
5
+ /**
6
+ * Requires a module synchronously
7
+ *
8
+ * @param {object} fileObject - The file to require
9
+ * @returns {object} The required module
10
+ */
11
+ static require(fileObject) {
12
+ return createRequire(import.meta.url)(fileObject.absolutePath)
13
+ }
14
+
15
+ /**
16
+ * Loads a JSON file asynchronously
17
+ *
18
+ * @param {object} jsonFileObject - The JSON file to load
19
+ * @returns {Promise<object>} The parsed JSON content
20
+ */
21
+ static async loadJson(jsonFileObject) {
22
+ // Read the file
23
+ const jsonContent = await FDUtil.readFile(jsonFileObject)
24
+ const json = JSON.parse(jsonContent)
25
+ return json
26
+ }
27
+
28
+ /**
29
+ * Loads the package.json file asynchronously
30
+ *
31
+ * @returns {Promise<object>} The parsed package.json content
32
+ */
33
+ static async loadPackageJson() {
34
+ const packageJsonFileObject = FDUtil.resolveFilename("./package.json")
35
+ const jsonContent = await FDUtil.readFile(packageJsonFileObject)
36
+ const json = JSON.parse(jsonContent)
37
+ return json
38
+ }
39
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Capitalizes the first letter of a string
3
+ *
4
+ * @param {string} str - The string to capitalize
5
+ * @returns {string} The capitalized string
6
+ */
7
+ function capitalize(str) {
8
+ return `${str.charAt(0).toUpperCase()}${str.slice(1)}`
9
+ }
10
+
11
+ export {capitalize}
@@ -0,0 +1,114 @@
1
+ import * as DataUtil from "./DataUtil.js"
2
+
3
+ const {isEmpty, typeOf, isArrayUniform, isValidType} = DataUtil
4
+
5
+ export default class TypeSpec {
6
+ #specs
7
+
8
+ constructor(string, options) {
9
+ this.#specs = []
10
+ this.#parse(string, options)
11
+ Object.freeze(this.#specs)
12
+ this.specs = this.#specs
13
+ this.length = this.#specs.length
14
+ this.stringRepresentation = this.toString()
15
+ Object.freeze(this)
16
+ }
17
+
18
+ toString() {
19
+ return this.#specs
20
+ .map((spec) => {
21
+ return `${spec.typeName}${spec.array ? "[]" : ""}`
22
+ })
23
+ .join("|")
24
+ }
25
+
26
+ toJSON() {
27
+ // Serialize as a string representation or as raw data
28
+ return {
29
+ specs: this.#specs,
30
+ length: this.length,
31
+ stringRepresentation: this.toString(),
32
+ }
33
+ }
34
+
35
+ forEach(callback) {
36
+ this.#specs.forEach(callback)
37
+ }
38
+ every(callback) {
39
+ return this.#specs.every(callback)
40
+ }
41
+ some(callback) {
42
+ return this.#specs.some(callback)
43
+ }
44
+ filter(callback) {
45
+ return this.#specs.filter(callback)
46
+ }
47
+ map(callback) {
48
+ return this.#specs.map(callback)
49
+ }
50
+ reduce(callback, initialValue) {
51
+ return this.#specs.reduce(callback, initialValue)
52
+ }
53
+ find(callback) {
54
+ return this.#specs.find(callback)
55
+ }
56
+
57
+ match(value, options) {
58
+ const allowEmpty = options?.allowEmpty ?? true
59
+ const empty = isEmpty(value)
60
+
61
+ // If we have a list of types, because the string was validly parsed,
62
+ // we need to ensure that all of the types that were parsed are valid types
63
+ // in JavaScript.
64
+ if(this.length && !this.every((t) => isValidType(t.typeName)))
65
+ return false
66
+
67
+ // Now, let's do some checking with the types, respecting the array flag
68
+ // with the value
69
+ const valueType = typeOf(value)
70
+ const isArray = valueType === "array"
71
+
72
+ // We need to ensure that we match the type and the consistency of the types
73
+ // in an array, if it is an array and an array is allowed.
74
+ const matchingTypeSpec = this.filter((spec) => {
75
+ const {typeName: allowedType, array: allowedArray} = spec
76
+
77
+ if(valueType === allowedType && !isArray && !allowedArray)
78
+ return !allowEmpty ? !empty : true
79
+
80
+ if(isArray) {
81
+ if(allowedType === "array")
82
+ if(!allowedArray)
83
+ return true
84
+
85
+ if(empty)
86
+ if(allowEmpty)
87
+ return true
88
+
89
+ return isArrayUniform(value, allowedType)
90
+ }
91
+ })
92
+
93
+ return matchingTypeSpec.length > 0
94
+ }
95
+
96
+ #parse(string, options) {
97
+ const delimiter = options?.delimiter ?? "|"
98
+ const parts = string.split(delimiter)
99
+
100
+ this.#specs = parts.map((part) => {
101
+ const typeMatches = /(\w+)(\[\])?/.exec(part)
102
+ if(!typeMatches || typeMatches.length !== 3)
103
+ throw new TypeError(`Invalid type: ${part}`)
104
+
105
+ if(!isValidType(typeMatches[1]))
106
+ throw new TypeError(`Invalid type: ${typeMatches[1]}`)
107
+
108
+ return {
109
+ typeName: typeMatches[1],
110
+ array: typeMatches[2] === "[]",
111
+ }
112
+ })
113
+ }
114
+ }
@@ -0,0 +1,50 @@
1
+ import _assert from "node:assert/strict"
2
+
3
+ import * as DataUtil from "./DataUtil.js"
4
+
5
+ const {isType} = DataUtil
6
+
7
+ /**
8
+ * Validates a value against a type
9
+ *
10
+ * @param {*} value - The value to validate
11
+ * @param {string} type - The expected type in the form of "object",
12
+ * "object[]", "object|object[]"
13
+ * @param {object} [options] - Additional options for validation.
14
+ */
15
+ function validType(value, type, options) {
16
+ assert(
17
+ isType(value, type, options),
18
+ `Invalid type. Expected ${type}, got ${JSON.stringify(value)}`,
19
+ 1,
20
+ )
21
+ }
22
+
23
+ /**
24
+ * Asserts a condition
25
+ *
26
+ * @param {boolean} condition - The condition to assert
27
+ * @param {string} message - The message to display if the condition is not
28
+ * met
29
+ * @param {number} [arg] - The argument to display if the condition is not
30
+ * met (optional)
31
+ */
32
+ function assert(condition, message, arg = null) {
33
+ _assert(
34
+ isType(condition, "boolean"),
35
+ `Condition must be a boolean, got ${condition}`,
36
+ )
37
+ _assert(
38
+ isType(message, "string"),
39
+ `Message must be a string, got ${message}`,
40
+ )
41
+ _assert(
42
+ arg !== null && isType(arg, "number"),
43
+ `Arg must be a number, got ${arg}`,
44
+ )
45
+
46
+ if(!condition)
47
+ throw new Error(`${message}${arg ? `: ${arg}` : ""}`)
48
+ }
49
+
50
+ export {assert, validType}