diff --git a/README.md b/README.md index 7bb3527..cdd0bd0 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ [![Version](https://img.shields.io/npm/v/lsfnd?logo=npm&label=lsfnd)](https://npmjs.com/package/lsfnd) ![Min. Node](https://img.shields.io/node/v-lts/lsfnd/latest?logo=node.js&label=node) -[![Bundle size (minified)](https://img.shields.io/bundlephobia/min/lsfnd)](https://npmjs.com/package/lsfnd)
+[![Bundle size (minified)](https://img.shields.io/bundlephobia/min/lsfnd)](https://npmjs.com/package/lsfnd)
[![Test CI](https://github.com/mitsuki31/lsfnd/actions/workflows/test.yml/badge.svg)](https://github.com/mitsuki31/lsfnd/actions/workflows/test.yml) -[![License](https://img.shields.io/github/license/mitsuki31/lsfnd?logo=github&logoColor=f9f9f9&label=License&labelColor=yellow&color=white)](https://github.com/mitsuki31/lsfnd/tree/master/LICENSE) +[![License](https://img.shields.io/github/license/mitsuki31/lsfnd?logo=readme&logoColor=f9f9f9&label=License&labelColor=yellow&color=white)](https://github.com/mitsuki31/lsfnd/tree/master/LICENSE) **LSFND** is an abbreviation for _list (ls) files (f) and (n) directories (d)_, a lightweight Node.js library designed to make listing files and directories more convenient. @@ -12,13 +12,22 @@ It offers an efficient and simple way to explore through your directory structur and retrieves the names of files and/or directories leveraging a configurable options to modify the listing behavior, such as recursive searches and regular expression filters. -This library's **primary benefit** is that every implemented API runs asynchronously, +This library's **primary benefit** is that every implemented API within main module runs asynchronously, guaranteeing that they will **NEVER** disrupt the execution of any other processes. -> [!IMPORTANT]\ -> Currently this library only focus on CommonJS (CJS) and ECMAScript Modules (ESM). +> [!IMPORTANT] > -> As of version 1.0.0, this library has supported TypeScript projects with various +> ### v1.2.0 +> Added synchronous version for `ls`, `lsFiles`, and `lsDirs`. +> Can be imported from submodule `/sync` as such below: +> ```js +> const { lsFiles } = require('lsfnd/sync'); +> // Or: +> import { lsFiles } from 'lsfnd/sync'; +> ``` +> +> ### v1.0.0 +> This library has supported TypeScript projects with various > module types (i.e., `node16`, `es6`, and many more). Previously, it was only supports > TypeScript projects with module type of `commonjs`. All type declarations in this > library also has been enhanced to more robust and strict, thus improving type safety. diff --git a/build.prop.js b/build.prop.js index 0cdb1d1..a27e2e5 100644 --- a/build.prop.js +++ b/build.prop.js @@ -5,6 +5,7 @@ module.exports = { files: [ 'dist/index.js', 'dist/lsfnd.js', + 'dist/lsfnd-sync.js', 'dist/lsTypes.js' ] }, diff --git a/package.json b/package.json index bd156ae..df4088c 100644 --- a/package.json +++ b/package.json @@ -14,19 +14,21 @@ "exports": { ".": "./dist/index.js", "./lsfnd": "./dist/lsfnd.js", + "./sync": "./dist/lsfnd-sync.js", "./types": "./types/index.d.ts", "./package.json": "./package.json" }, "scripts": { "dev": "tsx scripts/build.ts", "build": "tsx scripts/build.ts --minify", + "postbuild": "tsx scripts/postbuild.ts", "docs": "npm run-script build:docs", "build:docs": "typedoc --options typedoc.config.js", "test": "npm run test:cjs && npm run test:mjs", - "test:cjs": "node test/lsfnd.spec.cjs", - "test:mjs": "node test/lsfnd.spec.mjs", + "test:cjs": "node test/lsfnd.spec.cjs && node test/lsfnd-sync.spec.cjs", + "test:mjs": "node test/lsfnd.spec.mjs && node test/lsfnd-sync.spec.mjs", "prepublishOnly": "npm run build", - "prepack": "npm test" + "prepack": "npm test && npm pkg delete devDependencies peerDependencies" }, "repository": { "type": "git", diff --git a/scripts/postbuild.ts b/scripts/postbuild.ts new file mode 100644 index 0000000..c8b7537 --- /dev/null +++ b/scripts/postbuild.ts @@ -0,0 +1,71 @@ +import * as fs from 'node:fs'; +import * as buildProp from '../build.prop'; + +const includedFiles = buildProp.minify.files; + +/** + * Remove consecutive multiline comments (/* ... *\/) immediately after the + * first `"use strict"` or `'use strict'` directive in the given JS source. + * + * @param source - JavaScript source text + * @returns modified source + */ +function removeMultilineCommentsAfterUseStrict(source: string) { + // Find first "use strict" or 'use strict' + const m = /(['"])use strict\1\s*;?/.exec(source); + if (!m) return source; + + // index right after the matched directive + let pos = m.index + m[0].length; + + // Walk forward skipping whitespace/newlines and removing /* ... */ blocks + let changed = false; + while (true) { + // Skip whitespace and newlines + const wsMatch = /^[\t\v\f\r\n ]+/.exec(source.slice(pos)); + if (wsMatch) pos += wsMatch[0].length; + + // If next chars start a block comment, remove it + if (source.startsWith('/*', pos)) { + const end = source.indexOf('*/', pos + 2); + if (end === -1) { + // unterminated block comment — be conservative: stop + break; + } + // Remove from pos to end+2 + source = source.slice(0, pos) + source.slice(end + 2); + changed = true; + // continue loop from same pos (since content changed and there may be more) + continue; + } + break; // else nothing to remove; break + } + + return changed ? source : source; +} + +async function run() { + const modifiedFiles = includedFiles.reduce((acc, val) => { + acc[val] = false; + return acc; + }, {} as Record<(typeof includedFiles)[number], boolean>); + + const postbuildPromises = includedFiles.map(async file => { + const raw = await fs.promises.readFile(file, 'utf8'); + + // Remove comments + const modified = removeMultilineCommentsAfterUseStrict(raw); + if (modified !== raw) { + await fs.promises.writeFile(file, modified, 'utf8'); + modifiedFiles[file] = true; + } + }); + + void await Promise.all(postbuildPromises); + + // Summary + console.log('[postbuild] Modified files are included:'); + console.table(modifiedFiles); +} + +run(); diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..8ad43b4 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,34 @@ +import type { DefaultLsOptions } from '../types'; + +/** + * A regular expression pattern to parse the file URL path, + * following the WHATWG URL Standard. + * + * @see {@link https://url.spec.whatwg.org/ WHATWG URL Standard} + * @internal + */ +export const FILE_URL_PATTERN: RegExp = /^file:\/\/\/?(?:[A-Za-z]:)?(?:\/[^\s\\]+)*(?:\/)?/; + +/** + * A regular expression pattern to parse and detect the Windows path. + * + * @internal + */ +export const WIN32_PATH_PATTERN: RegExp = /^[A-Za-z]:?(?:\\|\/)(?:[^\\/:*?"<>|\r\n]+(?:\\|\/))*[^\\/:*?"<>|\r\n]*$/; + +/** + * An object containing all default values of {@link LsOptions `LsOptions`} type. + * + * @since 1.0.0 + * @see {@link DefaultLsOptions} + * @see {@link LsOptions} + */ +export const defaultLsOptions: DefaultLsOptions = { + encoding: 'utf8', + recursive: false, + match: /.+/, + exclude: undefined, + rootDir: process.cwd(), + absolute: false, + basename: false +} satisfies DefaultLsOptions; diff --git a/src/index.ts b/src/index.ts index f593772..8c7c383 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,3 +22,4 @@ export type { LsEntries, LsResult } from '../types'; +export { defaultLsOptions } from './constants'; diff --git a/src/lsfnd-sync.ts b/src/lsfnd-sync.ts new file mode 100644 index 0000000..277d32a --- /dev/null +++ b/src/lsfnd-sync.ts @@ -0,0 +1,357 @@ +/** + * A module that offers some functions to read and list files and/or directories + * in a specified directory with support filtering using regular expression pattern. + * + * @module lsfnd-sync + * @author Ryuu Mitsuki (https://github.com/mitsuki31) + * @since 1.2.0 + * @license MIT + */ + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { LsEntries, LsOptions, LsResult, LsTypes, StringPath } from '../types'; +import { lsTypes } from './lsTypes'; +import { + checkType, + encodeTo, + resolveFileURL, + resolveMatchExclude, + resolveOptions, +} from './utils'; + +export * from './lsTypes'; // Re-export the `lsTypes` enum here + +/** + * **Synchronously** lists files and directories in a specified directory path, filtering by a + * regular expression pattern. + * + * The returned entries are configurable using the additional {@link LsOptions options}, + * such as listing recursively to subdirectories, and filter specific file and/or + * directory names using a regular expression. + * + * The additional `options` can be an object or a regex pattern to specify only + * the {@link LsOptions.match match} field. If passed as a `RegExp` object, the rest + * options (except the `match` field) for reading the directory will uses default options. + * + * If the `options` argument not specified (or `undefined`), then it uses the + * default value: + * ```js + * [LsOptions]: { + * encoding: 'utf8', + * recursive: false, + * match: /.+/, + * exclude: undefined + * } + * ``` + * + *
+ *
+ * History + * + * ### 1.2.0 + * Added in version 1.2.0. + * + *
+ * + * @param dirpath - The directory path to search, must be a **Node** path + * (i.e., similar to POSIX path) or a valid file URL path. + * @param options - Additional options for reading the directory. Refer to + * {@link LsOptions} documentation for more details. + * @param type - A type to specify the returned file system type to be included. + * If not specified or set to `0`, then it will includes all types + * (including regular files and directories). + * See {@link lsTypes} to check all supported types. + * + * @returns An array of string representing the entries result excluding `'.'` and `'..'` + * or an empty array (`[]`) if any files and directories does not match with the specified filter options. + * + * @throws {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error **Error**} - + * If there is an error occurred while reading a directory. + * @throws {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/URIError **URIError**} - + * If the given URL path contains invalid file URL scheme or using + * unsupported protocols. + * + * @since 1.2.0 + * @see {@link lsFiles} + * @see {@link lsDirs} + * @see {@link lsTypes} + */ +export function ls( + dirpath: StringPath | URL, + options?: LsOptions | RegExp | undefined, + type?: LsTypes | undefined +): LsResult { + let absdirpath: StringPath; + let reldirpath: StringPath; + const lsTypesValues = Object.fromEntries(Object.entries(lsTypes)); + + // Resolve the options + if (!( + options instanceof RegExp || + (!options || (typeof options === 'object' && !Array.isArray(options))))) + { + throw new TypeError("Unknown type of 'options': " + + (Array.isArray(options) ? 'array' : typeof options)); + } + + const resOptions = resolveOptions(options); + const { encoding: usedEncoding } = resOptions; + let { match: _match, exclude: _exclude } = resOptions; + const match = resolveMatchExclude(_match); + const exclude = _exclude ? resolveMatchExclude(_exclude) : undefined; + + if (!(dirpath instanceof URL) && typeof dirpath !== 'string') { + throw new TypeError('Unknown type, expected a string or a URL object'); + } + + if (dirpath instanceof URL) { + if (dirpath.protocol !== 'file:') { + throw new URIError(`Unsupported protocol: '${dirpath.protocol}'`); + } + // We need to use `fileURLToPath` to ensure it converted to string path + // correctly on Windows platform, after that replace all Windows path separator ('\') + // with POSIX path separator ('/'). + dirpath = fileURLToPath(dirpath).replaceAll(/\\/g, '/'); + } else if (typeof dirpath === 'string' && /^[a-zA-Z]+:/.test(dirpath)) { + dirpath = resolveFileURL(dirpath); + } + + // Normalize the given path + dirpath = path.normalize(dirpath); + + // Check and resolve the `rootDir` option + if (resOptions.rootDir instanceof URL) { + if (resOptions.rootDir.protocol !== 'file:') { + throw new URIError(`Unsupported protocol: '${resOptions.rootDir.protocol}'`); + } + resOptions.rootDir = fileURLToPath(resOptions.rootDir).replaceAll(/\\/g, '/'); + } else if (typeof dirpath === 'string' && /^[a-zA-Z]+:/.test(resOptions.rootDir!)) { + resOptions.rootDir = resolveFileURL(resOptions.rootDir!); + } + + // Resolve the absolute and relative of the dirpath argument + absdirpath = path.isAbsolute(dirpath) + ? dirpath + : path.posix.resolve(dirpath); + reldirpath = path.relative(resOptions.rootDir ?? process.cwd(), absdirpath);; + + // Check the type argument + checkType(type, [ ...Object.keys(lsTypes), ...Object.values(lsTypes), 0, null, undefined ]); + + let result: LsResult = null; + try { + // Read the specified directory path recursively + const entries: LsEntries = fs.readdirSync(absdirpath, { + encoding: usedEncoding, + recursive: Boolean(resOptions.recursive) + }); + // Declare the copy of the entries with UTF-8 encoding to be used by `fs.stat`, + // this way we prevent the error due to invalid path thrown by `fs.stat` itself. + const utf8Entries: LsEntries = encodeTo(entries, usedEncoding, 'utf8'); + + // Filter the entries + result = utf8Entries.map(function (entry: StringPath): StringPath | null { + entry = path.join(absdirpath, entry); + let stats: fs.Stats | undefined; + let resultType: boolean = false, + isDir: boolean = false, + isFile: boolean = false; + + // Try to retrieve the information of file system using `fs.statSync` + try { + stats = fs.statSync(entry); + } catch (e: unknown) { + // Attempt to open the entry using `fs.opendir` if the file system could not be + // accessed because of a permission error or maybe access error. The function + // is meant to be used with directories exclusively, which is helpful for + // determining if an entry is a directory or a regular file. We can conclude that + // the entry is a regular file if it throws an error. In this method, we can + // avoid an internal error that occurs when try to access a read-protected file system, + // such the "System Volume Information" directory on all Windows drives. + try { + const dir = fs.opendirSync(entry); + isDir = true; // Detected as a directory + dir.close(); + } catch (eDir: unknown) { + if (eDir instanceof Error) { + if ('code' in eDir && eDir.code === 'ENOTDIR') { + isFile = true; // Detected as a regular file + } else { + eDir.cause = e; + throw eDir; + } + } + } + } + + switch (type ?? lsTypes.LS_A) { + case lsTypes.LS_D: + case lsTypesValues[String(lsTypes.LS_D)]: + resultType = ( + !(stats?.isFile() || isFile) + && (stats?.isDirectory() || isDir) + ); + break; + case lsTypes.LS_F: + case lsTypesValues[String(lsTypes.LS_F)]: + resultType = ( + (stats?.isFile() || isFile) + && !(stats?.isDirectory() || isDir) + ); + break; + case lsTypes.LS_A: + case lsTypesValues[String(lsTypes.LS_A)]: + case (0 as lsTypes): // Special case + resultType = ( + (stats?.isFile() || isFile) + || (stats?.isDirectory() || isDir) + ); + break; + default: + throw new TypeError(`Unknown value of 'type': ${type}`); + } + + return (( + resultType && (match.test(entry) && (exclude ? !exclude.test(entry) : true)) + ) + ? ( + // *** High priority + (resOptions.absolute && (resOptions.basename || !resOptions.basename)) + ? entry // already an absolute path + // *** Medium priority + : (!resOptions.absolute && resOptions.basename) + ? path.basename(entry) // get its basename + // *** Low priority + // convert back to the relative path + : path.join(reldirpath, path.relative(absdirpath, entry)) + ) + : null + ) + }) + // Remove any null entries + .filter((entry): entry is string => !!entry!); + } catch (err: unknown) { + if (err instanceof Error) throw err; + } + + // Encode back the entries to the specified encoding + if (result && usedEncoding !== 'utf8') { + result = encodeTo(result, 'utf8', usedEncoding); + } + return (result ? result.sort() : result); +} + +/** + * **Synchronously** lists files in the specified directory path, filtering by a regular + * expression pattern. + * + * The returned entries are configurable using the additional {@link LsOptions options}, + * such as listing recursively to subdirectories, and filter specific file names + * using a regular expression. + * + * The additional `options` can be an object or a regex pattern to specify only + * the {@link LsOptions.match match} field. If passed as a `RegExp` object, the rest + * options (except the `match` field) for reading the directory will uses default options. + * + * If the `options` argument not specified (or `undefined`), then it uses the + * default value: + * ```js + * [LsOptions]: { + * encoding: 'utf8', + * recursive: false, + * match: /.+/, + * exclude: undefined + * } + * ``` + * + *
+ *
+ * History + * + * ### 1.2.0 + * Added in version 1.2.0. + * + *
+ * + * @param dirpath - The directory path to search, must be a **Node** path + * (i.e., similar to POSIX path) or a valid file URL path. + * @param options - Additional options for reading the directory. Refer to + * {@link LsOptions} documentation for more details. + * + * @returns An array of string representing the entries result excluding `'.'` and `'..'` +* or an empty array (`[]`) if any files does not match with the specified filter options. + * @throws {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error **Error**} - + * If there is an error occurred while reading a directory. + * @throws {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/URIError **URIError**} - + * If the given URL path contains invalid file URL scheme or using + * unsupported protocols. + * + * @since 1.2.0 + * @see {@link ls} + * @see {@link lsDirs} + */ +export function lsFiles( + dirpath: StringPath | URL, + options?: LsOptions | RegExp | undefined +): LsResult { + return ls(dirpath, options, lsTypes.LS_F); +} + +/** + * **Synchronously** lists directories in the specified directory path, filtering by a regular + * expression pattern. + * + * The returned entries are configurable using the additional {@link LsOptions options}, + * such as listing recursively to subdirectories, and filter specific directory names + * using a regular expression. + * + * The additional `options` can be an object or a regex pattern to specify only + * the {@link LsOptions.match match} field. If passed as a `RegExp` object, the rest + * options (except the `match` field) for reading the directory will uses default options. + * + * If the `options` argument not specified (or `undefined`), then it uses the + * default value: + * ```js + * [LsOptions]: { + * encoding: 'utf8', + * recursive: false, + * match: /.+/, + * exclude: undefined + * } + * ``` + * + *
+ *
+ * History + * + * ### 1.2.0 + * Added in version 1.2.0. + * + *
+ * + * @param dirpath - The directory path to search, must be a **Node** path + * (i.e., similar to POSIX path) or a valid file URL path. + * @param options - Additional options for reading the directory. Refer to + * {@link LsOptions} documentation for more details. + * + * @returns An array of string representing the entries result excluding `'.'` and `'..'` + * or an empty array (`[]`) if any directories does not match with the specified filter options. + * + * @throws {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error **Error**} - + * If there is an error occurred while reading a directory. + * @throws {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/URIError **URIError**} - + * If the given URL path contains invalid file URL scheme or using + * unsupported protocols. + * + * @since 1.2.0 + * @see {@link ls} + * @see {@link lsFiles} + */ +export function lsDirs( + dirpath: StringPath | URL, + options?: LsOptions | RegExp | undefined +): LsResult { + return ls(dirpath, options, lsTypes.LS_D); +} diff --git a/src/lsfnd.ts b/src/lsfnd.ts index bbf78e4..0bcf2f4 100644 --- a/src/lsfnd.ts +++ b/src/lsfnd.ts @@ -2,8 +2,6 @@ * A module that offers some functions to read and list files and/or directories * in a specified directory with support filtering using regular expression pattern. * - * Copyright (c) 2024 Ryuu Mitsuki. All rights reserved. - * * @module lsfnd * @author Ryuu Mitsuki (https://github.com/mitsuki31) * @since 0.1.0 @@ -11,316 +9,27 @@ */ import * as fs from 'node:fs'; -import * as os from 'node:os'; import * as path from 'node:path'; import { URL, fileURLToPath } from 'node:url'; import type { - DefaultLsOptions, LsEntries, LsOptions, LsResult, LsTypes, - ResolvedLsOptions, StringPath } from '../types'; import { lsTypes } from './lsTypes'; - -type Unpack = A extends Array<(infer U)> ? U : A; - -/** - * A regular expression pattern to parse the file URL path, - * following the WHATWG URL Standard. - * - * @see {@link https://url.spec.whatwg.org/ WHATWG URL Standard} - * @internal - */ -const FILE_URL_PATTERN: RegExp = /^file:\/\/\/?(?:[A-Za-z]:)?(?:\/[^\s\\]+)*(?:\/)?/; - -/** - * A regular expression pattern to parse and detect the Windows path. - * - * @internal - */ -const WIN32_PATH_PATTERN: RegExp = /^[A-Za-z]:?(?:\\|\/)(?:[^\\/:*?"<>|\r\n]+(?:\\|\/))*[^\\/:*?"<>|\r\n]*$/; - -/** - * An object containing all default values of {@link LsOptions `LsOptions`} type. - * - * @since 1.0.0 - * @see {@link DefaultLsOptions} - * @see {@link LsOptions} - */ -export const defaultLsOptions: DefaultLsOptions = { - encoding: 'utf8', - recursive: false, - match: /.+/, - exclude: undefined, - rootDir: process.cwd(), - absolute: false, - basename: false -} as const; - -/** - * Converts a file URL to a file path. - * - * This function is similar to Node.js' - * [`url.fileURLToPath`](https://nodejs.org/api/url.html#urlfileurltopathurl) - * function, but with added support for relative file paths (e.g., `"file:./foo"`). - * If the input URL does not adhere to the file URL scheme or if it contains - * unsupported formats, such as providing unsupported protocols or invalid path - * structures, an error will be thrown. - * - * @param url - The file URL to convert. It can be either an instance of `URL` - * or a string representing a file URL and must starts with `"file:"` - * protocol. - * @returns A string representing the corresponding file path. - * @throws {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/URIError **URIError**} - - * If the URL is not a valid file URL or if it contains unsupported formats. - * - * @example - * // Convert a file URL to a file path - * const filePath = fileUrlToPath('file:///path/to/file.txt'); - * console.log(filePath); // Output: "/path/to/file.txt" - * - * @example - * // Handle relative file paths - * const filePath = fileUrlToPath('file:./path/to/file.txt'); - * console.log(filePath); // Output: "./path/to/file.txt" - * - * @since 1.0.0 - * @see {@link https://nodejs.org/api/url.html#urlfileurltopathurl url.fileURLToPath} - * - * @internal - * @deprecated - */ -function fileUrlToPath(url: URL | StringPath): StringPath { - if ((url instanceof URL && url.protocol !== 'file:') - || (typeof url === 'string' && !/^file:(\/\/?|\.\.?\/*)/.test(url))) { - throw new URIError('Invalid URL file scheme'); - } - return (url instanceof URL) - ? fileURLToPath(url).replaceAll(/\\/g, '/') - : url.replace(/^file:/, ''); -} - -/** - * Checks if the given string path is a Windows path. - * - * Before checking, the given path will be normalized first. - * - * @param p - The string path to be checked for. - * @returns `true` if the given path is a Windows path, `false` otherwise. - * @see {@link WIN32_PATH_PATTERN} - * - * @internal - */ -function isWin32Path(p: StringPath): boolean { - p = path.normalize(p); - return !!p && WIN32_PATH_PATTERN.test(p); -} - -/** - * Resolves a file URL to a file path. - * - * @param {StringPath} p - * The file URL to resolve. It should be a string representing - * a valid file URL following the **WHATWG URL Standard**. - * @returns {StringPath} - * The resolved file path. If the provided URL is valid, - * it returns the corresponding file path. - * @throws {URIError} - * If the provided file URL scheme is invalid. This can occur - * if the URL scheme is not recognized or if it does not conform - * to the expected format. - * - * @remarks - * This function is used to convert a file URL to a file path. It first checks - * if the provided URL matches the expected pattern for file URLs. If it does, - * it proceeds to resolve the URL to a file path. If the URL scheme is not recognized - * or is invalid, a `URIError` is thrown. - * - * If the provided URL is `'file://'` or `'file:///'`, it is replaced with the root directory - * path (in the current drive for Windows systems). Otherwise, the URL is parsed using the - * `fileURLToPath` function. - * - * If the operating system is not Windows and the provided URL contains a Windows-style path, - * or if the operating system is Windows and the URL does not start with 'file:', an error is - * thrown indicating an invalid file URL scheme. - * - * @example - * // POSIX Path - * const fooPath = resolveFileURL('file:///path/to/foo.txt'); - * console.log(filePath); // Output: '/path/to/foo.txt' - * - * @example - * // Windows Path - * const projectsPath = resolveFileURL('file:///G:/Projects'); - * console.log(projectsPath); // Output: 'G:\\Projects' - * - * @see {@link https://url.spec.whatwg.org/ WHATWG URL Standard} - * @internal - */ -function resolveFileURL(p: StringPath): StringPath { - if (FILE_URL_PATTERN.test(p)) { - // If and only if the given path is 'file://' or 'file:///' - // then replace the path to root directory (in current drive for Windows systems). - // When the specified above URL path being passed to `fileURLPath` function, - // it throws an error due to non-absolute URL path was given. - if (/^file:(?:\/\/\/?)$/.test(p)) p = '/'; - // Otherwise, parse the file URL path - else p = fileURLToPath(p); - } else if ((os.platform() !== 'win32' - && (isWin32Path(p) || !p.startsWith('file:'))) - || (os.platform() === 'win32' - && !(isWin32Path(p) || p.startsWith('file:')))) { - throw new URIError('Invalid file URL scheme'); - } - return p; -} - -/** - * Checks if a provided type matches any of the allowed types. - * - * This function verifies if a provided `type` argument matches any of the - * allowed types specified in the `validTypes` array. It throws a `TypeError` - * if the `type` doesn't match any valid type. - * - * @param type - The type value to be checked. - * @param validTypes - An array containing the allowed types for the `type` parameter. - * - * @throws {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/TypeError **TypeError**} - - * If the provided `type` doesn't match any of the valid types. - * - * @since 1.0.0 - * @internal - */ -function checkType( - type: LsTypes | N, - validTypes: Array<(string | number | N)> -): void { - function joinAll(arr: (typeof validTypes), delim: string): string { - let str: string = ''; - arr.forEach((e: Unpack<(typeof validTypes)>, i: number) => { - if (i > 0 && i <= arr.length - 1) str += delim; - str += (typeof e === 'string') ? `'${e}'` - : (e === null) ? 'null' - : (typeof e === 'undefined') ? 'undefined' : e; - }); - return str; - } - - let match: boolean = false; - validTypes.forEach((validType: Unpack<(typeof validTypes)>) => { - if (!match && type === validType) match = true; - }); - - if (!match) { - throw new TypeError( - `Invalid 'type' value of ${ type} ('${typeof type}'). Valid type is "${ - joinAll(validTypes.sort(), ' | ') - }"`); - } - return; -} - -/** - * Resolves the given `options` ({@link LsOptions}). - * - * @param options - An object represents the options to be resolved. Set to `null` - * or `undefined` to gets the default options. - * @returns A new object represents the resolved options. Returns the default - * options if the `options` parameter not specified or `null`. - * - * @since 1.0.0 - * @internal - */ -function resolveOptions(options: LsOptions | null | undefined): ResolvedLsOptions { - return > (!options ? defaultLsOptions : { - encoding: options?.encoding || defaultLsOptions.encoding, - recursive: options?.recursive || defaultLsOptions.recursive, - match: options?.match || defaultLsOptions.match, - exclude: options?.exclude || defaultLsOptions.exclude, - rootDir: options?.rootDir || defaultLsOptions.rootDir, - absolute: options?.absolute || defaultLsOptions.absolute, - basename: options?.basename || defaultLsOptions.basename - }); -} - -/** - * Encodes a string or an array of strings from one encoding to another. - * - * This function offers simplicity and flexibility by allowing encoding conversion - * between different encodings for either a string or a set of strings. - * - * @param val - The string to encode. - * @param from - The encoding of the input string. - * @param to - The encoding to convert the string to. - * - * @returns The encoded string. - * - * @throws {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/TypeError **TypeError**} - - * If the `from` or `to` encoding is unknown, or if the input value is - * neither a string nor an array of strings. - * - * @example - * Encode a string to 'base64' encoding: - * ```js - * const encodedString = encodeTo('Hello, world!', 'utf8', 'base64'); - * console.log(encodedString); - * // Output: 'SGVsbG8sIHdvcmxkIQ==' - * ``` - * - * Encode an array of strings to 'hex' encoding: - * ```js - * const encodedArray = encodeTo(['Hello', 'world'], 'utf8', 'hex'); - * console.log(encodedArray); - * // Output: ['48656c6c6f', '776f726c64'] - * ``` - * - * @since 1.0.0 - * @internal - */ -function encodeTo( - val: string, - from: BufferEncoding, - to: BufferEncoding -): string; -/** - * @param val - The array of strings to encode. - * @param from - The encoding of the input strings. - * @param to - The encoding to convert the strings to. - * - * @returns The array of encoded strings. - */ -function encodeTo( - val: Array, - from: BufferEncoding, - to: BufferEncoding -): Array; - -function encodeTo( - val: string | Array, - from: BufferEncoding, - to: BufferEncoding -): string | Array { - const { isEncoding } = Buffer; - if (!isEncoding(from)) throw new TypeError("Unknown 'from' encoding: " + from); - else if (!isEncoding(to)) throw new TypeError("Unknown 'to' encoding: " + to); - else if (!(typeof val === 'string' || Array.isArray(val))) { - throw new TypeError('Expected a string or an array of string'); - } - - if (typeof val === 'string') { - return Buffer.from(val, from).toString(to); - } - - return (> val).map(function (v: string): string { - return Buffer.from(v, from).toString(to); - }); -} +import { + type Unpack, + resolveOptions, + resolveMatchExclude, + resolveFileURL, + checkType, + encodeTo, +} from './utils'; /** - * Lists files and/or directories in a specified directory path, filtering by a + * Lists files and directories in a specified directory path, filtering by a * regular expression pattern. * * The returned entries are configurable using the additional {@link LsOptions options}, @@ -398,9 +107,25 @@ export async function ls( options?: LsOptions | RegExp | undefined, type?: LsTypes | undefined ): Promise { - let absdirpath: StringPath, - reldirpath: StringPath; - + let absdirpath: StringPath; + let reldirpath: StringPath; + const lsTypesValues = Object.fromEntries(Object.entries(lsTypes)); + + // Resolve the options + if (!( + options instanceof RegExp || + (!options || (typeof options === 'object' && !Array.isArray(options))))) + { + throw new TypeError("Unknown type of 'options': " + + (Array.isArray(options) ? 'array' : typeof options)); + } + + const resOptions = resolveOptions(options); + const { encoding: usedEncoding } = resOptions; + let { match: _match, exclude: _exclude } = resOptions; + const match = resolveMatchExclude(_match); + const exclude = _exclude ? resolveMatchExclude(_exclude) : undefined; + if (!(dirpath instanceof URL) && typeof dirpath !== 'string') { throw new TypeError('Unknown type, expected a string or a URL object'); } @@ -420,54 +145,41 @@ export async function ls( // Normalize the given path dirpath = path.normalize(dirpath); - if (options instanceof RegExp) { - // Store the regex value of `options` to temporary variable for `match` option - const temp: RegExp = new RegExp(options.source) || options; - options = resolveOptions(null); // Use the default options - ( options)!.match = temp; // Reassign the `match` field - } else if (!options || (typeof options === 'object' && !Array.isArray(options))) { - // Resolve the options, even it is not specified - options = resolveOptions(options); - } else { - throw new TypeError("Unknown type of 'options': " - + (Array.isArray(options) ? 'array' : typeof options)); - } - // Check and resolve the `rootDir` option - if (options.rootDir instanceof URL) { - if (options.rootDir.protocol !== 'file:') { - throw new URIError(`Unsupported protocol: '${options.rootDir.protocol}'`); + if (resOptions.rootDir instanceof URL) { + if (resOptions.rootDir.protocol !== 'file:') { + throw new URIError(`Unsupported protocol: '${resOptions.rootDir.protocol}'`); } - options.rootDir = fileURLToPath(options.rootDir).replaceAll(/\\/g, '/'); - } else if (typeof dirpath === 'string' && /^[a-zA-Z]+:/.test(options.rootDir!)) { - options.rootDir = resolveFileURL(options.rootDir!); + resOptions.rootDir = fileURLToPath(resOptions.rootDir).replaceAll(/\\/g, '/'); + } else if (typeof dirpath === 'string' && /^[a-zA-Z]+:/.test(resOptions.rootDir!)) { + resOptions.rootDir = resolveFileURL(resOptions.rootDir!); } // Resolve the absolute and relative of the dirpath argument - absdirpath = path.isAbsolute( dirpath) - ? dirpath - : path.posix.resolve( dirpath); - reldirpath = path.relative(options.rootDir! || process.cwd(), absdirpath);; + absdirpath = path.isAbsolute(dirpath) + ? dirpath + : path.posix.resolve(dirpath); + reldirpath = path.relative(resOptions.rootDir ?? process.cwd(), absdirpath);; // Check the type argument - checkType(type!, [ ...Object.values(lsTypes), 0, null, undefined ]); + checkType(type, [ ...Object.values(lsTypes), 0, null, undefined ]); let result: LsResult = null; try { // Read the specified directory path recursively const entries: LsEntries = await fs.promises.readdir(absdirpath, { - encoding: options?.encoding || 'utf8', - recursive: options?.recursive + encoding: usedEncoding, + recursive: Boolean(resOptions.recursive) }); // Declare the copy of the entries with UTF-8 encoding to be used by `fs.stat`, // this way we prevent the error due to invalid path thrown by `fs.stat` itself. - const utf8Entries: LsEntries = encodeTo(entries, options?.encoding!, 'utf8'); + const utf8Entries: LsEntries = encodeTo(entries, usedEncoding, 'utf8'); // Filter the entries result = await Promise.all( - utf8Entries.map(async function (entry: StringPath): Promise<(StringPath | null)> { + utf8Entries.map(async function (entry: StringPath): Promise { entry = path.join(absdirpath, entry); - let stats: fs.Stats | null = null; + let stats: fs.Stats | undefined; let resultType: boolean = false, isDir: boolean = false, isFile: boolean = false; @@ -492,46 +204,55 @@ export async function ls( } catch (eDir: unknown) { // If and only if the thrown error have a code "ENOTDIR", // then it treats the entry as a regular file. Otherwise, throw the error. - if (eDir instanceof Error && ('code' in eDir && eDir.code === 'ENOTDIR')) - isFile = true; // Detected as a regular file - else throw eDir; + if (eDir instanceof Error) { + if ('code' in eDir && eDir.code === 'ENOTDIR') { + isFile = true; // Detected as a regular file + } else { + eDir.cause = e; + throw eDir; + } + } } } - switch (type) { + // Don't worry about nullable values here, it will fallback to `LS_A`. + // Otherwise, any non-nullable values (not including from `lsTypes`) will throwing an error. + switch (type ?? lsTypes.LS_A) { case lsTypes.LS_D: - case 'LS_D': + case lsTypesValues[String(lsTypes.LS_D)]: resultType = ( !(stats?.isFile() || isFile) && (stats?.isDirectory() || isDir) ); break; case lsTypes.LS_F: - case 'LS_F': + case lsTypesValues[String(lsTypes.LS_F)]: resultType = ( (stats?.isFile() || isFile) && !(stats?.isDirectory() || isDir) ); break; - default: + case lsTypes.LS_A: + case lsTypesValues[String(lsTypes.LS_A)]: + case (0 as lsTypes): // Special case resultType = ( (stats?.isFile() || isFile) || (stats?.isDirectory() || isDir) ); + break; + default: + throw new TypeError(`Unknown value of 'type': ${type}`); } return (( - resultType && ( - ( options.match).test(entry) - && (options.exclude ? !( options.exclude).test(entry) : true) - ) + resultType && (match.test(entry) && (exclude ? !exclude.test(entry) : true)) ) ? ( // *** High priority - (options.absolute && (options.basename || !options.basename)) + (resOptions.absolute && (resOptions.basename || !resOptions.basename)) ? entry // already an absolute path // *** Medium priority - : (!options.absolute && options.basename) + : (!resOptions.absolute && resOptions.basename) ? path.basename(entry) // get its basename // *** Low priority // convert back to the relative path @@ -541,20 +262,21 @@ export async function ls( ) }) ).then(function (results: (Unpack | null)[]): LsEntries { - return results.filter( + return results.filter( function (entry: Unpack<(typeof results)>): boolean { return !!entry!; // Remove any null entries } - ); + ) as LsEntries; }); } catch (err: unknown) { if (err instanceof Error) throw err; } // Encode back the entries to the specified encoding - if (result && options?.encoding! !== 'utf8') - result = encodeTo(result, 'utf8', options.encoding!); - return (!!result ? result.sort() : result); + if (result && usedEncoding !== 'utf8') { + result = encodeTo(result, 'utf8', usedEncoding); + } + return (result ? result.sort() : result); } /** @@ -602,7 +324,7 @@ export async function ls( * * @returns A promise that resolves with an array of string representing the * entries result excluding `'.'` and `'..'` or an empty array (`[]`) - * if any files and directories does not match with the specified filter options. + * if any files does not match with the specified filter options. * @throws {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error **Error**} - * If there is an error occurred while reading a directory. * @throws {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/URIError **URIError**} - @@ -631,7 +353,7 @@ export async function lsFiles( } /** - * Lists files in the specified directory path, filtering by a regular + * Lists directories in the specified directory path, filtering by a regular * expression pattern. * * The returned entries are configurable using the additional {@link LsOptions options}, @@ -675,7 +397,7 @@ export async function lsFiles( * * @returns A promise that resolves with an array of string representing the * entries result excluding `'.'` and `'..'` or an empty array (`[]`) - * if any files and directories does not match with the specified filter options. + * if any directories does not match with the specified filter options. * @throws {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error **Error**} - * If there is an error occurred while reading a directory. * @throws {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/URIError **URIError**} - diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..a79742e --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,286 @@ +/** + * Utility module for LSFND. + * + * @module utils + * @author Ryuu Mitsuki (https://github.com/mitsuki31) + * @since 1.2.0 + * @license MIT + */ + +import * as os from 'node:os'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { StringPath, LsTypes, LsOptions, ResolvedLsOptions } from '../types'; +import { defaultLsOptions, FILE_URL_PATTERN, WIN32_PATH_PATTERN } from './constants'; + +export type Unpack = A extends (infer U)[] ? U : A; + +/** + * Checks if the given string path is a Windows path. + * + * Before checking, the given path will be normalized first. + * + * @param p - The string path to be checked for. + * @returns `true` if the given path is a Windows path, `false` otherwise. + * @see {@link WIN32_PATH_PATTERN} + * + * @internal + */ +export function isWin32Path(p: StringPath): boolean { + p = path.normalize(p.trim()); + return !!p && WIN32_PATH_PATTERN.test(p); +} + +/** + * Resolves a file URL to a file path. + * + * @param {StringPath} p + * The file URL to resolve. It should be a string representing + * a valid file URL following the **WHATWG URL Standard**. + * @returns {StringPath} + * The resolved file path. If the provided URL is valid, + * it returns the corresponding file path. + * @throws {URIError} + * If the provided file URL scheme is invalid. This can occur + * if the URL scheme is not recognized or if it does not conform + * to the expected format. + * + * @remarks + * This function is used to convert a file URL to a file path. It first checks + * if the provided URL matches the expected pattern for file URLs. If it does, + * it proceeds to resolve the URL to a file path. If the URL scheme is not recognized + * or is invalid, a `URIError` is thrown. + * + * If the provided URL is `'file://'` or `'file:///'`, it is replaced with the root directory + * path (in the current drive for Windows systems). Otherwise, the URL is parsed using the + * `fileURLToPath` function. + * + * If the operating system is not Windows and the provided URL contains a Windows-style path, + * or if the operating system is Windows and the URL does not start with 'file:', an error is + * thrown indicating an invalid file URL scheme. + * + * @example + * // POSIX Path + * const fooPath = resolveFileURL('file:///path/to/foo.txt'); + * console.log(filePath); // Output: '/path/to/foo.txt' + * + * @example + * // Windows Path + * const projectsPath = resolveFileURL('file:///G:/Projects'); + * console.log(projectsPath); // Output: 'G:\\Projects' + * + * @see {@link https://url.spec.whatwg.org/ WHATWG URL Standard} + * @internal + */ +export function resolveFileURL(p: StringPath): StringPath { + if (FILE_URL_PATTERN.test(p)) { + // If and only if the given path is 'file://' or 'file:///' + // then replace the path to root directory (in current drive for Windows systems). + // When the specified above URL path being passed to `fileURLPath` function, + // it throws an error due to non-absolute URL path was given. + if (/^file:(?:\/\/\/?)$/.test(p)) p = '/'; + // Otherwise, parse the file URL path + else p = fileURLToPath(p); + } else if ((os.platform() !== 'win32' + && (isWin32Path(p) || !p.startsWith('file:'))) + || (os.platform() === 'win32' + && !(isWin32Path(p) || p.startsWith('file:')))) { + throw new URIError('Invalid file URL scheme'); + } + return p; +} + +/** + * Normalize a match-exclusion value into a {@link RegExp}, + * used for resolving the {@link LsOptions.match match} and + * {@link LsOptions.exclude exclude} options. + * + * If `val` is already a `RegExp` it is returned as-is. If `val` is a string + * ({@link StringPath}) a new `RegExp` is constructed from that string. + * + * @remarks + * When passing a string, do not include JavaScript regex delimiters (`/`). + * If you need flags (e.g. `i`), provide a `RegExp` instance instead. + * + * @param val - A string pattern or a `RegExp` to normalize. + * @returns A `RegExp` instance representing the provided pattern. + * + * @throws {SyntaxError} If the provided string is not a valid regular expression. + * + * @example + * // from string + * resolveMatchExclude('^/api') // => new RegExp('^/api') + * + * // already a RegExp + * const r = /\.test\.js$/i; + * resolveMatchExclude(r) // => r + * + * @since 1.2.0 + * @internal + */ +export function resolveMatchExclude(val: StringPath | RegExp): RegExp { + return typeof val === 'string' ? new RegExp(val) : val; +} + +/** + * Checks if a provided type matches any of the allowed types. + * + * This function verifies if a provided `type` argument matches any of the + * allowed types specified in the `validTypes` array. It throws a `TypeError` + * if the `type` doesn't match any valid type. + * + * @param type - The type value to be checked. + * @param validTypes - An array containing the allowed types for the `type` parameter. + * + * @throws {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/TypeError **TypeError**} - + * If the provided `type` doesn't match any of the valid types. + * + * @since 1.0.0 + * @internal + */ +export function checkType( + type: LsTypes | null | undefined, + validTypes: (string | number | null | undefined)[] +): void { + function joinAll(arr: (typeof validTypes), delim: string): string { + let str: string = ''; + arr.forEach((e: Unpack<(typeof validTypes)>, i: number) => { + if (i > 0 && i <= arr.length - 1) str += delim; + str += (typeof e === 'string') ? `'${e}'` + : (e === null) ? 'null' + : (typeof e === 'undefined') ? 'undefined' : e; + }); + return str; + } + + let match: boolean = false; + validTypes.forEach((validType: Unpack<(typeof validTypes)>) => { + if (!match && type === validType) match = true; + }); + + if (!match) { + throw new TypeError( + `Invalid 'type' value of ${type} ('${typeof type}'). Valid type is "${ + joinAll(validTypes.sort(), ' | ') + }"`); + } + return; +} + +/** + * Resolves the given `options` into a fully defined {@link ResolvedLsOptions} object. + * + * This function takes an optional `options` parameter, which can be an {@link LsOptions} object, + * a {@link RegExp} to specify the match pattern, or `null`/`undefined` to use defaults. + * If a `RegExp` is provided, it creates a resolved options object with default values except + * for the `match` field, which is set to the provided `RegExp`. + * If `options` is an object, it merges the provided options with the defaults, ensuring all + * properties are defined. Otherwise, it returns the default options. + * + * @param options - The options to resolve. Can be an {@link LsOptions} object, a {@link RegExp} + * to set the match pattern, or `null`/`undefined` for defaults. + * @returns A new {@link ResolvedLsOptions} object with all properties defined. + * + * @example + * // Using default options + * const opts = resolveOptions(null); + * console.log(opts.match); // /.+/ + * + * @example + * // Using a RegExp for match + * const opts = resolveOptions(/\.js$/); + * console.log(opts.match); // /\.js$/ + * + * @since 1.0.0 + * @internal + */ +export function resolveOptions(options?: LsOptions | RegExp | null): ResolvedLsOptions { + if (options instanceof RegExp) { + const resolved = { ...defaultLsOptions }; + resolved.match = options; + return resolved; + } + + return (!options || (options && typeof options !== 'object')) ? { ...defaultLsOptions } : { + encoding: options?.encoding?.trim() as BufferEncoding ?? defaultLsOptions.encoding, + recursive: options?.recursive ?? defaultLsOptions.recursive, + match: options?.match ?? defaultLsOptions.match, + exclude: options?.exclude ?? defaultLsOptions.exclude, + rootDir: options?.rootDir ?? defaultLsOptions.rootDir, + absolute: options?.absolute ?? defaultLsOptions.absolute, + basename: options?.basename ?? defaultLsOptions.basename + } satisfies ResolvedLsOptions; +} + +/** + * Encodes a string or an array of strings from one encoding to another. + * + * This function offers simplicity and flexibility by allowing encoding conversion + * between different encodings for either a string or a set of strings. + * + * @param val - The string to encode. + * @param from - The encoding of the input string. + * @param to - The encoding to convert the string to. + * + * @returns The encoded string. + * + * @throws {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/TypeError **TypeError**} - + * If the `from` or `to` encoding is unknown, or if the input value is + * neither a string nor an array of strings. + * + * @example + * Encode a string to 'base64' encoding: + * ```js + * const encodedString = encodeTo('Hello, world!', 'utf8', 'base64'); + * console.log(encodedString); + * // Output: 'SGVsbG8sIHdvcmxkIQ==' + * ``` + * + * Encode an array of strings to 'hex' encoding: + * ```js + * const encodedArray = encodeTo(['Hello', 'world'], 'utf8', 'hex'); + * console.log(encodedArray); + * // Output: ['48656c6c6f', '776f726c64'] + * ``` + * + * @since 1.0.0 + * @internal + */ +export function encodeTo( + val: string, + from: BufferEncoding, + to: BufferEncoding +): string; +/** + * @param val - The array of strings to encode. + * @param from - The encoding of the input strings. + * @param to - The encoding to convert the strings to. + * + * @returns The array of encoded strings. + */ +export function encodeTo( + val: string[], + from: BufferEncoding, + to: BufferEncoding +): string[]; + +export function encodeTo( + val: string | string[], + from: BufferEncoding, + to: BufferEncoding +): string | string[] { + const { isEncoding } = Buffer; + if (!isEncoding(from)) throw new TypeError("Unknown 'from' encoding: " + from); + else if (!isEncoding(to)) throw new TypeError("Unknown 'to' encoding: " + to); + else if (!(typeof val === 'string' || Array.isArray(val))) { + throw new TypeError('Expected a string or an array of string'); + } + + if (typeof val === 'string') { + return Buffer.from(val, from).toString(to); + } + + return val.map(function (v: string): string { + return Buffer.from(v, from).toString(to); + }); +} diff --git a/test/lib/simpletest.js b/test/lib/simpletest.js index 1b0eb1d..04ea7fe 100644 --- a/test/lib/simpletest.js +++ b/test/lib/simpletest.js @@ -38,7 +38,7 @@ class TestError extends Error { * @param {string} desc - A string describing the test case. * @param {Function} func - The function containing the test logic. * @param {boolean} [continueOnErr=false] - Whether to continue when error occurred. - * @throws {module:simpletest~TestError} If there is an error occurred in test logic. + * @throws {TestError} If there is an error occurred in test logic. */ async function it(desc, func, continueOnErr=false) { const { isAsyncFunction } = require('node:util').types; diff --git a/test/lsfnd-sync.spec.cjs b/test/lsfnd-sync.spec.cjs new file mode 100644 index 0000000..ae388d0 --- /dev/null +++ b/test/lsfnd-sync.spec.cjs @@ -0,0 +1,89 @@ +/** + * A test module for `lsfnd-sync` package designed for CommonJS module (CJS). + * @author Ryuu Mitsuki (https://github.com/mitsuki31) + */ + +const path = require('node:path'); +const { pathToFileURL } = require('node:url'); +const { ls, lsFiles, lsDirs } = require('../dist/lsfnd-sync'); +const { it, throws, doesNotThrow, deepEq } = require('./lib/simpletest'); + +const rootDir = path.resolve('..'); +const rootDirPosix = rootDir.replaceAll(path.sep, '/'); + +console.log(`\n\x1b[1m${path.basename(__filename)}:\x1b[0m`); + +it('test `ls` function by listing this file directory', () => { + const results = ls(__dirname, { absolute: true }, 0); + const expected = [ + 'lib', + 'lsfnd.spec.cjs', 'lsfnd.spec.mjs', + 'lsfnd-sync.spec.cjs', 'lsfnd-sync.spec.mjs', + ].map((e) => path.join(__dirname, e)).sort(); + deepEq(results, expected); +}, false); + +it('test `lsFiles` function by listing this file directory', () => { + const results = lsFiles(__dirname, { absolute: true }); + const expected = [ + 'lsfnd.spec.cjs', 'lsfnd.spec.mjs', + 'lsfnd-sync.spec.cjs', 'lsfnd-sync.spec.mjs', + ].map((e) => path.join(__dirname, e)).sort(); + deepEq(results, expected); +}, false); + +it('test `lsDirs` function by listing this file directory', () => { + const results = lsDirs(__dirname, { absolute: true }); + const expected = [ 'lib' ].map((e) => path.join(__dirname, e)); + deepEq(results, expected); +}, false); + +it('list root directory using URL object', () => { + doesNotThrow(() => ls(pathToFileURL(rootDirPosix)), URIError); +}, false); + +it('list root directory using file URL path', () => { + doesNotThrow(() => ls(pathToFileURL(rootDirPosix)), URIError); +}, false); + +it('test if the options argument allows explicit null value', () => { + doesNotThrow(() => lsFiles(__dirname, null), TypeError); +}, false); + +it('test if the type argument accepts a string value', () => { + doesNotThrow(() => ls(__dirname, {}, 'LS_D'), TypeError); +}, false); + +it("list this file directory with 'base64' encoding", () => { + const results = ls(__dirname, { rootDir: __dirname, encoding: 'base64' }); + const expected = [ + 'lib', + 'lsfnd.spec.cjs', 'lsfnd.spec.mjs', + 'lsfnd-sync.spec.cjs', 'lsfnd-sync.spec.mjs', + ].map((e) => Buffer.from(e, 'utf8').toString('base64')).sort(); + deepEq(results, expected); +}, false); + +// --- [ ERROR TESTS ] --- // + +it('throws an error if the given directory path not exist', () => { + throws(() => ls('./this/is/not/exist/directory/path'), Error); +}, false); + +it('throws a `URIError` if the given file URL path using unsupported protocol', + () => throws(() => ls('http:///'.concat(rootDirPosix)), URIError), + false +); + +it('throws a `TypeError` if the given type is an unexpected value', + () => { + throws(() => ls(__dirname, {}, 'LS_FOO'), TypeError); // Invalid string value test + throws(() => ls(__dirname, {}, []), TypeError); // Array test + }, + false +); + +it('throws an error if the given encoding option is unknown', () => { + throws(() => lsFiles(__dirname, { encoding: 'NotDefinedEncoding' }), TypeError); + throws(() => lsFiles(__dirname, { encoding: true }), TypeError); +}); diff --git a/test/lsfnd-sync.spec.mjs b/test/lsfnd-sync.spec.mjs new file mode 100644 index 0000000..01ea19d --- /dev/null +++ b/test/lsfnd-sync.spec.mjs @@ -0,0 +1,93 @@ +/** + * A test module for `lsfnd-sync` package designed for ECMAScript module (ESM). + * @author Ryuu Mitsuki (https://github.com/mitsuki31) + */ + +import * as path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { ls, lsFiles, lsDirs } from '../dist/lsfnd-sync.js'; +import test from './lib/simpletest.js'; +const { it, throws, doesNotThrow, deepEq } = test; // Resolve import from CommonJS module + +// Create the '__dirname' and '__filename' variable, because in ESM these are not defined +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const rootDir = path.resolve('..'); +const rootDirPosix = rootDir.replaceAll(path.sep, '/'); + +console.log(`\n\x1b[1m${path.basename(__filename)}:\x1b[0m`); + +it('test `ls` function by listing this file directory', () => { + const results = ls(__dirname, { absolute: true }, 0); + const expected = [ + 'lib', + 'lsfnd.spec.cjs', 'lsfnd.spec.mjs', + 'lsfnd-sync.spec.cjs', 'lsfnd-sync.spec.mjs', + ].map((e) => path.join(__dirname, e)).sort(); + deepEq(results, expected); +}, false); + +it('test `lsFiles` function by listing this file directory', () => { + const results = lsFiles(__dirname, { absolute: true }); + const expected = [ + 'lsfnd.spec.cjs', 'lsfnd.spec.mjs', + 'lsfnd-sync.spec.cjs', 'lsfnd-sync.spec.mjs', + ].map((e) => path.join(__dirname, e)).sort(); + deepEq(results, expected); +}, false); + +it('test `lsDirs` function by listing this file directory', () => { + const results = lsDirs(__dirname, { absolute: true }); + const expected = [ 'lib' ].map((e) => path.join(__dirname, e)); + deepEq(results, expected); +}, false); + +it('list root directory using URL object', () => { + doesNotThrow(() => ls(pathToFileURL(rootDirPosix)), URIError); +}, false); + +it('list root directory using file URL path', () => { + doesNotThrow(() => ls(pathToFileURL(rootDirPosix)), URIError); +}, false); + +it('test if the options argument allows explicit null value', () => { + doesNotThrow(() => lsFiles(__dirname, null), TypeError); +}, false); + +it('test if the type argument accepts a string value', () => { + doesNotThrow(() => ls(__dirname, {}, 'LS_D'), TypeError); +}, false); + +it("list this file directory with 'base64' encoding", () => { + const results = ls(__dirname, { rootDir: __dirname, encoding: 'base64' }); + const expected = [ + 'lib', + 'lsfnd.spec.cjs', 'lsfnd.spec.mjs', + 'lsfnd-sync.spec.cjs', 'lsfnd-sync.spec.mjs' + ].map((e) => Buffer.from(e, 'utf8').toString('base64')).sort(); + deepEq(results, expected); +}, false); + +// --- [ ERROR TESTS ] --- // + +it('throws an error if the given directory path not exist', () => { + throws(() => ls('./this/is/not/exist/directory/path'), Error); +}, false); + +it('throws a URIError if the given file URL path using unsupported protocol', + () => throws(() => ls('http:///'.concat(rootDirPosix)), URIError), + false +); + +it('throws a `TypeError` if the given type is an unexpected value', + () => { + throws(() => ls(__dirname, {}, 'LS_FOO'), TypeError); // Invalid string value test + throws(() => ls(__dirname, {}, []), TypeError); // Array test + }, + false +); + +it('throws an error if the given encoding option is unknown', () => { + throws(() => lsFiles(__dirname, { encoding: 'NotDefinedEncoding' }), TypeError); + throws(() => lsFiles(__dirname, { encoding: true }), TypeError); +}); diff --git a/test/lsfnd.spec.cjs b/test/lsfnd.spec.cjs index 65afa57..b487c9d 100644 --- a/test/lsfnd.spec.cjs +++ b/test/lsfnd.spec.cjs @@ -15,15 +15,20 @@ console.log(`\n\x1b[1m${path.basename(__filename)}:\x1b[0m`); it('test `ls` function by listing this file directory', async () => { const results = await ls(__dirname, { absolute: true }, 0); - const expected = [ 'lib', 'lsfnd.spec.cjs', 'lsfnd.spec.mjs' ] - .map((e) => path.join(__dirname, e)); + const expected = [ + 'lib', + 'lsfnd.spec.cjs', 'lsfnd.spec.mjs', + 'lsfnd-sync.spec.cjs', 'lsfnd-sync.spec.mjs', + ].map((e) => path.join(__dirname, e)).sort(); deepEq(results, expected); }, false); it('test `lsFiles` function by listing this file directory', async () => { const results = await lsFiles(__dirname, { absolute: true }); - const expected = [ 'lsfnd.spec.cjs', 'lsfnd.spec.mjs' ] - .map((e) => path.join(__dirname, e)); + const expected = [ + 'lsfnd.spec.cjs', 'lsfnd.spec.mjs', + 'lsfnd-sync.spec.cjs', 'lsfnd-sync.spec.mjs' + ].map((e) => path.join(__dirname, e)).sort(); deepEq(results, expected); }, false); @@ -51,8 +56,11 @@ it('test if the type argument accepts a string value', async () => { it("list this file directory with 'base64' encoding", async () => { const results = await ls(__dirname, { rootDir: __dirname, encoding: 'base64' }); - const expected = [ 'lib', 'lsfnd.spec.cjs', 'lsfnd.spec.mjs' ] - .map((e) => Buffer.from(e, 'utf8').toString('base64')); + const expected = [ + 'lib', + 'lsfnd.spec.cjs', 'lsfnd.spec.mjs', + 'lsfnd-sync.spec.cjs', 'lsfnd-sync.spec.mjs' + ].map((e) => Buffer.from(e, 'utf8').toString('base64')).sort(); deepEq(results, expected); }, false); diff --git a/test/lsfnd.spec.mjs b/test/lsfnd.spec.mjs index 716a781..279e352 100644 --- a/test/lsfnd.spec.mjs +++ b/test/lsfnd.spec.mjs @@ -19,15 +19,20 @@ console.log(`\n\x1b[1m${path.basename(__filename)}:\x1b[0m`); it('test `ls` function by listing this file directory', async () => { const results = await ls(__dirname, { absolute: true }, 0); - const expected = [ 'lib', 'lsfnd.spec.cjs', 'lsfnd.spec.mjs' ] - .map((e) => path.join(__dirname, e)); + const expected = [ + 'lib', + 'lsfnd.spec.cjs', 'lsfnd.spec.mjs', + 'lsfnd-sync.spec.cjs', 'lsfnd-sync.spec.mjs', + ].map((e) => path.join(__dirname, e)).sort(); deepEq(results, expected); }, false); it('test `lsFiles` function by listing this file directory', async () => { const results = await lsFiles(__dirname, { absolute: true }); - const expected = [ 'lsfnd.spec.cjs', 'lsfnd.spec.mjs' ] - .map((e) => path.join(__dirname, e)); + const expected = [ + 'lsfnd.spec.cjs', 'lsfnd.spec.mjs', + 'lsfnd-sync.spec.cjs', 'lsfnd-sync.spec.mjs', + ].map((e) => path.join(__dirname, e)).sort(); deepEq(results, expected); }, false); @@ -55,8 +60,11 @@ it('test if the type argument accepts a string value', async () => { it("list this file directory with 'base64' encoding", async () => { const results = await ls(__dirname, { rootDir: __dirname, encoding: 'base64' }); - const expected = [ 'lib', 'lsfnd.spec.cjs', 'lsfnd.spec.mjs' ] - .map((e) => Buffer.from(e, 'utf8').toString('base64')); + const expected = [ + 'lib', + 'lsfnd.spec.cjs', 'lsfnd.spec.mjs', + 'lsfnd-sync.spec.cjs', 'lsfnd-sync.spec.mjs', + ].map((e) => Buffer.from(e, 'utf8').toString('base64')).sort(); deepEq(results, expected); }, false); diff --git a/tsconfig.production.json b/tsconfig.production.json index 1f6b47a..c0b89e7 100644 --- a/tsconfig.production.json +++ b/tsconfig.production.json @@ -3,7 +3,7 @@ "compilerOptions": { "strict": true, "alwaysStrict": true, - "removeComments": true, + "removeComments": false, "declaration": true } } diff --git a/types/index.d.ts b/types/index.d.ts index 64f4063..ef95c9c 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -10,7 +10,6 @@ * @since 0.1.0 * @license MIT */ -/// /** * A type representing the string path. @@ -25,7 +24,7 @@ export declare type StringPath = string; * the listed directory. * @since 0.1.0 */ -export declare type LsEntries = Array; +export declare type LsEntries = StringPath[]; /** * This type alias represents the possible return values of the `ls*` functions. @@ -56,7 +55,7 @@ export declare type LsTypesKeys = keyof LsTypesInterface; * @see {@link LsTypesInterface} * @see {@link LsTypesKeys} */ -export declare type LsTypesValues = keyof typeof LsTypesInterface; +export declare type LsTypesValues = LsTypesInterface[LsTypesKeys]; /** * Interface defining the {@link lsTypes} enum with string literal keys @@ -230,10 +229,9 @@ export declare interface LsOptions { * @see {@link LsOptions} * @see {@link DefaultLsOptions} */ -export declare type ResolvedLsOptions = { - [T in keyof LsOptions]-?: T extends 'exclude' - ? NonNullable | undefined - : NonNullable +export declare type ResolvedLsOptions = Required> & { + // Keep this to have undefined, so we can use that value if don't want to exclude any files + exclude: LsOptions["exclude"]; }; /** @@ -255,40 +253,3 @@ export declare interface DefaultLsOptions { readonly absolute: false; readonly basename: false; } - -// ====== APIs ===== // - -/** - * {@inheritDoc !lsTypes~lsTypes} - * - * @see For more details, refer to {@link !lsTypes~lsTypes lsTypes} enum documentation. - */ -export declare const lsTypes: Record< - LsTypesKeys, - LsTypesValues -> & Record< - LsTypesValues, - LsTypesKeys ->; - -/** {@inheritDoc !lsfnd~ls} */ -export declare function ls( - dirpath: StringPath | URL, - options?: LsOptions | RegExp | undefined, - type?: LsTypes | undefined -): Promise - -/** {@inheritDoc !lsfnd~lsFiles} */ -export declare function lsFiles( - dirpath: StringPath | URL, - options?: LsOptions | RegExp | undefined -): Promise - -/** {@inheritDoc !lsfnd~lsDirs} */ -export declare function lsDirs( - dirpath: StringPath | URL, - options?: LsOptions | RegExp | undefined -): Promise - -// Ensure it is treated as module -export {};