UNPKG

yargs-file-commands

Version:

A yargs helper function that lets you define your commands structure via directory and file naming conventions.

124 lines 5.52 kB
import path from 'node:path'; import { buildSegmentTree, createCommand, logCommandTree } from './buildSegmentTree.js'; import { importCommandFromFile } from './importCommand.js'; import { scanDirectory } from './scanDirectory.js'; import { segmentPath } from './segmentPath.js'; import { validatePositionals } from './validatePositionals.js'; /** * Default configuration options for file-based commands * @constant * @type {Partial<FileCommandsOptions>} */ export const DefaultFileCommandsOptions = { /** Default directories to scan for command files */ commandDirs: [], /** Default file extensions to process */ extensions: ['.js', '.ts'], /** Default patterns to ignore when scanning directories. * Note: System files (files starting with '.') are ALWAYS ignored regardless of these patterns. * These defaults can be overridden by providing your own ignorePatterns. */ ignorePatterns: [ /\.(?:test|spec)\.[jt]s$/, // Test files /__(?:test|spec)__/, // Test directories /\.d\.ts$/, // TypeScript declaration files ], /** Default logging level */ logLevel: 'info', /** Default log prefix */ logPrefix: ' ', /** Default validation setting - enabled by default */ validation: true, }; /** * Generates a command tree structure from files in specified directories * @async * @param {FileCommandsOptions} options - Configuration options for command generation * @returns {Promise<Command[]>} Array of root-level commands with their nested subcommands * * @description * This function scans the specified directories for command files and builds a hierarchical * command structure based on the file system layout. It processes files in parallel for better * performance and supports nested commands through directory structure. * * The function will: * 1. Scan all specified command directories * 2. Process found files to extract command information * 3. Build a tree structure based on file paths * 4. Convert the tree into a command hierarchy */ export const fileCommands = async (options) => { const fullOptions = { ...DefaultFileCommandsOptions, ...options, }; // validate extensions have dots in them if (fullOptions.extensions.some((ext) => !ext.startsWith('.'))) { throw new Error(`Invalid extensions provided, must start with a dot: ${fullOptions.extensions.join(', ')}`); } // check for empty list of directories to scan if (fullOptions.commandDirs.length === 0) { throw new Error('No command directories provided'); } // throw if some command directories are not absolute, first filter to find non-absolute an then throw, listing those that are not absolute const nonAbsoluteDirs = fullOptions.commandDirs.filter((dir) => !path.isAbsolute(dir)); if (nonAbsoluteDirs.length > 0) { throw new Error(`Command directories must be absolute paths: ${nonAbsoluteDirs.join(', ')}`); } const commands = []; // Process all command directories in parallel const directoryResults = await Promise.all(fullOptions.commandDirs.map(async (commandDir) => { const fullPath = path.resolve(commandDir); if (fullOptions.logLevel === 'debug') { console.debug(`Scanning directory for commands: ${fullPath}`); } const filePaths = await scanDirectory(commandDir, commandDir, fullOptions); return { commandDir, filePaths }; })); if (fullOptions.logLevel === 'debug') { console.debug(`Importing found commands:`); } // Process all files in parallel const fileResults = await Promise.all(directoryResults.flatMap(({ commandDir, filePaths }) => filePaths.map(async (filePath) => { const localPath = path.relative(commandDir, filePath); const segments = segmentPath(filePath, commandDir); // Remove extension (last segment) if there are multiple segments // If there's only one segment, it means the file has no name (e.g., .js) if (segments.length > 1) { segments.pop(); // remove extension. } else if (segments.length === 0) { throw new Error(`No segments found for file: ${filePath}`); } if (fullOptions.logLevel === 'debug') { console.debug(` ${localPath} - importing command module`); } const lastSegment = segments[segments.length - 1]; if (lastSegment === undefined) { throw new Error(`No segments found for file: ${filePath}`); } const commandModule = await importCommandFromFile(filePath, lastSegment, fullOptions); // Validate positional arguments if validation is enabled if (fullOptions.validation) { await validatePositionals(commandModule, filePath); } return { fullPath: filePath, segments, commandModule, }; }))); commands.push(...fileResults); // check if no commands were found if (commands.length === 0) { throw new Error(`No commands found in specified directories: ${fullOptions.commandDirs.join(', ')}`); } const commandRootNodes = buildSegmentTree(commands); if (fullOptions.logLevel === 'debug') { console.debug('Command tree structure:'); logCommandTree(commandRootNodes, 1); } const rootCommands = commandRootNodes.map((node) => createCommand(node)); return rootCommands; }; //# sourceMappingURL=fileCommands.js.map