diff --git a/src/lsfnd.ts b/src/lsfnd.ts
index 9b76275..260c69d 100644
--- a/src/lsfnd.ts
+++ b/src/lsfnd.ts
@@ -13,6 +13,7 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { isRegExp } from 'node:util';
+import { URL } from 'node:url';
import { lsTypes } from './lsTypes';
import type {
LsEntries,
@@ -22,6 +23,46 @@ import type {
LsTypesValues
} from '../types';
+/**
+ * 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 {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
+ */
+function fileUrlToPath(url: URL | string): string {
+ if ((url instanceof URL && url.protocol !== 'file:')
+ || (typeof url === 'string' && !/^file:(\/\/?|\.\.?\/*)/.test(url))) {
+ throw new URIError('Invalid URL file scheme');
+ }
+ return (url instanceof URL) ? url.pathname : url.replace(/^file:/, '');
+}
+
/**
* Lists files and/or directories in a specified directory path, filtering by a
* regular expression pattern.
@@ -31,8 +72,8 @@ import type {
* directory names using a regular expression.
*
* The additional `options` can be an object or a regex pattern to specify only
- * the {@link LsOptions.match} field. If passed as a `RegExp` object,
- * the additional options for reading the directory will uses default options.
+ * 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:
@@ -45,23 +86,49 @@ import type {
* }
* ```
*
- * @param dirpath - The directory path to search.
- * @param options - Additional options for reading the directory.
+ *
+ *
+ * History
+ *
+ * ### 1.0.0
+ * As of version 1.0.0, this function now accepts file URL paths. This can be
+ * either a string URL path or a `URL` object, and it must follow the `'file:'` protocol.
+ * An `URIError` will be thrown if the specified file URL path has invalid file
+ * URL syntax or is used with unsupported protocols.
+ *
+ * ### 0.1.0
+ * Added in version 0.1.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~lsTypes lsTypes} to check all supported types.
*
* @returns A promise that resolves with an array of string representing the
- * entries result or an empty array if any files and directories doesn't
- * match with the specified filter options.
+ * entries result excluding `'.'` and `'..'` or an empty array (`[]`)
+ * if any files and directories does not match with the specified filter options.
* @throws {Error} If there is an error occurred while reading a directory.
+ * @throws {URIError} If the given URL path contains invalid file URL scheme or
+ * using unsupported protocols.
*
* @example
* // List all installed packages in 'node_modules' directory
* ls('node_modules', { exclude: /\.bin/ }, lsTypes.LS_D)
* .then((dirs) => console.log(dirs));
*
+ * @example
+ * // List current directory using an URL object
+ * const { pathToFileURL } = require('node:url');
+ * // ESM: import { pathToFileURL } from 'node:url';
+ * ls(pathToFileURL('.')).then((entries) =>
+ * console.log(entries));
+ *
* @since 0.1.0
* @see {@link lsTypes}
* @see {@link lsFiles}
@@ -69,7 +136,7 @@ import type {
* @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback fs.readdir}
*/
export async function ls(
- dirpath: string,
+ dirpath: string | URL,
options?: LsOptions | RegExp | undefined,
type?:
| lsTypes
@@ -77,10 +144,31 @@ export async function ls(
| LsTypesValues
| undefined
): Promise {
- const absdirpath: string = path.resolve(dirpath);
+ let absdirpath: string;
let match: string | RegExp,
exclude: string | RegExp | undefined;
+ if (dirpath instanceof URL) {
+ if (dirpath.protocol !== 'file:') {
+ throw new URIError(`Unsupported protocol: '${dirpath.protocol}'`);
+ }
+ dirpath = dirpath.pathname; // Extract the path (without the protocol)
+ } else if (typeof dirpath === 'string') {
+ if (/^[a-zA-Z]+:/.test(dirpath)) {
+ if (!dirpath.startsWith('file:')) {
+ throw new URIError(`Unsupported protocol: '${dirpath.split(':')[0]}:'`);
+ }
+ dirpath = fileUrlToPath(dirpath);
+ }
+ } else {
+ throw new Error('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;
@@ -110,7 +198,7 @@ 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);
+ entry = path.join( dirpath, entry);
const stats: fs.Stats = await fs.promises.stat(entry);
let resultType: boolean = false;
@@ -153,8 +241,8 @@ export async function ls(
* directory names using a regular expression.
*
* The additional `options` can be an object or a regex pattern to specify only
- * the {@link LsOptions.match} field. If passed as a `RegExp` object,
- * the additional options for reading the directory will uses default options.
+ * 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:
@@ -167,13 +255,32 @@ export async function ls(
* }
* ```
*
- * @param dirpath - The directory path to search.
- * @param options - Additional options for reading the directory.
+ *
+ *
+ * History
+ *
+ * ### 1.0.0
+ * As of version 1.0.0, this function now accepts file URL paths. This can be
+ * either a string URL path or a `URL` object, and it must follow the `'file:'` protocol.
+ * An `URIError` will be thrown if the specified file URL path has invalid file
+ * URL syntax or is used with unsupported protocols.
+ *
+ * ### 0.1.0
+ * Added in version 0.1.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 A promise that resolves with an array of string representing the
- * entries result or an empty array if any files doesn't match with
- * the specified filter options.
+ * entries result excluding `'.'` and `'..'` or an empty array (`[]`)
+ * if any files and directories does not match with the specified filter options.
* @throws {Error} If there is an error occurred while reading a directory.
+ * @throws {URIError} If the given URL path contains invalid file URL scheme or
+ * using unsupported protocols.
*
* @example
* // List all JavaScript files in current directory recursively,
@@ -190,7 +297,7 @@ export async function ls(
* @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback fs.readdir}
*/
export async function lsFiles(
- dirpath: string,
+ dirpath: string | URL,
options?: LsOptions | RegExp
): Promise {
return ls(dirpath, options, lsTypes.LS_F);
@@ -200,13 +307,13 @@ export async function lsFiles(
* Lists files in the specified directory path, filtering by a regular
* expression pattern.
*
- * The returned entries are configurable using the additional `options`, such as
- * listing recursively to subdirectories, and filter specific file and/or
+ * 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
- * `match` field. If passed as a `RegExp` object, the additional options for reading
- * the directory will uses default options.
+ * 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:
@@ -219,13 +326,32 @@ export async function lsFiles(
* }
* ```
*
- * @param dirpath - The directory path to search.
- * @param options - Additional options for reading the directory.
+ *
+ *
+ * History
+ *
+ * ### 1.0.0
+ * As of version 1.0.0, this function now accepts file URL paths. This can be
+ * either a string URL path or a `URL` object, and it must follow the `'file:'` protocol.
+ * An `URIError` will be thrown if the specified file URL path has invalid file
+ * URL syntax or is used with unsupported protocols.
+ *
+ * ### 0.1.0
+ * Added in version 0.1.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 A promise that resolves with an array of string representing the
- * entries result or an empty array if any directories doesn't match
- * with the specified filter options.
+ * entries result excluding `'.'` and `'..'` or an empty array (`[]`)
+ * if any files and directories does not match with the specified filter options.
* @throws {Error} If there is an error occurred while reading a directory.
+ * @throws {URIError} If the given URL path contains invalid file URL scheme or
+ * using unsupported protocols.
*
* @example
* // Search and list directory named 'foo' in 'src' directory
@@ -240,7 +366,7 @@ export async function lsFiles(
* @see {@link https://nodejs.org/api/fs.html#fsreaddirpath-options-callback fs.readdir}
*/
export async function lsDirs(
- dirpath: string,
+ dirpath: string | URL,
options?: LsOptions | RegExp
): Promise {
return ls(dirpath, options, lsTypes.LS_D);
diff --git a/test/lib/simpletest.js b/test/lib/simpletest.js
index 4d6bbf7..035744c 100644
--- a/test/lib/simpletest.js
+++ b/test/lib/simpletest.js
@@ -62,5 +62,9 @@ module.exports = {
deepEq: assert.deepStrictEqual,
notDeepEq: assert.notDeepStrictEqual,
throws: assert.throws,
- rejects: assert.rejects
+ doesNotThrow: assert.doesNotThrow,
+ rejects: assert.rejects,
+ doesNotReject: assert.doesNotReject,
+ match: assert.match,
+ doesNotMatch: assert.doesNotMatch
};
diff --git a/test/lsfnd.spec.cjs b/test/lsfnd.spec.cjs
index 162fe20..1cbeb72 100644
--- a/test/lsfnd.spec.cjs
+++ b/test/lsfnd.spec.cjs
@@ -1,29 +1,51 @@
-const { join, basename } = require('node:path');
+/**
+ * A test module for `lsfnd` 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('..');
-const { it, rejects, deepEq } = require('./lib/simpletest');
+const { it, rejects, doesNotReject, deepEq } = require('./lib/simpletest');
+
+const rootDir = path.resolve('..');
+const rootDirPosix = path.posix.resolve('..');
-console.log(`\n\x1b[1m${basename(__filename)}:\x1b[0m`);
+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 expected = [ 'lib', 'lsfnd.spec.cjs', 'lsfnd.spec.mjs' ]
- .map((e) => join(__dirname, e));
+ .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 expected = [ 'lsfnd.spec.cjs', 'lsfnd.spec.mjs' ]
- .map((e) => join(__dirname, e));
+ .map((e) => path.join(__dirname, e));
deepEq(results, expected);
}, false);
+it('list root directory using URL object', async () => {
+ await doesNotReject(ls(pathToFileURL(rootDirPosix)), URIError);
+}, false);
+
+it('list root directory using file URL path', async () => {
+ await doesNotReject(ls('file:'.concat(rootDirPosix)), URIError);
+}, false);
+
it('test `lsDirs` function by listing this file directory', async () => {
const results = await lsDirs(__dirname);
- const expected = [ 'lib' ].map((e) => join(__dirname, e));
+ const expected = [ 'lib' ].map((e) => path.join(__dirname, e));
deepEq(results, expected);
}, false);
-it('throws an error if given directory path not exist', async () => {
+it('throws an error if the given directory path not exist', async () => {
await rejects(ls('./this/is/not/exist/directory/path'), Error);
}, false);
+
+it('throws an URIError if the given file URL path using unsupported protocol',
+ async () => await rejects(ls('http:'.concat(rootDirPosix)), URIError),
+ false
+);
diff --git a/test/lsfnd.spec.mjs b/test/lsfnd.spec.mjs
index 0260156..a84abd1 100644
--- a/test/lsfnd.spec.mjs
+++ b/test/lsfnd.spec.mjs
@@ -1,35 +1,55 @@
-import { join, dirname, basename } from 'node:path';
-import { fileURLToPath } from 'node:url';
+/**
+ * A test module for `lsfnd` 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/index.js';
import test from './lib/simpletest.js';
-const { it, rejects, deepEq } = test;
+const { it, rejects, doesNotReject, 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 = dirname(__filename);
+const __dirname = path.dirname(__filename);
+const rootDir = path.resolve('..');
+const rootDirPosix = path.posix.resolve('..');
-console.log(`\n\x1b[1m${basename(__filename)}:\x1b[0m`);
+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 expected = [ 'lib', 'lsfnd.spec.cjs', 'lsfnd.spec.mjs' ]
- .map((e) => join(__dirname, e));
+ .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 expected = [ 'lsfnd.spec.cjs', 'lsfnd.spec.mjs' ]
- .map((e) => join(__dirname, e));
+ .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 expected = [ 'lib' ].map((e) => join(__dirname, e));
+ const expected = [ 'lib' ].map((e) => path.join(__dirname, e));
deepEq(results, expected);
}, false);
-it('throws an error if given directory path not exist', async () => {
+it('list root directory using URL object', async () => {
+ await doesNotReject(ls(pathToFileURL(rootDirPosix)), URIError);
+}, false);
+
+it('list root directory using file URL path', async () => {
+ await doesNotReject(ls('file:'.concat(rootDirPosix)), URIError);
+}, false);
+
+it('throws an error if the given directory path not exist', async () => {
await rejects(ls('./this/is/not/exist/directory/path'), Error);
}, false);
+
+it('throws an URIError if the given file URL path using unsupported protocol',
+ async () => await rejects(ls('http:'.concat(rootDirPosix)), URIError),
+ false
+);
diff --git a/types/index.d.ts b/types/index.d.ts
index 61e2484..e3cd666 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -113,18 +113,18 @@ export declare interface LsOptions {
// ====== APIs ===== //
export declare function ls(
- dirpath: string,
+ dirpath: string | URL,
options?: LsOptions | RegExp | undefined,
type?: LsTypes | LsTypesKeys | LsTypesValues | undefined
): Promise
export declare function lsFiles(
- dirpath: string,
+ dirpath: string | URL,
options?: LsOptions | RegExp | undefined
): Promise
export declare function lsDirs(
- dirpath: string,
+ dirpath: string | URL,
options?: LsOptions | RegExp | undefined
): Promise