From 8ab99acdcb42b03d4a3cf14f979c9673e1157169 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki Date: Wed, 10 Dec 2025 22:18:01 +0700 Subject: [PATCH 1/8] refactor: Refactor and improve type definitions - Fixed the `LsTypesValues` type definition to iterate through `LsTypesInterface` interface to get the values instead of `keyof typeof LsTypesInterface` - Improved the `ResolvedLsOptions` and keeping the `exclude` property to have `undefined` type exclusively - Removed redundant and unnecessary type casting in core (`lsfnd`) module - Improved all types (including functions' parameters) within core module In addition, this change also make all `ls*` functions (`ls`, `lsDirs`, and `lsFiles`) to support to use `lsTypes` keys (string) or values (numbers) with stable and will throws an error if specified any unknown values (not including nullable values). --- src/lsfnd.ts | 117 ++++++++++++++++++++++++++--------------------- types/index.d.ts | 12 ++--- 2 files changed, 71 insertions(+), 58 deletions(-) diff --git a/src/lsfnd.ts b/src/lsfnd.ts index bbf78e4..c19627e 100644 --- a/src/lsfnd.ts +++ b/src/lsfnd.ts @@ -25,12 +25,12 @@ import type { } from '../types'; import { lsTypes } from './lsTypes'; -type Unpack = A extends Array<(infer U)> ? U : A; +type Unpack = A extends (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 */ @@ -58,7 +58,7 @@ export const defaultLsOptions: DefaultLsOptions = { rootDir: process.cwd(), absolute: false, basename: false -} as const; +} satisfies DefaultLsOptions; /** * Converts a file URL to a file path. @@ -115,7 +115,7 @@ function fileUrlToPath(url: URL | StringPath): StringPath { * @internal */ function isWin32Path(p: StringPath): boolean { - p = path.normalize(p); + p = path.normalize(p.trim()); return !!p && WIN32_PATH_PATTERN.test(p); } @@ -194,9 +194,9 @@ function resolveFileURL(p: StringPath): StringPath { * @since 1.0.0 * @internal */ -function checkType( - type: LsTypes | N, - validTypes: Array<(string | number | N)> +function checkType( + type: LsTypes | null | undefined, + validTypes: (string | number | null | undefined)[] ): void { function joinAll(arr: (typeof validTypes), delim: string): string { let str: string = ''; @@ -216,7 +216,7 @@ function checkType( 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(), ' | ') }"`); } @@ -234,16 +234,16 @@ function checkType( * @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 - }); +function resolveOptions(options?: LsOptions | null): ResolvedLsOptions { + 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; } /** @@ -293,16 +293,16 @@ function encodeTo( * @returns The array of encoded strings. */ function encodeTo( - val: Array, + val: string[], from: BufferEncoding, to: BufferEncoding -): Array; +): string[]; function encodeTo( - val: string | Array, + val: string | string[], from: BufferEncoding, to: BufferEncoding -): string | Array { +): 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); @@ -314,7 +314,7 @@ function encodeTo( return Buffer.from(val, from).toString(to); } - return (> val).map(function (v: string): string { + return val.map(function (v: string): string { return Buffer.from(v, from).toString(to); }); } @@ -398,9 +398,10 @@ 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)); + if (!(dirpath instanceof URL) && typeof dirpath !== 'string') { throw new TypeError('Unknown type, expected a string or a URL object'); } @@ -423,11 +424,11 @@ export async function ls( 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 + 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); + options = resolveOptions(options); } else { throw new TypeError("Unknown type of 'options': " + (Array.isArray(options) ? 'array' : typeof options)); @@ -444,30 +445,32 @@ export async function ls( } // 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(options.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, { + // FIXME encoding: options?.encoding || 'utf8', recursive: options?.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'); + // FIXME + const utf8Entries: LsEntries = encodeTo(entries, options?.encoding, '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,38 +495,49 @@ 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)]: 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) + options.match?.test(entry) // FIXME + && (options.exclude ? !options.exclude.test(entry) : true) // FIXME ) ) ? ( @@ -541,20 +555,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 && options?.encoding !== 'utf8') + // FIXME + result = encodeTo(result, 'utf8', options.encoding); + return (result ? result.sort() : result); } /** diff --git a/types/index.d.ts b/types/index.d.ts index 64f4063..49c6a50 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"]; }; /** From ca82e04ec1e3cc09e99d0e8833ce4617701fa203 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki Date: Wed, 10 Dec 2025 23:32:22 +0700 Subject: [PATCH 2/8] refactor: Refactor and improve the core module - Fixed unresolved type for `match` and `exclude` options - Updated the `resolveOptions` util function to accept RegExp also for more convenience when being used - Added special case with value 0 for case `LS_A`, it will behaves similar to `LS_A` and list both files and directories - Several code improvements --- src/lsfnd.ts | 109 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 43 deletions(-) diff --git a/src/lsfnd.ts b/src/lsfnd.ts index c19627e..1a95f60 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 @@ -178,6 +176,10 @@ function resolveFileURL(p: StringPath): StringPath { return p; } +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. * @@ -224,18 +226,40 @@ function checkType( } /** - * Resolves the given `options` ({@link LsOptions}). + * 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); // /.+/ * - * @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`. + * @example + * // Using a RegExp for match + * const opts = resolveOptions(/\.js$/); + * console.log(opts.match); // /\.js$/ * * @since 1.0.0 * @internal */ -function resolveOptions(options?: LsOptions | null): ResolvedLsOptions { - return (!options || (options && typeof options !== 'object')) ? defaultLsOptions : { +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, @@ -319,6 +343,7 @@ function encodeTo( }); } + /** * Lists files and/or directories in a specified directory path, filtering by a * regular expression pattern. @@ -402,6 +427,21 @@ export async function ls( 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'); } @@ -421,34 +461,21 @@ 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);; + reldirpath = path.relative(resOptions.rootDir ?? process.cwd(), absdirpath);; // Check the type argument checkType(type, [ ...Object.values(lsTypes), 0, null, undefined ]); @@ -457,14 +484,12 @@ export async function ls( try { // Read the specified directory path recursively const entries: LsEntries = await fs.promises.readdir(absdirpath, { - // FIXME - 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. - // FIXME - const utf8Entries: LsEntries = encodeTo(entries, options?.encoding, 'utf8'); + const utf8Entries: LsEntries = encodeTo(entries, usedEncoding, 'utf8'); // Filter the entries result = await Promise.all( @@ -525,6 +550,7 @@ export async function ls( break; case lsTypes.LS_A: case lsTypesValues[String(lsTypes.LS_A)]: + case (0 as lsTypes): // Special case resultType = ( (stats?.isFile() || isFile) || (stats?.isDirectory() || isDir) @@ -535,17 +561,14 @@ export async function ls( } return (( - resultType && ( - options.match?.test(entry) // FIXME - && (options.exclude ? !options.exclude.test(entry) : true) // FIXME - ) + 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 @@ -566,9 +589,9 @@ export async function ls( } // Encode back the entries to the specified encoding - if (result && options?.encoding !== 'utf8') - // FIXME - result = encodeTo(result, 'utf8', options.encoding); + if (result && usedEncoding !== 'utf8') { + result = encodeTo(result, 'utf8', usedEncoding); + } return (result ? result.sort() : result); } From 01876986f746ce7000eff9d614b506183ffdcd23 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki Date: Thu, 11 Dec 2025 17:14:45 +0700 Subject: [PATCH 3/8] refactor: Reorganize the core module - Migrated all utility functions to new `utils` module - Moved all constants from core module to new `constants` module --- src/constants.ts | 34 +++++ src/index.ts | 1 + src/lsfnd.ts | 340 ++--------------------------------------------- src/utils.ts | 286 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 333 insertions(+), 328 deletions(-) create mode 100644 src/constants.ts create mode 100644 src/utils.ts 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.ts b/src/lsfnd.ts index 1a95f60..0bcf2f4 100644 --- a/src/lsfnd.ts +++ b/src/lsfnd.ts @@ -9,343 +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 (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 -} satisfies DefaultLsOptions; - -/** - * 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.trim()); - return !!p && WIN32_PATH_PATTERN.test(p); -} +import { + type Unpack, + resolveOptions, + resolveMatchExclude, + resolveFileURL, + checkType, + encodeTo, +} from './utils'; /** - * 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; -} - -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 - */ -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 - */ -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 - */ -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: string[], - from: BufferEncoding, - to: BufferEncoding -): string[]; - -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); - }); -} - - -/** - * 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}, @@ -640,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**} - @@ -669,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}, @@ -713,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); + }); +} From 30e051c135aeae2569ba6e05ede060a2f6f8e6d5 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki Date: Thu, 11 Dec 2025 18:24:11 +0700 Subject: [PATCH 4/8] feat: Add `lsfnd-sync` module for sync version of lsfnd The code inside are mirroring to the core module (`lsfnd.ts`) with some adjustments for synchronous operations. --- build.prop.js | 1 + src/lsfnd-sync.ts | 355 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 356 insertions(+) create mode 100644 src/lsfnd-sync.ts 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/src/lsfnd-sync.ts b/src/lsfnd-sync.ts new file mode 100644 index 0000000..f2bb1ea --- /dev/null +++ b/src/lsfnd-sync.ts @@ -0,0 +1,355 @@ +/** + * 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'; + +/** + * **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); +} From 729892779c62a2dd54f2b0f6b5ae7f3318c61f70 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki Date: Thu, 11 Dec 2025 18:28:08 +0700 Subject: [PATCH 5/8] test: Add tests for `lsfnd-sync` module - Added test modules `lsfnd-sync.spec.cjs` and `lsfnd-sync.spec.mjs` - Readjusted the expected values within the existing test modules - Updated the `test:cjs` and `test:mjs` scripts within `package.json` to include and run the new test modules --- package.json | 5 ++- test/lib/simpletest.js | 2 +- test/lsfnd-sync.spec.cjs | 89 ++++++++++++++++++++++++++++++++++++++ test/lsfnd-sync.spec.mjs | 93 ++++++++++++++++++++++++++++++++++++++++ test/lsfnd.spec.cjs | 20 ++++++--- test/lsfnd.spec.mjs | 20 ++++++--- 6 files changed, 214 insertions(+), 15 deletions(-) create mode 100644 test/lsfnd-sync.spec.cjs create mode 100644 test/lsfnd-sync.spec.mjs diff --git a/package.json b/package.json index bd156ae..a76daad 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "exports": { ".": "./dist/index.js", "./lsfnd": "./dist/lsfnd.js", + "./sync": "./dist/lsfnd-sync.js", "./types": "./types/index.d.ts", "./package.json": "./package.json" }, @@ -23,8 +24,8 @@ "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" }, 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); From f2901e46348b55b04f9366ea1722e78a88e20b1d Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki Date: Thu, 11 Dec 2025 19:47:13 +0700 Subject: [PATCH 6/8] build: Add postbuild process and preserve comments - Updated the `tsconfig.production.json` to preserve comments for transpiled files - Added `scripts/postbuild` module - Added `removeMultilineCommentsAfterUseStrict` function to remove the module's header comment - Removed the APIs type implementation from `types/index` module - Added `postbuild` script to `package.json` executing the `scripts/postbuild` module With this change, we can guarantee the APIs are now will appears along with the documentation when used with Intellisense. --- package.json | 3 +- scripts/postbuild.ts | 71 ++++++++++++++++++++++++++++++++++++++++ tsconfig.production.json | 2 +- types/index.d.ts | 37 --------------------- 4 files changed, 74 insertions(+), 39 deletions(-) create mode 100644 scripts/postbuild.ts diff --git a/package.json b/package.json index a76daad..df4088c 100644 --- a/package.json +++ b/package.json @@ -21,13 +21,14 @@ "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 && 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/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 49c6a50..ef95c9c 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -253,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 {}; From bd6dfb6b5bc9fb1165e4e3a9d41d42a36fc24633 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki Date: Thu, 11 Dec 2025 19:52:25 +0700 Subject: [PATCH 7/8] docs(readme): Update the README file --- README.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7bb3527..cdd0bd0 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ [![Version](https://img.shields.io/npm/v/lsfnd?logo=npm&label=lsfnd)](https://npmjs.com/package/lsfnd) ![Min. Node](https://img.shields.io/node/v-lts/lsfnd/latest?logo=node.js&label=node) -[![Bundle size (minified)](https://img.shields.io/bundlephobia/min/lsfnd)](https://npmjs.com/package/lsfnd)
+[![Bundle size (minified)](https://img.shields.io/bundlephobia/min/lsfnd)](https://npmjs.com/package/lsfnd)
[![Test CI](https://github.com/mitsuki31/lsfnd/actions/workflows/test.yml/badge.svg)](https://github.com/mitsuki31/lsfnd/actions/workflows/test.yml) -[![License](https://img.shields.io/github/license/mitsuki31/lsfnd?logo=github&logoColor=f9f9f9&label=License&labelColor=yellow&color=white)](https://github.com/mitsuki31/lsfnd/tree/master/LICENSE) +[![License](https://img.shields.io/github/license/mitsuki31/lsfnd?logo=readme&logoColor=f9f9f9&label=License&labelColor=yellow&color=white)](https://github.com/mitsuki31/lsfnd/tree/master/LICENSE) **LSFND** is an abbreviation for _list (ls) files (f) and (n) directories (d)_, a lightweight Node.js library designed to make listing files and directories more convenient. @@ -12,13 +12,22 @@ It offers an efficient and simple way to explore through your directory structur and retrieves the names of files and/or directories leveraging a configurable options to modify the listing behavior, such as recursive searches and regular expression filters. -This library's **primary benefit** is that every implemented API runs asynchronously, +This library's **primary benefit** is that every implemented API within main module runs asynchronously, guaranteeing that they will **NEVER** disrupt the execution of any other processes. -> [!IMPORTANT]\ -> Currently this library only focus on CommonJS (CJS) and ECMAScript Modules (ESM). +> [!IMPORTANT] > -> As of version 1.0.0, this library has supported TypeScript projects with various +> ### v1.2.0 +> Added synchronous version for `ls`, `lsFiles`, and `lsDirs`. +> Can be imported from submodule `/sync` as such below: +> ```js +> const { lsFiles } = require('lsfnd/sync'); +> // Or: +> import { lsFiles } from 'lsfnd/sync'; +> ``` +> +> ### v1.0.0 +> This library has supported TypeScript projects with various > module types (i.e., `node16`, `es6`, and many more). Previously, it was only supports > TypeScript projects with module type of `commonjs`. All type declarations in this > library also has been enhanced to more robust and strict, thus improving type safety. From 6a2e53c58a6c9a05f2561cfaa7dea4463c0dda08 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki Date: Thu, 11 Dec 2025 22:04:37 +0700 Subject: [PATCH 8/8] refactor: Re-export `lsTypes` from `lsfnd-sync` module --- src/lsfnd-sync.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lsfnd-sync.ts b/src/lsfnd-sync.ts index f2bb1ea..277d32a 100644 --- a/src/lsfnd-sync.ts +++ b/src/lsfnd-sync.ts @@ -21,6 +21,8 @@ import { 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.