yargs-file-commands
Version:
A yargs helper function that lets you define your commands structure via directory and file naming conventions.
91 lines • 4.55 kB
JavaScript
import { readdir } from 'node:fs/promises';
import path, { join } from 'node:path';
/**
* System-level ignore patterns that ALWAYS apply regardless of user configuration.
* These patterns match system files and hidden files that should never be treated as command files.
* This ensures compatibility across macOS (.DS_Store), Linux, and Windows.
*/
const SYSTEM_IGNORE_PATTERNS = [
/^[.].*/, // Any file or directory starting with a dot (hidden files)
];
/**
* Default ignore patterns for directory scanning (optional, can be overridden).
* These patterns match common files that should not be treated as command files.
* System ignore patterns are always applied in addition to these defaults.
*/
const DEFAULT_IGNORE_PATTERNS = [
/\.(?:test|spec)\.[jt]s$/, // Test files
/__(?:test|spec)__/, // Test directories
/\.d\.ts$/, // TypeScript declaration files
];
/**
* Recursively scans a directory for command files
* @async
* @param {string} dirPath - The directory path to scan
* @param {ScanDirectoryOptions} options - Scanning configuration options
* @returns {Promise<string[]>} Array of full paths to command files
*
* @description
* Performs a recursive directory scan, filtering files based on:
* - Ignore patterns (skips matching files/directories)
* - File extensions (only includes matching files)
* The scan is performed in parallel for better performance.
*/
export const scanDirectory = async (dirPath, commandDir, options = {}) => {
const { ignorePatterns = DEFAULT_IGNORE_PATTERNS, extensions = ['.js', '.ts'], logLevel = 'info', logPrefix = '', } = options;
// Always merge system ignore patterns with user-provided patterns
// System patterns are checked first to ensure system files are never included
const allIgnorePatterns = [...SYSTEM_IGNORE_PATTERNS, ...ignorePatterns];
try {
const entries = await readdir(dirPath, { withFileTypes: true });
// First, filter out entries that match ignore patterns
const entriesToProcess = entries.filter((entry) => {
const fullPath = join(dirPath, entry.name);
const localPath = fullPath.replace(commandDir, '');
// Apply ignore patterns - system patterns are always checked first
const matchingPattern = allIgnorePatterns.find((pattern) => pattern.test(localPath));
if (matchingPattern) {
if (logLevel === 'debug') {
const isSystemPattern = SYSTEM_IGNORE_PATTERNS.some((pattern) => pattern.test(localPath));
// biome-ignore lint/security/noSecrets: This is not a secret, it's a descriptive string for logging
const patternType = isSystemPattern ? 'system ignore pattern' : 'ignorePattern';
console.debug(`${logPrefix}${localPath} - ignoring because it matches ${patternType}: ${matchingPattern.toString()}`);
}
return false;
}
return true;
});
// Process all entries in parallel
const entryResults = await Promise.all(entriesToProcess.map(async (entry) => {
const fullPath = join(dirPath, entry.name);
const localPath = fullPath.replace(commandDir, '');
if (entry.isDirectory()) {
if (logLevel === 'debug') {
console.debug(`${logPrefix}${localPath} - directory, scanning for commands:`);
}
return scanDirectory(fullPath, commandDir, {
...options,
logPrefix: `${logPrefix} `,
});
}
const extension = path.extname(fullPath);
if (!extensions.includes(extension)) {
if (logLevel === 'debug') {
console.debug(`${logPrefix}${localPath} - ignoring as its extension, ${extension}, doesn't match required extension: ${extensions.join(', ')}`);
}
return [];
}
if (logLevel === 'debug') {
console.debug(`${logPrefix}${localPath} - possible command file`);
}
return [fullPath];
}));
// Flatten the results
return entryResults.flat();
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`${logPrefix}Failed to scan directory ${dirPath}: ${errorMessage}. Ensure the directory exists and is accessible.`);
}
};
//# sourceMappingURL=scanDirectory.js.map