From 2638e4f897c0610ce0cad477ef32c1fb4cbeb7dc Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki Date: Fri, 19 Apr 2024 17:41:32 +0700 Subject: [PATCH 1/6] feat: Add an internal function to convert file URL This function behave the same as NodeJS `url.fileURLToPath` function, but that makes this function differ is that this function supports relative paths being passed in the argument (e.g., "file:./foo/bar"). If the given URL is have non-file protocols or contains invalid path structures, an URIError will be thrown. This function will be used as part of `ls*` functions to support file URL paths. In addition to this change, I've also added the documentation for the function. --- src/lsfnd.ts | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/lsfnd.ts b/src/lsfnd.ts index 9b76275..f3bee00 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 not `file:` protocol 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:./relative/file.txt'); + * console.log(filePath); // Output: "./relative/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. From 871ab46128359c3ba791e6ad600d4eb7dae27553 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki Date: Fri, 19 Apr 2024 17:49:08 +0700 Subject: [PATCH 2/6] feat: Add file URL support for `ls*` functions Now all `ls*` functions have supported file URL path, this can be a string URL path or an URL object with 'file:' protocol. Note: Please note, that this function currently only supported 'file:' protocol to resolve and list the specified directory. --- src/lsfnd.ts | 38 +++++++++++++++++++++++++++++++++----- types/index.d.ts | 6 +++--- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/lsfnd.ts b/src/lsfnd.ts index f3bee00..03008d9 100644 --- a/src/lsfnd.ts +++ b/src/lsfnd.ts @@ -103,6 +103,13 @@ function fileUrlToPath(url: URL | string): string { * 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} @@ -110,7 +117,7 @@ function fileUrlToPath(url: URL | string): string { * @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 @@ -118,10 +125,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.resolve( dirpath); + if (isRegExp(options)) { match = options; exclude = undefined; @@ -151,7 +179,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; @@ -231,7 +259,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); @@ -281,7 +309,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/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 From fbf739a0a6a611c3f2a2afec64dbbe1397e79812 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki Date: Fri, 19 Apr 2024 19:11:46 +0700 Subject: [PATCH 3/6] test(simpletest): Include several assert functions Included functions (from 'node:assert' module): - doesNotThrow: assert.doesNotThrow, - rejects: assert.rejects, - doesNotReject: assert.doesNotReject, - match: assert.match, - doesNotMatch: assert.doesNotMatch --- test/lib/simpletest.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 }; From d6c91079d95bb1e60733b81e7a075a943f3a5657 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki Date: Fri, 19 Apr 2024 20:39:26 +0700 Subject: [PATCH 4/6] refactor(ls): Fix absolute path resolver on Windows The path passed to `fs.readdir` function can't be a Windows path, instead it must a node path (i.e., similar to POSIX path). That's why we need to resolve the relative path to absolute with format of POSIX path, so the path can be used to the `fs.readdir` function without getting an internal error. --- src/lsfnd.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lsfnd.ts b/src/lsfnd.ts index 03008d9..6b9a14a 100644 --- a/src/lsfnd.ts +++ b/src/lsfnd.ts @@ -148,7 +148,7 @@ export async function ls( // Resolve its absolute path absdirpath = path.isAbsolute( dirpath) ? dirpath - : path.resolve( dirpath); + : path.posix.resolve( dirpath); if (isRegExp(options)) { match = options; From 14daa9a126152e0ec1158769136ac70d1ac63077 Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki Date: Fri, 19 Apr 2024 22:09:44 +0700 Subject: [PATCH 5/6] docs: Update and improve the APIs documentation ALso added a history section to describe changes to the APIs in every versions. --- src/lsfnd.ts | 115 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 86 insertions(+), 29 deletions(-) diff --git a/src/lsfnd.ts b/src/lsfnd.ts index 6b9a14a..260c69d 100644 --- a/src/lsfnd.ts +++ b/src/lsfnd.ts @@ -25,30 +25,30 @@ import type { /** * 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"). + * 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 not `file:` protocol or invalid path + * 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:./relative/file.txt'); - * console.log(filePath); // Output: "./relative/file.txt" + * 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} @@ -72,8 +72,8 @@ function fileUrlToPath(url: URL | string): string { * 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: @@ -86,17 +86,36 @@ function fileUrlToPath(url: URL | string): string { * } * ``` * - * @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 @@ -222,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: @@ -236,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, @@ -269,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: @@ -288,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 From 6c3a254371df588c480147c2573a85569667816c Mon Sep 17 00:00:00 2001 From: Ryuu Mitsuki Date: Fri, 19 Apr 2024 22:12:48 +0700 Subject: [PATCH 6/6] test: Introduce new test cases Added new test cases to test the APIs whether it correctly accepts file URL path and a `URL` object with 'file:' protocol, and correctly throws an `URIError` if the provided URL path using unsupported protocols. --- test/lsfnd.spec.cjs | 36 +++++++++++++++++++++++++++++------- test/lsfnd.spec.mjs | 38 +++++++++++++++++++++++++++++--------- 2 files changed, 58 insertions(+), 16 deletions(-) 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 +);