@gesslar/sassy 0.21.0 → 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/File.js DELETED
@@ -1,346 +0,0 @@
1
- /**
2
- * @file File system utilities for reading, writing, and manipulating files and directories.
3
- * Provides comprehensive file operations including data file loading (JSON5/YAML),
4
- * path resolution, and file system navigation with support for both files and directories.
5
- */
6
-
7
- import {globby} from "globby"
8
- import JSON5 from "json5"
9
- import fs from "node:fs/promises"
10
- import path from "node:path"
11
- import url from "node:url"
12
- import YAML from "yaml"
13
-
14
- import Sass from "./Sass.js"
15
- import Data from "./Data.js"
16
- import DirectoryObject from "./DirectoryObject.js"
17
- import FileObject from "./FileObject.js"
18
- import Valid from "./Valid.js"
19
-
20
- export default class File {
21
- /**
22
- * Fix slashes in a path
23
- *
24
- * @param {string} pathName - The path to fix
25
- * @returns {string} The fixed path
26
- */
27
- static fixSlashes(pathName) {
28
- return pathName.replace(/\\/g, "/")
29
- }
30
-
31
- /**
32
- * Convert a path to a URI
33
- *
34
- * @param {string} pathName - The path to convert
35
- * @returns {string} The URI
36
- */
37
- static pathToUri(pathName) {
38
- try {
39
- return url.pathToFileURL(pathName).href
40
- } catch(e) {
41
- void e // stfu linter
42
-
43
- return pathName
44
- }
45
- }
46
-
47
- /**
48
- * Check if a file can be read. Returns true if the file can be read, false
49
- *
50
- * @param {FileObject} file - The file map to check
51
- * @returns {Promise<boolean>} Whether the file can be read
52
- */
53
- static async canReadFile(file) {
54
- try {
55
- await fs.access(file.path, fs.constants.R_OK)
56
-
57
- return true
58
- } catch(_) {
59
- return false
60
- }
61
- }
62
-
63
- /**
64
- * Check if a file can be written. Returns true if the file can be written,
65
- *
66
- * @param {FileObject} file - The file map to check
67
- * @returns {Promise<boolean>} Whether the file can be written
68
- */
69
- static async canWriteFile(file) {
70
- try {
71
- await fs.access(file.path, fs.constants.W_OK)
72
-
73
- return true
74
- } catch(_error) {
75
- return false
76
- }
77
- }
78
-
79
- /**
80
- * Check if a file exists
81
- *
82
- * @param {FileObject} file - The file map to check
83
- * @returns {Promise<boolean>} Whether the file exists
84
- */
85
- static async fileExists(file) {
86
- try {
87
- await fs.access(file.path, fs.constants.R_OK)
88
-
89
- return true
90
- } catch(_) {
91
- return false
92
- }
93
- }
94
-
95
- /**
96
- * Determines the size of a file.
97
- *
98
- * @param {FileObject} file - The file object to test
99
- * @returns {Promise<number?>} - The size of the file or null, if it doesn't exist.
100
- */
101
- static async fileSize(file) {
102
- try {
103
- const stat = await fs.stat(file.path)
104
-
105
- return stat.size
106
- } catch(_) {
107
- return null
108
- }
109
- }
110
-
111
- /**
112
- * Gets the last modification time of a file.
113
- * Used by the caching system to determine if cached data is still valid.
114
- *
115
- * @param {FileObject} file - The file object to check
116
- * @returns {Promise<Date|null>} The last modification time, or null if file doesn't exist
117
- */
118
- static async fileModified(file) {
119
- try {
120
- const stat = await fs.stat(file.path)
121
-
122
- return stat.mtime
123
- } catch(_) {
124
- return null
125
- }
126
- }
127
-
128
- /**
129
- * Check if a directory exists
130
- *
131
- * @param {DirectoryObject} dirObject - The directory map to check
132
- * @returns {Promise<boolean>} Whether the directory exists
133
- */
134
- static async directoryExists(dirObject) {
135
- try {
136
- (await fs.opendir(dirObject.path)).close()
137
-
138
- return true
139
- } catch(_) {
140
- return false
141
- }
142
- }
143
-
144
- /**
145
- * Convert a URI to a path
146
- *
147
- * @param {string} pathName - The URI to convert
148
- * @returns {string} The path
149
- */
150
- static uriToPath(pathName) {
151
- try {
152
- return url.fileURLToPath(pathName)
153
- } catch(_) {
154
- return pathName
155
- }
156
- }
157
-
158
- /**
159
- * @typedef {object} FileParts
160
- * @property {string} base - The file name with extension
161
- * @property {string} dir - The directory path
162
- * @property {string} ext - The file extension (including dot)
163
- */
164
-
165
- /**
166
- * Deconstruct a filename into parts
167
- *
168
- * @param {string} fileName - The filename to deconstruct
169
- * @returns {FileParts} The filename parts
170
- */
171
- static deconstructFilenameToParts(fileName) {
172
- Valid.assert(typeof fileName === "string" && fileName.length > 0,
173
- "file must be a non-zero length string", 1)
174
-
175
- return path.parse(fileName)
176
- }
177
-
178
- /**
179
- * Retrieve all files matching a specific glob pattern.
180
- *
181
- * @param {string|string[]} glob - The glob pattern(s) to search.
182
- * @returns {Promise<Array<FileObject>>} A promise that resolves to an array of file objects
183
- * @throws {Sass} If the input is not a string or array of strings.
184
- * @throws {Sass} If the glob pattern array is empty or for other search failures.
185
- */
186
- static async getFiles(glob) {
187
- Valid.assert(
188
- (
189
- (typeof glob === "string" && glob.length > 0) ||
190
- (
191
- Array.isArray(glob) && Data.uniformStringArray(glob) &&
192
- glob.length > 0
193
- )
194
- ),
195
- "glob must be a non-empty string or array of strings.",
196
- 1
197
- )
198
-
199
- const globbyArray = (
200
- typeof glob === "string"
201
- ? glob
202
- .split("|")
203
- .map(g => g.trim())
204
- .filter(Boolean)
205
- : glob
206
- ).map(g => File.fixSlashes(g))
207
-
208
- if(
209
- Array.isArray(globbyArray) &&
210
- Data.uniformStringArray(globbyArray) &&
211
- !globbyArray.length
212
- )
213
- throw Sass.new(
214
- `Invalid glob pattern: Array must contain only strings. Got ${JSON.stringify(glob)}`,
215
- )
216
-
217
- // Use Globby to fetch matching files
218
-
219
- const filesArray = await globby(globbyArray)
220
- const files = filesArray.map(file => new FileObject(file))
221
-
222
- // Flatten the result and remove duplicates
223
- return files
224
- }
225
-
226
- /**
227
- * Lists the contents of a directory.
228
- *
229
- * @param {string} directory - The directory to list.
230
- * @returns {Promise<{files: Array<FileObject>, directories: Array<DirectoryObject>}>} The files and
231
- * directories in the directory.
232
- */
233
- static async ls(directory) {
234
- const found = await fs.readdir(directory, {withFileTypes: true})
235
- const results = await Promise.all(
236
- found.map(async dirent => {
237
- const fullPath = path.join(directory, dirent.name)
238
- const stat = await fs.stat(fullPath)
239
-
240
- return {dirent, stat, fullPath}
241
- }),
242
- )
243
-
244
- const files = results
245
- .filter(({stat}) => stat.isFile())
246
- .map(({fullPath}) => new FileObject(fullPath))
247
-
248
- const directories = results
249
- .filter(({stat}) => stat.isDirectory())
250
- .map(({fullPath}) => new DirectoryObject(fullPath))
251
-
252
- return {files, directories}
253
- }
254
-
255
- /**
256
- * Reads the content of a file asynchronously.
257
- *
258
- * @param {FileObject} fileObject - The file map containing the file path
259
- * @returns {Promise<string>} The file contents
260
- */
261
- static async readFile(fileObject) {
262
- const filePath = fileObject.path
263
-
264
- if(!(await fileObject.exists))
265
- throw Sass.new(`No such file '${filePath}'`)
266
-
267
- if(!filePath)
268
- throw Sass.new("No absolute path in file map")
269
-
270
- return await fs.readFile(filePath, "utf8")
271
- }
272
-
273
- /**
274
- * Writes content to a file synchronously.
275
- *
276
- * @param {FileObject} fileObject - The file map containing the file path
277
- * @param {string} content - The content to write
278
- */
279
- static async writeFile(fileObject, content) {
280
- if(!fileObject.path)
281
- throw Sass.new("No absolute path in file")
282
-
283
- await fs.writeFile(fileObject.path, content, "utf8")
284
- }
285
-
286
- /**
287
- * Loads an object from JSON or YAML provided a fileMap
288
- *
289
- * @param {FileObject} fileObject - The FileObj file to load containing
290
- * JSON or YAML text.
291
- * @returns {object} The parsed data object.
292
- */
293
- static async loadDataFile(fileObject) {
294
- const content = await File.readFile(fileObject)
295
-
296
- try {
297
- return JSON5.parse(content)
298
- } catch {
299
- try {
300
- return YAML.parse(content)
301
- } catch {
302
- throw Sass.new(`Content is neither valid JSON nor valid YAML:\n'${fileObject.path}'`)
303
- }
304
- }
305
- }
306
-
307
- /**
308
- * Ensures a directory exists, creating it if necessary
309
- *
310
- * @async
311
- * @param {DirectoryObject} dirObject - The path or DirMap of the directory to assure exists
312
- * @param {object} [options] - Any options to pass to mkdir
313
- * @returns {Promise<boolean>} True if directory exists, false otherwise
314
- * @throws {Sass} If directory creation fails
315
- */
316
- static async assureDirectory(dirObject, options = {}) {
317
- if(await dirObject.exists)
318
- return true
319
-
320
- try {
321
- await fs.mkdir(dirObject.path, options)
322
- } catch(e) {
323
- throw Sass.new(`Unable to create directory '${dirObject.path}': ${e.message}`)
324
- }
325
-
326
- return dirObject.exists
327
- }
328
-
329
- /**
330
- * Computes the relative path from one file or directory to another.
331
- *
332
- * If the target is outside the source (i.e., the relative path starts with ".."),
333
- * returns the absolute path to the target instead.
334
- *
335
- * @param {FileObject|DirectoryObject} from - The source file or directory object
336
- * @param {FileObject|DirectoryObject} to - The target file or directory object
337
- * @returns {string} The relative path from `from` to `to`, or the absolute path if not reachable
338
- */
339
- static relativeOrAbsolutePath(from, to) {
340
- const relative = path.relative(from.path, to.path)
341
-
342
- return relative.startsWith("..")
343
- ? to.path
344
- : relative
345
- }
346
- }
package/src/FileObject.js DELETED
@@ -1,226 +0,0 @@
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
- }