diff --git a/README.md b/README.md
index 7bb3527..cdd0bd0 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
[](https://npmjs.com/package/lsfnd)

-[](https://npmjs.com/package/lsfnd)
+[](https://npmjs.com/package/lsfnd)
[](https://github.com/mitsuki31/lsfnd/actions/workflows/test.yml)
-[](https://github.com/mitsuki31/lsfnd/tree/master/LICENSE)
+[](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 {};