diff --git a/src/index.ts b/src/index.ts
index 06775ce..f593772 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -11,11 +11,14 @@
export * from './lsTypes';
export * from './lsfnd';
export type {
+ StringPath,
LsTypes,
LsTypesInterface,
LsTypesKeys,
LsTypesValues,
LsOptions,
+ ResolvedLsOptions,
+ DefaultLsOptions,
LsEntries,
LsResult
} from '../types';
diff --git a/src/lsfnd.ts b/src/lsfnd.ts
index 0efcd8c..ca65877 100644
--- a/src/lsfnd.ts
+++ b/src/lsfnd.ts
@@ -16,14 +16,34 @@ import { isRegExp } from 'node:util';
import { URL } from 'node:url';
import { lsTypes } from './lsTypes';
import type {
+ StringPath,
LsEntries,
LsResult,
LsOptions,
+ ResolvedLsOptions,
+ DefaultLsOptions,
LsTypes
} from '../types';
type Unpack = A extends Array<(infer U)> ? U : A;
+/**
+ * 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.
*
@@ -56,7 +76,7 @@ type Unpack = A extends Array<(infer U)> ? U : A;
*
* @internal
*/
-function fileUrlToPath(url: URL | string): string {
+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');
@@ -99,15 +119,39 @@ function checkType(
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 "${
+ `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
+ });
+}
+
/**
* Lists files and/or directories in a specified directory path, filtering by a
* regular expression pattern.
@@ -183,13 +227,12 @@ function checkType(
* @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback fs.readdir}
*/
export async function ls(
- dirpath: string | URL,
+ dirpath: StringPath | URL,
options?: LsOptions | RegExp | undefined,
type?: LsTypes | undefined
): Promise {
- let absdirpath: string;
- let match: string | RegExp,
- exclude: string | RegExp | undefined;
+ let absdirpath: StringPath,
+ reldirpath: StringPath;
if (dirpath instanceof URL) {
if (dirpath.protocol !== 'file:') {
@@ -204,33 +247,35 @@ export async function ls(
dirpath = fileUrlToPath(dirpath);
}
} else {
- throw new Error('Unknown type, expected a string or an URL object');
+ throw new TypeError('Unknown type, expected a string or an URL object');
}
- // Resolve its absolute path
- absdirpath = path.isAbsolute( dirpath)
- ? dirpath
- : path.posix.resolve( dirpath);
-
if (isRegExp(options)) {
- match = options;
- exclude = undefined;
- options = { encoding: 'utf8', recursive: false };
- } else if (typeof options === 'undefined' || options === null) {
- options = { encoding: 'utf8', recursive: false };
- match = /.+/;
- } else if (options && typeof options === 'object' && !Array.isArray(options)) {
- match = (typeof options!.match === 'string')
- ? new RegExp(options!.match)
- : (isRegExp(options!.match) ? options!.match : /.+/);
- exclude = (typeof options!.exclude === 'string')
- ? new RegExp(options!.exclude)
- : (isRegExp(options!.exclude) ? options!.exclude : undefined);
+ // 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": '
+ throw new TypeError("Unknown type of 'options': "
+ (Array.isArray(options) ? 'array' : typeof options));
}
+ // Check and resolve the `rootDir` option
+ if (options.rootDir && (options.rootDir instanceof URL
+ || (typeof options.rootDir === 'string' && /^[a-zA-Z]+:/.test(options.rootDir))
+ )) {
+ options.rootDir = fileUrlToPath(options.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);;
+
// Check the type argument
checkType(type!, [ ...Object.values(lsTypes), 0, null, undefined ]);
@@ -244,8 +289,8 @@ export async function ls(
// Filter the entries
result = await Promise.all(
- entries.map(async function (entry: string): Promise<(string | null)> {
- entry = path.join( dirpath, entry);
+ entries.map(async function (entry: StringPath): Promise<(StringPath | null)> {
+ entry = path.join(absdirpath, entry);
const stats: fs.Stats = await fs.promises.stat(entry);
let resultType: boolean = false;
@@ -261,18 +306,30 @@ export async function ls(
default: resultType = (stats.isFile() || stats.isDirectory());
}
- return (
+ return ((
resultType && (
- ( match).test(entry)
- && (exclude ? !( exclude).test(entry) : true)
+ ( options.match).test(entry)
+ && (options.exclude ? !( options.exclude).test(entry) : true)
+ )
+ )
+ ? (
+ // *** High priority
+ (options.absolute && (options.basename || !options.basename))
+ ? entry // already an absolute path
+ // *** Medium priority
+ : (!options.absolute && options.basename)
+ ? path.basename(entry) // get its basename
+ // *** Low priority
+ // convert back to the relative path
+ : path.join(reldirpath, path.relative(absdirpath, entry))
)
- ) ? entry : null;
+ : null
+ )
})
- ).then(function (results: Array): LsEntries {
+ ).then(function (results: (Unpack | null)[]): LsEntries {
return results.filter(
function (entry: Unpack<(typeof results)>): boolean {
- // Remove any null entries
- return !!entry!;
+ return !!entry!; // Remove any null entries
}
);
});
@@ -349,7 +406,7 @@ export async function ls(
* @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback fs.readdir}
*/
export async function lsFiles(
- dirpath: string | URL,
+ dirpath: StringPath | URL,
options?: LsOptions | RegExp | undefined
): Promise {
return ls(dirpath, options, lsTypes.LS_F);
@@ -420,7 +477,7 @@ export async function lsFiles(
* @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback fs.readdir}
*/
export async function lsDirs(
- dirpath: string | URL,
+ dirpath: StringPath | URL,
options?: LsOptions | RegExp | undefined
): Promise {
return ls(dirpath, options, lsTypes.LS_D);
diff --git a/test/lsfnd.spec.cjs b/test/lsfnd.spec.cjs
index 61fa721..f5d57e5 100644
--- a/test/lsfnd.spec.cjs
+++ b/test/lsfnd.spec.cjs
@@ -14,21 +14,21 @@ const rootDirPosix = path.posix.resolve('..');
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, {}, 0);
+ const results = await ls(__dirname, { absolute: true }, 0);
const expected = [ 'lib', 'lsfnd.spec.cjs', 'lsfnd.spec.mjs' ]
.map((e) => path.join(__dirname, e));
deepEq(results, expected);
}, false);
it('test `lsFiles` function by listing this file directory', async () => {
- const results = await lsFiles(__dirname);
+ const results = await lsFiles(__dirname, { absolute: true });
const expected = [ 'lsfnd.spec.cjs', 'lsfnd.spec.mjs' ]
.map((e) => path.join(__dirname, e));
deepEq(results, expected);
}, false);
it('test `lsDirs` function by listing this file directory', async () => {
- const results = await lsDirs(__dirname);
+ const results = await lsDirs(__dirname, { absolute: true });
const expected = [ 'lib' ].map((e) => path.join(__dirname, e));
deepEq(results, expected);
}, false);
diff --git a/test/lsfnd.spec.mjs b/test/lsfnd.spec.mjs
index c32c996..b2ad20e 100644
--- a/test/lsfnd.spec.mjs
+++ b/test/lsfnd.spec.mjs
@@ -18,21 +18,21 @@ const rootDirPosix = path.posix.resolve('..');
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, {}, 0);
+ const results = await ls(__dirname, { absolute: true }, 0);
const expected = [ 'lib', 'lsfnd.spec.cjs', 'lsfnd.spec.mjs' ]
.map((e) => path.join(__dirname, e));
deepEq(results, expected);
}, false);
it('test `lsFiles` function by listing this file directory', async () => {
- const results = await lsFiles(__dirname);
+ const results = await lsFiles(__dirname, { absolute: true });
const expected = [ 'lsfnd.spec.cjs', 'lsfnd.spec.mjs' ]
.map((e) => path.join(__dirname, e));
deepEq(results, expected);
}, false);
it('test `lsDirs` function by listing this file directory', async () => {
- const results = await lsDirs(__dirname);
+ const results = await lsDirs(__dirname, { absolute: true });
const expected = [ 'lib' ].map((e) => path.join(__dirname, e));
deepEq(results, expected);
}, false);
diff --git a/types/index.d.ts b/types/index.d.ts
index 18d7b98..3c0c51b 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -13,13 +13,19 @@
///
/**
- * This type alias represents an array of strings. It is typically used to
+ * A type representing the string path.
+ * @since 1.0.0
+ */
+export declare type StringPath = string;
+
+/**
+ * This type alias represents an array of {@link StringPath}. It is typically used to
* represent the list of file and/or directory entries returned by the `ls*` functions.
- * Each string in the array represents the path of a file or directory within
+ * Each entry in the array represents the path of a file or directory within
* the listed directory.
* @since 0.1.0
*/
-export declare type LsEntries = Array;
+export declare type LsEntries = Array;
/**
* This type alias represents the possible return values of the `ls*` functions.
@@ -31,8 +37,9 @@ export declare type LsResult = LsEntries | null;
/**
* A combination union types containing all possible values used to specify the
- * returned results on {@link !lsfnd~ls ls} function.
+ * returned results on {@link !lsfnd~ls ls} function.
* @since 1.0.0
+ * @see {@link !lsTypes~lsTypes lsTypes}
*/
export declare type LsTypes = LsTypesKeys | LsTypesValues;
@@ -47,12 +54,9 @@ export declare type LsTypesKeys = keyof LsTypesInterface;
* Type representing all possible values of the {@link lsTypes} enum.
* @since 0.1.0
* @see {@link LsTypesInterface}
+ * @see {@link LsTypesKeys}
*/
-export declare type LsTypesValues =
- | 0b00 // 0 (interpreted the same as LS_A | 1)
- | 0b01 // 1 (LS_A)
- | 0b10 // 2 (LS_D)
- | 0b100 // 4 (LS_F)
+export declare type LsTypesValues = keyof typeof LsTypesInterface;
/**
* Interface defining the {@link lsTypes} enum with string literal keys
@@ -66,22 +70,23 @@ export declare interface LsTypesInterface {
* Represents an option to include all file types.
* @defaultValue `0b01 << 0b00` (`0b01` | `0o01` | `0x01` | `1`)
*/
- readonly LS_A: number; // ALL
+ readonly LS_A: 0b01; // ALL
/**
* Represents an option to include only the directory type.
* @defaultValue `0b01 << 0b01` (`0b10` | `0o02` | `0x02` | `2`)
*/
- readonly LS_D: number; // DIRECTORY
+ readonly LS_D: 0b10; // DIRECTORY
/**
* Represents an option to include only the file type.
* @defaultValue `0b01 << 0b10` (`0b100` | `0o04` | `0x04` | `4`)
*/
- readonly LS_F: number; // FILE
+ readonly LS_F: 0b100; // FILE
}
/**
* This interface defines the optional configuration options that can be passed
- * to the `ls*` function. These options control the behavior of the file listing.
+ * to every `ls*` functions. These options control the behavior of the directory listing.
+ *
* @interface
* @since 0.1.0
*/
@@ -89,26 +94,164 @@ export declare interface LsOptions {
/**
* Specifies the character encoding to be used when reading a directory.
* @defaultValue `'utf8'`
+ * @since 0.1.0
*/
- encoding?: BufferEncoding | undefined,
+ encoding?: BufferEncoding | undefined;
/**
* A boolean flag indicating whether to include subdirectories in the listing.
* @defaultValue `false`
+ * @since 0.1.0
*/
- recursive?: boolean | undefined,
+ recursive?: boolean | undefined;
/**
* A regular expression or string pattern used to filter the listed entries.
* Only entries matching the pattern will be included in the results.
* @defaultValue `/.+/` (match all files)
+ * @since 0.1.0
*/
- match?: RegExp | string | undefined,
+ match?: RegExp | string | undefined;
/**
* A regular expression or string pattern used to exclude entries from the
* listing. Any entries matching this pattern will be filtered out of the
* results, even if they match the {@link match} pattern.
* @defaultValue `undefined`
+ * @since 0.1.0
+ */
+ exclude?: RegExp | string | undefined;
+ /**
+ * A string path representing the root directory to resolve the results to
+ * relative paths.
+ *
+ * This option will be ignored if either one of the {@link absolute `absolute`}
+ * or {@link basename `basename`} option are enabled, this is due to their
+ * higher priority. This option have the lowest priority when compared with those
+ * options.
+ *
+ * @defaultValue `'.'` or `process.cwd()`
+ * @since 1.0.0
+ */
+ rootDir?: StringPath | URL | undefined;
+ /**
+ * Determines whether to return absolute paths for all entries.
+ *
+ * When enabled (i.e., set to `true`), each entry of the returned results
+ * will be an absolute path. Otherwise, paths will be relative to the directory
+ * specified in the {@link rootDir `rootDir`} field.
+ *
+ * Enabling this option are equivalent with the following code.
+ * Let's assume we want to list all files within a directory named `'foo'`:
+ *
+ * ```js
+ * const { resolve } = require('node:path');
+ * const { lsFiles } = require('lsfnd');
+ * // Or ESM:
+ * // import { resolve } from 'node:path';
+ * // import { lsFiles } from 'lsfnd';
+ *
+ * const absfiles = (await lsFiles('foo/')).map((entry) => resolve(entry));
+ * ```
+ *
+ * In previous release (prior to version 0.1.0) you can literally use an
+ * explicit method that makes the returned results as absolute paths entirely.
+ * That is by utilizing the `path.resolve` function, here is an example:
+ *
+ * ```js
+ * const absfiles = await lsFiles(path.resolve('foo/'));
+ * ```
+ *
+ * In the above code, the directory path is resolved to an absolute path before
+ * being passed to the {@link !lsfnd~lsFiles `lsFiles`} function. As a result,
+ * the function treats the specified directory path as a relative path and
+ * does not attempt to resolve it back to a relative path, thus returning
+ * absolute paths. This approach was considered unstable and problematic due
+ * to inconsistencies in the internal logic. Therefore, this option was
+ * introduced as a replacement and will default returns relative paths when
+ * this option are disabled (set to `false` or unspecified), they will relative
+ * to the path specified in the {@link rootDir `rootDir`} field. Refer to
+ * {@link rootDir `rootDir`} option for more details.
+ *
+ * @defaultValue `false`
+ * @since 1.0.0
+ * @see {@link rootDir}
+ */
+ absolute?: boolean | undefined;
+ /**
+ * Whether to make the returned result paths only have their basenames, trimming any
+ * directories behind the path separator (i.e., `\\` in Windows, and `/` in POSIX).
+ *
+ * When set to `true`, the returned paths will only include the file and/or
+ * directory names itself. This can be useful if you need only the names while
+ * listing a directory.
+ *
+ * If you enabled both this option and the {@link absolute `absolute`} option,
+ * the `absolute` option will be treated instead due to its higher priority rather
+ * than this option's priority.
+ *
+ * > ### Note
+ * > Please note, that this option implicitly includes any directories on the
+ * > returned entries. If you want to only include the filenames, then
+ * > combine this option with {@link !lsTypes~lsTypes.LS_F `LS_F`} type if you
+ * > are using {@link !lsfnd~ls `ls`} function, or use this option with
+ * > {@link !lsfnd~lsFiles `lsFiles`} function for better flexibility.
+ *
+ * This option achieves the same functionality as the following code:
+ *
+ * ```js
+ * const path = require('node:path');
+ * // Or ESM:
+ * // import * as path from 'node:path';
+ *
+ * // Assume you have `results` variable contains all files paths
+ * // from listing a directory using `lsFiles` function
+ * const baseResults = results.map((res) => res.split(path.sep).pop());
+ * ```
+ *
+ * Or even a bit more simple like this:
+ * ```js
+ * // ...
+ * const baseResults = results.map((res) => path.basename(res));
+ * ```
+ *
+ * @defaultValue `false`
+ * @since 1.0.0
+ * @see {@link rootDir}
*/
- exclude?: RegExp | string | undefined
+ basename?: boolean | undefined;
+}
+
+/**
+ * Represents resolved options type for the `ls*` functions, where all properties are
+ * required and both `null` and `undefined` values are omitted, except for the
+ * {@link LsOptions.exclude `exclude`} property which keeps the `undefined` type.
+ *
+ * @since 1.0.0
+ * @see {@link LsOptions}
+ * @see {@link DefaultLsOptions}
+ */
+export declare type ResolvedLsOptions = {
+ [T in keyof LsOptions]-?: T extends 'exclude'
+ ? NonNullable | undefined
+ : NonNullable
+};
+
+/**
+ * Represents the default options type for the `ls*` functions, used by
+ * {@link !lsfnd~defaultLsOptions `defaultLsOptions`}.
+ *
+ * @interface
+ * @since 1.0.0
+ * @see {@link LsOptions}
+ * @see {@link ResolvedLsOptions}
+ * @see {@link !lsfnd~defaultLsOptions defaultLsOptions}
+ */
+export declare interface DefaultLsOptions {
+ readonly encoding: 'utf8';
+ readonly recursive: false;
+ readonly match: RegExp;
+ readonly exclude: undefined;
+ readonly rootDir: StringPath | URL;
+ readonly absolute: false;
+ readonly basename: false;
}
// ====== APIs ===== //
@@ -128,20 +271,20 @@ export declare const lsTypes: Record<
/** {@inheritDoc !lsfnd~ls} */
export declare function ls(
- dirpath: string | URL,
+ dirpath: StringPath | URL,
options?: LsOptions | RegExp | undefined,
type?: LsTypes | undefined
): Promise
/** {@inheritDoc !lsfnd~lsFiles} */
export declare function lsFiles(
- dirpath: string | URL,
+ dirpath: StringPath | URL,
options?: LsOptions | RegExp | undefined
): Promise
/** {@inheritDoc !lsfnd~lsDirs} */
export declare function lsDirs(
- dirpath: string | URL,
+ dirpath: StringPath | URL,
options?: LsOptions | RegExp | undefined
): Promise