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