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