@gesslar/sassy 0.21.1 → 0.21.3

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/src/Sass.js DELETED
@@ -1,166 +0,0 @@
1
- /**
2
- * @file Sass.js
3
- *
4
- * Defines the Sass class, a custom error type for theme 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 theme engine for structured error handling and
11
- * debugging.
12
- */
13
-
14
- import Term from "./Term.js"
15
-
16
- /**
17
- * Custom error class for Sassy theme compilation 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
- }
package/src/Term.js DELETED
@@ -1,175 +0,0 @@
1
- import c from "@gesslar/colours"
2
- // import colorSupport from "color-support"
3
- import console from "node:console"
4
- import process from "node:process"
5
-
6
- import Sass from "./Sass.js"
7
-
8
- export default class Term {
9
- /**
10
- * Log an informational message.
11
- *
12
- * @param {...unknown} [arg] - Values to log.
13
- */
14
- static log(...arg) {
15
- console.log(...arg)
16
- }
17
-
18
- /**
19
- * Log an informational message.
20
- *
21
- * @param {...unknown} [arg] - Values to log.
22
- */
23
- static info(...arg) {
24
- console.info(...arg)
25
- }
26
-
27
- /**
28
- * Log a warning message.
29
- *
30
- * @param {...unknown} [arg] - Warning text / object.
31
- */
32
- static warn(...arg) {
33
- console.warn(...arg)
34
- }
35
-
36
- /**
37
- * Log an error message (plus optional details).
38
- *
39
- * @param {...unknown} [arg] - Values to log.
40
- */
41
- static error(...arg) {
42
- console.error(...arg)
43
- }
44
-
45
- /**
46
- * Log a debug message (no-op unless console.debug provided/visible by env).
47
- *
48
- * @param {...unknown} [arg] - Values to log.
49
- */
50
- static debug(...arg) {
51
- console.debug(...arg)
52
- }
53
-
54
- /**
55
- * Emit a status line to the terminal.
56
- *
57
- * Accepts either a plain string or an array of message segments (see
58
- * `terminalMessage()` for formatting options). If `silent` is true, output
59
- * is suppressed.
60
- *
61
- * This is a convenient shortcut for logging status updates, with optional
62
- * formatting and easy suppression.
63
- *
64
- * @param {string | Array<string | [string, string]>} args - Message or segments.
65
- * @param {object} [options] - Behaviour flags.
66
- * @param {boolean} options.silent - When true, suppress output.
67
- * @returns {void}
68
- */
69
- static status(args, {silent=false} = {}) {
70
- if(silent)
71
- return
72
-
73
- return Term.info(Term.terminalMessage(args))
74
- }
75
-
76
- /**
77
- * Constructs a formatted status line.
78
- *
79
- * Input forms:
80
- * - string: printed as-is
81
- * - array: each element is either:
82
- * - a plain string (emitted unchanged), or
83
- * - a tuple: [level, text] where `level` maps to an ansiColors alias
84
- * (e.g. success, info, warn, error, modified).
85
- * - a tuple: [level, text, [openBracket,closeBracket]] where `level` maps to an ansiColors alias
86
- * (e.g. success, info, warn, error, modified). These are rendered as
87
- * colourised bracketed segments: [TEXT].
88
- *
89
- * The function performs a shallow validation: tuple elements must both be
90
- * strings; otherwise a TypeError is thrown. Nested arrays beyond depth 1 are
91
- * not supported.
92
- *
93
- * Recursion: array input is normalised into a single string then re-dispatched
94
- * through `status` to leverage the string branch (keeps logic DRY).
95
- *
96
- * @param {string | Array<string, string> | Array<string, string, string>} argList - Message spec.
97
- * @returns {void}
98
- */
99
- static terminalMessage(argList) {
100
- if(typeof argList === "string")
101
- return argList
102
-
103
- if(Array.isArray(argList)) {
104
- const message = argList
105
- .map(args => {
106
- // Bracketed
107
- if(Array.isArray(args))
108
-
109
- if(args.length === 3 && Array.isArray(args[2]))
110
- return Term.terminalBracket(args)
111
-
112
- else
113
- return Term.terminalBracket([...args])
114
-
115
- // Plain string, no decoration
116
- if(typeof args === "string")
117
- return args
118
- })
119
- .join(" ")
120
-
121
- return Term.terminalMessage(message)
122
- }
123
-
124
- throw Sass.new("Invalid arguments passed to terminalMessage")
125
- }
126
-
127
- /**
128
- * Construct a single coloured bracketed segment from a tuple specifying
129
- * the style level and the text. The first element ("level") maps to an
130
- * `ansiColors` alias (e.g. success, info, warn, error, modified) and is
131
- * used both for the inner text colour and to locate its matching
132
- * "-bracket" alias for the surrounding square brackets. The second
133
- * element is the raw text to display.
134
- *
135
- * Input validation: every element of `parts` must be a string; otherwise
136
- * an `Sass` error is thrown. (Additional elements beyond the first two are
137
- * ignored – the method destructures only the first pair.)
138
- *
139
- * Example:
140
- * terminalBracket(["success", "COMPILED"]) → "[COMPILED]" with coloured
141
- * brackets + inner text (assuming colour support is available in the
142
- * terminal).
143
- *
144
- * This method does not append trailing spaces; callers are responsible for
145
- * joining multiple segments with appropriate separators.
146
- *
147
- * @param {string[]} parts - Tuple: [level, text]. Additional entries ignored.
148
- * @returns {string} Colourised bracketed segment (e.g. "[TEXT]").
149
- * @throws {Sass} If any element of `parts` is not a string.
150
- */
151
- static terminalBracket([level, text, brackets=["",""]]) {
152
- if(!(typeof level === "string" && typeof text === "string"))
153
- throw Sass.new("Each element must be a string.")
154
-
155
- return "" +
156
- c`{${level}-bracket}${brackets[0]}{/}`
157
- + c`{${level}}${text}{/}`
158
- + c`{${level}-bracket}${brackets[1]}{/}`
159
- }
160
-
161
- static async resetTerminal() {
162
- await Term.directWrite("\x1b[?25h")
163
- process.stdin.setRawMode(false)
164
- }
165
-
166
- static async clearLines(num) {
167
- await Term.directWrite(`${"\r\x1b[2K\x1b[1A".repeat(num)}`)
168
- }
169
-
170
- static directWrite(output) {
171
- return new Promise(resolve => {
172
- process.stdout.write(output, () => resolve())
173
- })
174
- }
175
- }
package/src/Type.js DELETED
@@ -1,207 +0,0 @@
1
- /**
2
- * @file Type specification and validation utilities.
3
- * Provides TypeSpec class for parsing and validating complex type specifications
4
- * including arrays, unions, and options.
5
- */
6
-
7
- import Sass from "./Sass.js"
8
- import Data from "./Data.js"
9
-
10
- /**
11
- * Type specification class for parsing and validating complex type definitions.
12
- * Supports union types, array types, and validation options.
13
- */
14
- export default class TypeSpec {
15
- #specs
16
-
17
- /**
18
- * Creates a new TypeSpec instance.
19
- *
20
- * @param {string} string - The type specification string (e.g., "string|number", "object[]")
21
- * @param {object} options - Additional parsing options
22
- */
23
- constructor(string, options) {
24
- this.#specs = []
25
- this.#parse(string, options)
26
- Object.freeze(this.#specs)
27
- this.specs = this.#specs
28
- this.length = this.#specs.length
29
- this.stringRepresentation = this.toString()
30
- Object.freeze(this)
31
- }
32
-
33
- /**
34
- * Returns a string representation of the type specification.
35
- *
36
- * @returns {string} The type specification as a string (e.g., "string|number[]")
37
- */
38
- toString() {
39
- return this.#specs
40
- .map(spec => {
41
- return `${spec.typeName}${spec.array ? "[]" : ""}`
42
- })
43
- .join("|")
44
- }
45
-
46
- /**
47
- * Returns a JSON representation of the TypeSpec.
48
- *
49
- * @returns {object} Object containing specs, length, and string representation
50
- */
51
- toJSON() {
52
- // Serialize as a string representation or as raw data
53
- return {
54
- specs: this.#specs,
55
- length: this.length,
56
- stringRepresentation: this.toString(),
57
- }
58
- }
59
-
60
- /**
61
- * Executes a provided function once for each type specification.
62
- *
63
- * @param {function(unknown): void} callback - Function to execute for each spec
64
- */
65
- forEach(callback) {
66
- this.#specs.forEach(callback)
67
- }
68
-
69
- /**
70
- * Tests whether all type specifications pass the provided test function.
71
- *
72
- * @param {function(unknown): boolean} callback - Function to test each spec
73
- * @returns {boolean} True if all specs pass the test
74
- */
75
- every(callback) {
76
- return this.#specs.every(callback)
77
- }
78
-
79
- /**
80
- * Tests whether at least one type specification passes the provided test function.
81
- *
82
- * @param {function(unknown): boolean} callback - Function to test each spec
83
- * @returns {boolean} True if at least one spec passes the test
84
- */
85
- some(callback) {
86
- return this.#specs.some(callback)
87
- }
88
-
89
- /**
90
- * Creates a new array with all type specifications that pass the provided test function.
91
- *
92
- * @param {function(unknown): boolean} callback - Function to test each spec
93
- * @returns {Array} New array with filtered specs
94
- */
95
- filter(callback) {
96
- return this.#specs.filter(callback)
97
- }
98
-
99
- /**
100
- * Creates a new array populated with the results of calling the provided function on every spec.
101
- *
102
- * @param {function(unknown): unknown} callback - Function to call on each spec
103
- * @returns {Array} New array with mapped values
104
- */
105
- map(callback) {
106
- return this.#specs.map(callback)
107
- }
108
-
109
- /**
110
- * Executes a reducer function on each spec, resulting in a single output value.
111
- *
112
- * @param {function(unknown, unknown): unknown} callback - Function to execute on each spec
113
- * @param {unknown} initialValue - Initial value for the accumulator
114
- * @returns {unknown} The final accumulated value
115
- */
116
- reduce(callback, initialValue) {
117
- return this.#specs.reduce(callback, initialValue)
118
- }
119
-
120
- /**
121
- * Returns the first type specification that satisfies the provided testing function.
122
- *
123
- * @param {function(unknown): boolean} callback - Function to test each spec
124
- * @returns {object|undefined} The first spec that matches, or undefined
125
- */
126
- find(callback) {
127
- return this.#specs.find(callback)
128
- }
129
-
130
- /**
131
- * Tests whether a value matches any of the type specifications.
132
- * Handles array types, union types, and empty value validation.
133
- *
134
- * @param {unknown} value - The value to test against the type specifications
135
- * @param {object} options - Validation options
136
- * @param {boolean} options.allowEmpty - Whether empty values are allowed
137
- * @returns {boolean} True if the value matches any type specification
138
- */
139
- match(value, options) {
140
- const allowEmpty = options?.allowEmpty ?? true
141
- const empty = Data.isEmpty(value)
142
-
143
- // If we have a list of types, because the string was validly parsed,
144
- // we need to ensure that all of the types that were parsed are valid types
145
- // in JavaScript.
146
- if(this.length && !this.every(t => Data.isValidType(t.typeName)))
147
- return false
148
-
149
- // Now, let's do some checking with the types, respecting the array flag
150
- // with the value
151
- const valueType = Data.typeOf(value)
152
- const isArray = valueType === "array"
153
-
154
- // We need to ensure that we match the type and the consistency of the types
155
- // in an array, if it is an array and an array is allowed.
156
- const matchingTypeSpec = this.filter(spec => {
157
- const {typeName: allowedType, array: allowedArray} = spec
158
-
159
- if(valueType === allowedType && !isArray && !allowedArray)
160
- return !allowEmpty ? !empty : true
161
-
162
- if(isArray) {
163
- if(allowedType === "array")
164
- if(!allowedArray)
165
- return true
166
-
167
- if(empty)
168
- if(allowEmpty)
169
- return true
170
-
171
- return Data.isArrayUniform(value, allowedType)
172
- }
173
- })
174
-
175
- return matchingTypeSpec.length > 0
176
- }
177
-
178
- /**
179
- * Parses a type specification string into individual type specs.
180
- * Handles union types separated by delimiters and array notation.
181
- *
182
- * @private
183
- * @param {string} string - The type specification string to parse
184
- * @param {object} options - Parsing options
185
- * @param {string} options.delimiter - The delimiter for union types
186
- * @throws {TypeError} If the type specification is invalid
187
- */
188
- #parse(string, options) {
189
- const delimiter = options?.delimiter ?? "|"
190
- const parts = string.split(delimiter)
191
-
192
- this.#specs = parts.map(part => {
193
- const typeMatches = /(\w+)(\[\])?/.exec(part)
194
-
195
- if(!typeMatches || typeMatches.length !== 3)
196
- throw Sass.new(`Invalid type: ${part}`)
197
-
198
- if(!Data.isValidType(typeMatches[1]))
199
- throw Sass.new(`Invalid type: ${typeMatches[1]}`)
200
-
201
- return {
202
- typeName: typeMatches[1],
203
- array: typeMatches[2] === "[]",
204
- }
205
- })
206
- }
207
- }
package/src/Util.js DELETED
@@ -1,132 +0,0 @@
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 `${text.slice(0,1).toUpperCase()}${text.slice(1)}`
17
- }
18
-
19
- /**
20
- * Measure wall-clock time for an async function.
21
- *
22
- * @template T
23
- * @param {() => Promise<T>} fn - Thunk returning a promise.
24
- * @returns {Promise<{result: T, cost: number}>} Object containing result and elapsed ms (number, 1 decimal).
25
- */
26
- static async time(fn) {
27
- const t0 = performance.now()
28
- const result = await fn()
29
- const cost = Math.round((performance.now() - t0) * 10) / 10
30
-
31
- return {result, cost}
32
- }
33
-
34
- /**
35
- * Right-align a string inside a fixed width (left pad with spaces).
36
- * If the string exceeds width it is returned unchanged.
37
- *
38
- * @param {string|number} text - Text to align.
39
- * @param {number} width - Target field width (default 80).
40
- * @returns {string} Padded string.
41
- */
42
- static rightAlignText(text, width=80) {
43
- const work = String(text)
44
-
45
- if(work.length > width)
46
- return work
47
-
48
- const diff = width-work.length
49
-
50
- return `${" ".repeat(diff)}${work}`
51
- }
52
-
53
- /**
54
- * Compute sha256 hash (hex) of the provided string.
55
- *
56
- * @param {string} s - Input string.
57
- * @returns {string} 64-char hexadecimal digest.
58
- */
59
- static hashOf(s) {
60
- return createHash("sha256").update(s).digest("hex")
61
- }
62
-
63
- /**
64
- * Extracts canonical option names from a Commander-style options object.
65
- *
66
- * Each key in the input object is a string containing one or more option
67
- * forms, separated by commas (e.g. "-w, --watch"). This function splits each
68
- * key, trims whitespace, and parses out the long option name (e.g. "watch")
69
- * for each entry. If no long option ("--") is present, the short option (e.g.
70
- * "v" from "-v") will be included in the result array. If both are present,
71
- * the long option is preferred.
72
- *
73
- * Example:
74
- * generateOptionNames({"-w, --watch": "desc", "-v": "desc"})
75
- * → ["watch", "v"]
76
- *
77
- * Edge cases:
78
- * - If a key contains only a short option ("-v"), that short name will be
79
- * included in the result.
80
- * - If multiple long options are present, only the first is used.
81
- * - If the option string is malformed, may return undefined for that entry
82
- * (filtered out).
83
- *
84
- * @param {object} object - Mapping of option strings to descriptions.
85
- * @returns {string[]} Array of canonical option names (long preferred, short if no long present).
86
- */
87
- static generateOptionNames(object) {
88
- return Object.keys(object)
89
- .map(key => {
90
- return key
91
- .split(",")
92
- .map(o => o.trim())
93
- .map(o => o.match(/^(?<sign>--?)(?<option>[\w-]+)/).groups)
94
- .reduce((acc, curr) => acc.sign === "--" ? acc : curr, {})
95
- ?.option
96
- })
97
- .filter(Boolean)
98
- }
99
-
100
- /**
101
- * Asynchronously awaits all promises in parallel.
102
- * Wrapper around Promise.all for consistency with other utility methods.
103
- *
104
- * @param {Promise[]} promises - Array of promises to await
105
- * @returns {Promise<unknown[]>} Results of all promises
106
- */
107
- static async awaitAll(promises) {
108
- return await Promise.all(promises)
109
- }
110
-
111
- /**
112
- * Settles all promises (both fulfilled and rejected) in parallel.
113
- * Wrapper around Promise.allSettled for consistency with other utility methods.
114
- *
115
- * @param {Promise[]} promises - Array of promises to settle
116
- * @returns {Promise<Array>} Results of all settled promises with status and value/reason
117
- */
118
- static async settleAll(promises) {
119
- return await Promise.allSettled(promises)
120
- }
121
-
122
- /**
123
- * Returns the first promise to resolve or reject from an array of promises.
124
- * Wrapper around Promise.race for consistency with other utility methods.
125
- *
126
- * @param {Promise[]} promises - Array of promises to race
127
- * @returns {Promise<unknown>} Result of the first settled promise
128
- */
129
- static async race(promises) {
130
- return await Promise.race(promises)
131
- }
132
- }