Skip to content

Commit

Permalink
feat: Enhance APIs with file URL path support (#2)
Browse files Browse the repository at this point in the history
This changes brings new features, enhance the functionality APIs, improve documentation, and ensure compatibility across different environments.

Signed-off-by: Ryuu Mitsuki <[email protected]>
  • Loading branch information
mitsuki31 committed Apr 19, 2024
2 parents a3df89a + 6c3a254 commit 61748da
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 46 deletions.
178 changes: 152 additions & 26 deletions src/lsfnd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -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:
Expand All @@ -45,42 +86,89 @@ import type {
* }
* ```
*
* @param dirpath - The directory path to search.
* @param options - Additional options for reading the directory.
* <br>
* <details>
* <summary><b>History</b></summary>
*
* ### 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.
*
* </details>
*
* @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}
* @see {@link lsDirs}
* @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
| LsTypesKeys
| LsTypesValues
| undefined
): Promise<LsResult> {
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(<string> dirpath)
? <string> dirpath
: path.posix.resolve(<string> dirpath);

if (isRegExp(options)) {
match = options;
exclude = undefined;
Expand Down Expand Up @@ -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(<string> dirpath, entry);
const stats: fs.Stats = await fs.promises.stat(entry);
let resultType: boolean = false;

Expand Down Expand Up @@ -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:
Expand All @@ -167,13 +255,32 @@ export async function ls(
* }
* ```
*
* @param dirpath - The directory path to search.
* @param options - Additional options for reading the directory.
* <br>
* <details>
* <summary><b>History</b></summary>
*
* ### 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.
*
* </details>
*
* @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,
Expand All @@ -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<LsResult> {
return ls(dirpath, options, lsTypes.LS_F);
Expand All @@ -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:
Expand All @@ -219,13 +326,32 @@ export async function lsFiles(
* }
* ```
*
* @param dirpath - The directory path to search.
* @param options - Additional options for reading the directory.
* <br>
* <details>
* <summary><b>History</b></summary>
*
* ### 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.
*
* </details>
*
* @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
Expand All @@ -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<LsResult> {
return ls(dirpath, options, lsTypes.LS_D);
Expand Down
6 changes: 5 additions & 1 deletion test/lib/simpletest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
36 changes: 29 additions & 7 deletions test/lsfnd.spec.cjs
Original file line number Diff line number Diff line change
@@ -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
);
Loading

0 comments on commit 61748da

Please sign in to comment.