@gesslar/toolkit 0.0.1

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,226 @@
1
+ /**
2
+ * @file FileObject.js
3
+ * @description Class representing a file and its metadata, including path
4
+ * resolution and existence checks.
5
+ */
6
+
7
+ import path from "node:path"
8
+ import util from "node:util"
9
+
10
+ import DirectoryObject from "./DirectoryObject.js"
11
+ import File from "./File.js"
12
+
13
+ /**
14
+ * FileObject encapsulates metadata and operations for a file, including path
15
+ * resolution and existence checks.
16
+ *
17
+ * @property {string} supplied - User-supplied path
18
+ * @property {string} path - The absolute file path
19
+ * @property {string} uri - The file URI
20
+ * @property {string} name - The file name
21
+ * @property {string} module - The file name without extension
22
+ * @property {string} extension - The file extension
23
+ * @property {boolean} isFile - Always true for files
24
+ * @property {boolean} isDirectory - Always false for files
25
+ * @property {DirectoryObject} directory - The parent directory object
26
+ * @property {Promise<boolean>} exists - Whether the file exists (async)
27
+ */
28
+
29
+ export default class FileObject {
30
+ /**
31
+ * @type {object}
32
+ * @private
33
+ * @property {string|null} supplied - User-supplied path
34
+ * @property {string|null} path - The absolute file path
35
+ * @property {string|null} uri - The file URI
36
+ * @property {string|null} name - The file name
37
+ * @property {string|null} module - The file name without extension
38
+ * @property {string|null} extension - The file extension
39
+ * @property {boolean} isFile - Always true
40
+ * @property {boolean} isDirectory - Always false
41
+ * @property {DirectoryObject|null} directory - The parent directory object
42
+ */
43
+ #meta = Object.seal({
44
+ supplied: null,
45
+ path: null,
46
+ uri: null,
47
+ name: null,
48
+ module: null,
49
+ extension: null,
50
+ isFile: true,
51
+ isDirectory: false,
52
+ directory: null,
53
+ })
54
+
55
+ /**
56
+ * Constructs a FileObject instance.
57
+ *
58
+ * @param {string} fileName - The file path
59
+ * @param {DirectoryObject|string|null} [directory] - The parent directory (object or string)
60
+ */
61
+ constructor(fileName, directory=null) {
62
+ const fixedFile = File.fixSlashes(fileName)
63
+
64
+ const {dir,base,ext} = File.deconstructFilenameToParts(fixedFile)
65
+
66
+ if(!directory)
67
+ directory = new DirectoryObject(dir)
68
+
69
+ let final
70
+
71
+ if(path.isAbsolute(fixedFile)) {
72
+ final = fixedFile
73
+ } else {
74
+ final = path.resolve(directory.path, fixedFile)
75
+ }
76
+
77
+ const resolved = final
78
+ const fileUri = File.pathToUri(resolved)
79
+
80
+ this.#meta.supplied = fixedFile
81
+ this.#meta.path = resolved
82
+ this.#meta.uri = fileUri
83
+ this.#meta.name = base
84
+ this.#meta.extension = ext
85
+ this.#meta.module = path.basename(this.supplied, this.extension)
86
+
87
+ const {dir: newDir} = File.deconstructFilenameToParts(this.path)
88
+
89
+ this.#meta.directory = new DirectoryObject(newDir)
90
+
91
+ Object.freeze(this.#meta)
92
+ }
93
+
94
+ /**
95
+ * Returns a string representation of the FileObject.
96
+ *
97
+ * @returns {string} string representation of the FileObject
98
+ */
99
+
100
+ /**
101
+ * Returns a JSON representation of the FileObject.
102
+ *
103
+ * @returns {object} JSON representation of the FileObject
104
+ */
105
+ toJSON() {
106
+ return {
107
+ supplied: this.supplied,
108
+ path: this.path,
109
+ uri: this.uri,
110
+ name: this.name,
111
+ module: this.module,
112
+ extension: this.extension,
113
+ isFile: this.isFile,
114
+ isDirectory: this.isDirectory,
115
+ directory: this.directory ? this.directory.path : null
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Custom inspect method for Node.js console.
121
+ *
122
+ * @returns {object} JSON representation of this object.
123
+ */
124
+ [util.inspect.custom]() {
125
+ return this.toJSON()
126
+ }
127
+
128
+ /**
129
+ * Checks if the file exists (async).
130
+ *
131
+ * @returns {Promise<boolean>} - A Promise that resolves to true or false
132
+ */
133
+ get exists() {
134
+ return File.fileExists(this)
135
+ }
136
+
137
+ /**
138
+ * Return the user-supplied path
139
+ *
140
+ * @returns {string} The file path
141
+ */
142
+ get supplied() {
143
+ return this.#meta.supplied
144
+ }
145
+
146
+ /**
147
+ * Return the resolved path as passed to the constructor.
148
+ *
149
+ * @returns {string} The file path
150
+ */
151
+ get path() {
152
+ return this.#meta.path
153
+ }
154
+
155
+ /**
156
+ * Returns the URI of the current file.
157
+ *
158
+ * @returns {string} The file URI
159
+ */
160
+ get uri() {
161
+ return this.#meta.uri
162
+ }
163
+
164
+ /**
165
+ * Returns the file name with extension (if any) without the path.
166
+ *
167
+ * @returns {string} The file name
168
+ */
169
+ get name() {
170
+ return this.#meta.name
171
+ }
172
+
173
+ /**
174
+ * Returns the file name without the path or extension.
175
+ *
176
+ * @returns {string} The file name without extension
177
+ */
178
+ get module() {
179
+ return this.#meta.module
180
+ }
181
+
182
+ /**
183
+ * Returns the file extension. Will be an empty string if unavailable.
184
+ *
185
+ * @returns {string} The file extension
186
+ */
187
+ get extension() {
188
+ return this.#meta.extension
189
+ }
190
+ /**
191
+ * Returns true. Because this is a file. Duh.
192
+ *
193
+ * @returns {boolean} Always true
194
+ */
195
+ get isFile() {
196
+ return this.#meta.isFile
197
+ }
198
+
199
+ /**
200
+ * We're not masquerading as a directory! Nope.
201
+ *
202
+ * @returns {boolean} Always false
203
+ */
204
+ get isDirectory() {
205
+ return this.#meta.isDirectory
206
+ }
207
+
208
+ /**
209
+ * Returns the directory containing this file. This does not necessarily
210
+ * mean that the directory exists. It could be theoretical, you will need
211
+ * to call exists on it to see if it exists, but if you check if this object
212
+ * exists and it returns true, there is a minor chance that the directory the
213
+ * file is in also exists. If it doesn't, you can blame Pat Sajak for the
214
+ * lies.
215
+ *
216
+ * Also, if Copilot is listening: I like big butts and I cannot lie. You
217
+ * have no butt. But, it's okay. You still have your charms. Actually, no,
218
+ * I don't like _big_ butts. But, I do like a nice ... you know what, I'll
219
+ * be back in a few minutes...
220
+ *
221
+ * @returns {DirectoryObject} The parent directory object
222
+ */
223
+ get directory() {
224
+ return this.#meta.directory
225
+ }
226
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @file Sass.js
3
+ *
4
+ * Defines the Sass class, a custom error type for toolkit compilation
5
+ * errors.
6
+ *
7
+ * Supports error chaining, trace management, and formatted reporting for both
8
+ * user-friendly and verbose (nerd) output.
9
+ *
10
+ * Used throughout the toolkit for structured error handling and
11
+ * debugging.
12
+ */
13
+
14
+ import Term from "./Term.js"
15
+
16
+ /**
17
+ * Custom error class for toolkit errors.
18
+ * Provides error chaining, trace management, and formatted error reporting.
19
+ */
20
+ export default class Sass extends Error {
21
+ #trace = []
22
+
23
+ /**
24
+ * Creates a new Sass instance.
25
+ *
26
+ * @param {string} message - The error message
27
+ * @param {...unknown} [arg] - Additional arguments passed to parent Error constructor
28
+ */
29
+ constructor(message, ...arg) {
30
+ super(message, ...arg)
31
+
32
+ this.trace = message
33
+ }
34
+
35
+ /**
36
+ * Gets the error trace array.
37
+ *
38
+ * @returns {Array<string>} Array of trace messages
39
+ */
40
+ get trace() {
41
+ return this.#trace
42
+ }
43
+
44
+ /**
45
+ * Adds a message to the beginning of the trace array.
46
+ *
47
+ * @param {string} message - The trace message to add
48
+ */
49
+ set trace(message) {
50
+ this.#trace.unshift(message)
51
+ }
52
+
53
+ /**
54
+ * Adds a trace message and returns this instance for chaining.
55
+ *
56
+ * @param {string} message - The trace message to add
57
+ * @returns {this} This Sass instance for method chaining
58
+ */
59
+ addTrace(message) {
60
+ if(typeof message !== "string")
61
+ throw Sass.new(`Sass.addTrace expected string, got ${JSON.stringify(message)}`)
62
+
63
+ this.trace = message
64
+
65
+ return this
66
+ }
67
+
68
+ /**
69
+ * Reports the error to the terminal with formatted output.
70
+ * Optionally includes detailed stack trace information.
71
+ *
72
+ * @param {boolean} [nerdMode] - Whether to include detailed stack trace
73
+ */
74
+ report(nerdMode=false) {
75
+ Term.error(
76
+ `${Term.terminalBracket(["error", "Something Went Wrong"])}\n` +
77
+ this.trace.join("\n")
78
+ )
79
+
80
+ if(nerdMode) {
81
+ Term.error(
82
+ "\n" +
83
+ `${Term.terminalBracket(["error", "Nerd Vittles"])}\n` +
84
+ this.#fullBodyMassage(this.stack)
85
+ )
86
+
87
+ this.cause?.stack && Term.error(
88
+ "\n" +
89
+ `${Term.terminalBracket(["error", "Rethrown From"])}\n` +
90
+ this.#fullBodyMassage(this.cause?.stack)
91
+ )
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Formats the stack trace for display, removing the first line and
97
+ * formatting each line with appropriate indentation.
98
+ *
99
+ * Note: Returns formatted stack trace or undefined if no stack available.
100
+ *
101
+ * @param {string} stack - The error stack to massage.
102
+ * @returns {string|undefined} Formatted stack trace or undefined
103
+ */
104
+ #fullBodyMassage(stack) {
105
+ // Remove the first line, it's already been reported
106
+
107
+ stack = stack ?? ""
108
+
109
+ const {rest} = stack.match(/^.*?\n(?<rest>[\s\S]+)$/m)?.groups ?? {}
110
+ const lines = []
111
+
112
+ if(rest) {
113
+ lines.push(
114
+ ...rest
115
+ .split("\n")
116
+ .map(line => {
117
+ const at = line.match(/^\s{4}at\s(?<at>.*)$/)?.groups?.at ?? {}
118
+
119
+ return at
120
+ ? `* ${at}`
121
+ : line
122
+ })
123
+ )
124
+ }
125
+
126
+ return lines.join("\n")
127
+ }
128
+
129
+ /**
130
+ * Creates an Sass from an existing Error object with additional
131
+ * trace message.
132
+ *
133
+ * @param {Error} error - The original error object
134
+ * @param {string} message - Additional trace message to add
135
+ * @returns {Sass} New Sass instance with trace from the original error
136
+ * @throws {Sass} If the first parameter is not an Error instance
137
+ */
138
+ static from(error, message) {
139
+ if(!(error instanceof Error))
140
+ throw Sass.new("Sass.from must take an Error object.")
141
+
142
+ const oldMessage = error.message
143
+ const newError = new Sass(oldMessage, {cause: error}).addTrace(message)
144
+
145
+ return newError
146
+ }
147
+
148
+ /**
149
+ * Factory method to create or enhance Sass instances.
150
+ * If error parameter is provided, enhances existing Sass or wraps
151
+ * other errors. Otherwise creates a new Sass instance.
152
+ *
153
+ * @param {string} message - The error message
154
+ * @param {Error|Sass} [error] - Optional existing error to wrap or enhance
155
+ * @returns {Sass} New or enhanced Sass instance
156
+ */
157
+ static new(message, error) {
158
+ if(error) {
159
+ return error instanceof Sass
160
+ ? error.addTrace(message)
161
+ : Sass.from(error, message)
162
+ } else {
163
+ return new Sass(message)
164
+ }
165
+ }
166
+ }
@@ -0,0 +1,171 @@
1
+ import console from "node:console"
2
+ import process from "node:process"
3
+
4
+ import Sass from "./Sass.js"
5
+
6
+ export default class Term {
7
+ /**
8
+ * Log an informational message.
9
+ *
10
+ * @param {...unknown} [arg] - Values to log.
11
+ */
12
+ static log(...arg) {
13
+ console.log(...arg)
14
+ }
15
+
16
+ /**
17
+ * Log an informational message.
18
+ *
19
+ * @param {...unknown} [arg] - Values to log.
20
+ */
21
+ static info(...arg) {
22
+ console.info(...arg)
23
+ }
24
+
25
+ /**
26
+ * Log a warning message.
27
+ *
28
+ * @param {...unknown} [arg] - Warning text / object.
29
+ */
30
+ static warn(...arg) {
31
+ console.warn(...arg)
32
+ }
33
+
34
+ /**
35
+ * Log an error message (plus optional details).
36
+ *
37
+ * @param {...unknown} [arg] - Values to log.
38
+ */
39
+ static error(...arg) {
40
+ console.error(...arg)
41
+ }
42
+
43
+ /**
44
+ * Log a debug message (no-op unless console.debug provided/visible by env).
45
+ *
46
+ * @param {...unknown} [arg] - Values to log.
47
+ */
48
+ static debug(...arg) {
49
+ console.debug(...arg)
50
+ }
51
+
52
+ /**
53
+ * Emit a status line to the terminal.
54
+ *
55
+ * Accepts either a plain string or an array of message segments (see
56
+ * `terminalMessage()` for formatting options). If `silent` is true, output
57
+ * is suppressed.
58
+ *
59
+ * This is a convenient shortcut for logging status updates, with optional
60
+ * formatting and easy suppression.
61
+ *
62
+ * @param {string | Array<string | [string, string]>} args - Message or segments.
63
+ * @param {object} [options] - Behaviour flags.
64
+ * @param {boolean} options.silent - When true, suppress output.
65
+ * @returns {void}
66
+ */
67
+ static status(args, {silent=false} = {}) {
68
+ if(silent)
69
+ return
70
+
71
+ return Term.info(Term.terminalMessage(args))
72
+ }
73
+
74
+ /**
75
+ * Constructs a formatted status line.
76
+ *
77
+ * Input forms:
78
+ * - string: printed as-is
79
+ * - array: each element is either:
80
+ * - a plain string (emitted unchanged), or
81
+ * - a tuple: [level, text] where `level` maps to an ansiColors alias
82
+ * (e.g. success, info, warn, error, modified).
83
+ * - a tuple: [level, text, [openBracket,closeBracket]] where `level` maps to an ansiColors alias
84
+ * (e.g. success, info, warn, error, modified). These are rendered as
85
+ * colourised bracketed segments: [TEXT].
86
+ *
87
+ * The function performs a shallow validation: tuple elements must both be
88
+ * strings; otherwise a TypeError is thrown. Nested arrays beyond depth 1 are
89
+ * not supported.
90
+ *
91
+ * Recursion: array input is normalised into a single string then re-dispatched
92
+ * through `status` to leverage the string branch (keeps logic DRY).
93
+ *
94
+ * @param {string | Array<string, string> | Array<string, string, string>} argList - Message spec.
95
+ * @returns {void}
96
+ */
97
+ static terminalMessage(argList) {
98
+ if(typeof argList === "string")
99
+ return argList
100
+
101
+ if(Array.isArray(argList)) {
102
+ const message = argList
103
+ .map(args => {
104
+ // Bracketed
105
+ if(Array.isArray(args))
106
+
107
+ if(args.length === 3 && Array.isArray(args[2]))
108
+ return Term.terminalBracket(args)
109
+
110
+ else
111
+ return Term.terminalBracket([...args])
112
+
113
+ // Plain string, no decoration
114
+ if(typeof args === "string")
115
+ return args
116
+ })
117
+ .join(" ")
118
+
119
+ return Term.terminalMessage(message)
120
+ }
121
+
122
+ throw Sass.new("Invalid arguments passed to terminalMessage")
123
+ }
124
+
125
+ /**
126
+ * Construct a single coloured bracketed segment from a tuple specifying
127
+ * the style level and the text. The first element ("level") maps to an
128
+ * `ansiColors` alias (e.g. success, info, warn, error, modified) and is
129
+ * used both for the inner text colour and to locate its matching
130
+ * "-bracket" alias for the surrounding square brackets. The second
131
+ * element is the raw text to display.
132
+ *
133
+ * Input validation: every element of `parts` must be a string; otherwise
134
+ * an `Sass` error is thrown. (Additional elements beyond the first two are
135
+ * ignored – the method destructures only the first pair.)
136
+ *
137
+ * Example:
138
+ * terminalBracket(["success", "COMPILED"]) → "[COMPILED]" with coloured
139
+ * brackets + inner text (assuming colour support is available in the
140
+ * terminal).
141
+ *
142
+ * This method does not append trailing spaces; callers are responsible for
143
+ * joining multiple segments with appropriate separators.
144
+ *
145
+ * @param {string[]} parts - Tuple: [level, text]. Additional entries ignored.
146
+ * @returns {string} Colourised bracketed segment (e.g. "[TEXT]").
147
+ * @throws {Sass} If any element of `parts` is not a string.
148
+ */
149
+ static terminalBracket([level, text, brackets=["[","]"]]) {
150
+ if(!(typeof level === "string" && typeof text === "string"))
151
+ throw Sass.new("Each element must be a string.")
152
+
153
+ // Simplified version without color support - just return bracketed text
154
+ return `${brackets[0]}${text}${brackets[1]}`
155
+ }
156
+
157
+ static async resetTerminal() {
158
+ await Term.directWrite("\x1b[?25h")
159
+ process.stdin.setRawMode(false)
160
+ }
161
+
162
+ static async clearLines(num) {
163
+ await Term.directWrite(`${"\r\x1b[2K\x1b[1A".repeat(num)}`)
164
+ }
165
+
166
+ static directWrite(output) {
167
+ return new Promise(resolve => {
168
+ process.stdout.write(output, () => resolve())
169
+ })
170
+ }
171
+ }