yargs-file-commands 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { describe, it } from 'node:test';
3
- import { buildSegmentTree } from '../lib/buildSegmentTree.js';
3
+ import { buildSegmentTree } from './buildSegmentTree.js';
4
4
  describe('buildSegmentTree', async () => {
5
- it('should build correct tree structure', async () => {
5
+ it('should build correct tree structure', () => {
6
6
  const commands = [
7
7
  {
8
8
  fullPath: '/commands/db/migration/command.js',
@@ -42,11 +42,11 @@ describe('buildSegmentTree', async () => {
42
42
  .sort();
43
43
  assert.deepEqual(childSegments, ['health', 'migration'], 'Should have "health" and "migration" sub-commands');
44
44
  });
45
- it('should handle empty input', async () => {
45
+ it('should handle empty input', () => {
46
46
  const tree = buildSegmentTree([]);
47
47
  assert.equal(tree.length, 0, 'Should return empty array for empty input');
48
48
  });
49
- it('should handle single command', async () => {
49
+ it('should handle single command', () => {
50
50
  const commands = [
51
51
  {
52
52
  fullPath: '/commands/test.ts',
@@ -1,23 +1,18 @@
1
+ import { type ScanDirectoryOptions } from './scanDirectory.js';
1
2
  /**
2
3
  * Configuration options for file-based command generation
3
4
  * @interface FileCommandsOptions
4
5
  */
5
- export type FileCommandsOptions = {
6
+ export type FileCommandsOptions = ScanDirectoryOptions & {
6
7
  /** Array of directory paths to scan for command files */
7
8
  commandDirs: string[];
8
- /** File extensions to consider when scanning for command files */
9
- extensions?: string[];
10
- /** Regular expressions for patterns to ignore when scanning directories */
11
- ignorePatterns?: RegExp[];
12
- /** Logging verbosity level */
13
- logLevel?: 'info' | 'debug';
14
9
  };
15
10
  /**
16
11
  * Default configuration options for file-based commands
17
12
  * @constant
18
13
  * @type {Partial<FileCommandsOptions>}
19
14
  */
20
- export declare const DefaultFileCommandsOptions: Partial<FileCommandsOptions>;
15
+ export declare const DefaultFileCommandsOptions: Required<FileCommandsOptions>;
21
16
  /**
22
17
  * Generates a command tree structure from files in specified directories
23
18
  * @async
@@ -1,3 +1,4 @@
1
+ import path from 'path';
1
2
  import { buildSegmentTree, createCommand, logCommandTree } from './buildSegmentTree.js';
2
3
  import { importCommandFromFile } from './importCommand.js';
3
4
  import { scanDirectory } from './scanDirectory.js';
@@ -8,6 +9,8 @@ import { segmentPath } from './segmentPath.js';
8
9
  * @type {Partial<FileCommandsOptions>}
9
10
  */
10
11
  export const DefaultFileCommandsOptions = {
12
+ /** Default directories to scan for command files */
13
+ commandDirs: [],
11
14
  /** Default file extensions to process */
12
15
  extensions: ['.js', '.ts'],
13
16
  /** Default patterns to ignore when scanning directories */
@@ -18,7 +21,9 @@ export const DefaultFileCommandsOptions = {
18
21
  /\.d\.ts$/ // TypeScript declaration files
19
22
  ],
20
23
  /** Default logging level */
21
- logLevel: 'info'
24
+ logLevel: 'info',
25
+ /** Default log prefix */
26
+ logPrefix: ' '
22
27
  };
23
28
  /**
24
29
  * Generates a command tree structure from files in specified directories
@@ -38,23 +43,41 @@ export const DefaultFileCommandsOptions = {
38
43
  * 4. Convert the tree into a command hierarchy
39
44
  */
40
45
  export const fileCommands = async (options) => {
41
- const fullOptions = { ...DefaultFileCommandsOptions, ...options };
46
+ const fullOptions = {
47
+ ...DefaultFileCommandsOptions,
48
+ ...options
49
+ };
50
+ // validate extensions have dots in them
51
+ if (fullOptions.extensions.some((ext) => !ext.startsWith('.'))) {
52
+ throw new Error(`Invalid extensions provided, must start with a dot: ${fullOptions.extensions.join(', ')}`);
53
+ }
54
+ // check for empty list of directories to scan
55
+ if (fullOptions.commandDirs.length === 0) {
56
+ throw new Error('No command directories provided');
57
+ }
42
58
  const commands = [];
43
- await Promise.all(options.commandDirs.map(async (commandDir) => {
44
- const filePaths = await scanDirectory(commandDir, fullOptions);
45
- const rootDirCommands = await Promise.all(filePaths.map(async (filePath) => {
59
+ for (const commandDir of fullOptions.commandDirs) {
60
+ const fullPath = path.resolve(commandDir);
61
+ console.debug(`Scanning directory for commands: ${fullPath}`);
62
+ const filePaths = await scanDirectory(commandDir, commandDir, fullOptions);
63
+ for (const filePath of filePaths) {
46
64
  const segments = segmentPath(filePath, commandDir);
47
- return {
65
+ segments.pop(); // remove extension.
66
+ commands.push({
48
67
  fullPath: filePath,
49
- segments: segmentPath(filePath, commandDir),
68
+ segments,
50
69
  commandModule: await importCommandFromFile(filePath, segments[segments.length - 1], fullOptions)
51
- };
52
- }));
53
- commands.push(...rootDirCommands);
54
- }));
70
+ });
71
+ }
72
+ }
73
+ // check if no commands were found
74
+ if (commands.length === 0) {
75
+ throw new Error(`No commands found in specified directories: ${fullOptions.commandDirs.join(', ')}`);
76
+ }
55
77
  const commandRootNodes = buildSegmentTree(commands);
56
78
  if (fullOptions.logLevel === 'debug') {
57
- logCommandTree(commandRootNodes);
79
+ console.debug('Command tree structure:');
80
+ logCommandTree(commandRootNodes, 1);
58
81
  }
59
82
  const rootCommands = commandRootNodes.map((node) => createCommand(node));
60
83
  return rootCommands;
@@ -7,7 +7,7 @@ import { hideBin } from 'yargs/helpers';
7
7
  import { fileCommands } from '../lib/fileCommands.js';
8
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
9
  describe('fileCommands', async () => {
10
- it('should load commands from directory structure', async () => {
10
+ await it('should load commands from directory structure', async () => {
11
11
  const commandsDir = path.join(__dirname, 'fixtures', 'commands');
12
12
  const commands = await fileCommands({
13
13
  commandDirs: [commandsDir],
@@ -27,7 +27,7 @@ describe('fileCommands', async () => {
27
27
  const description = rootCommand.describe;
28
28
  assert(typeof description === 'string' && description.includes('db'), 'Command should have correct description');
29
29
  });
30
- it('should respect ignore patterns', async () => {
30
+ await it('should respect ignore patterns', async () => {
31
31
  const commandsDir = path.join(__dirname, 'fixtures', 'commands');
32
32
  const commands = await fileCommands({
33
33
  commandDirs: [commandsDir],
@@ -5,14 +5,14 @@ import { fileURLToPath } from 'node:url';
5
5
  import { importCommandFromFile } from '../lib/importCommand.js';
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
7
  describe('importCommandFromFile', async () => {
8
- it('should import command module correctly', async () => {
8
+ await it('should import command module correctly', async () => {
9
9
  const commandPath = path.join(__dirname, 'fixtures', 'commands', 'db', 'health.js');
10
10
  const command = await importCommandFromFile(commandPath, 'health', {});
11
11
  assert(command.describe, 'Should have describe property');
12
12
  assert(typeof command.builder === 'function', 'Should have builder function');
13
13
  assert(typeof command.handler === 'function', 'Should have handler function');
14
14
  });
15
- it('should handle non-existent files', async () => {
15
+ await it('should handle non-existent files', async () => {
16
16
  const nonExistentPath = path.join(__dirname, 'fixtures', 'commands', 'non-existent.ts');
17
17
  await assert.rejects(async () => {
18
18
  await importCommandFromFile(nonExistentPath, 'non-existent', {});
@@ -3,11 +3,14 @@
3
3
  * @interface ScanDirectoryOptions
4
4
  */
5
5
  export interface ScanDirectoryOptions {
6
- /** Regular expressions for patterns to ignore */
7
- ignorePatterns?: RegExp[];
8
- /** File extensions to include in the scan */
6
+ /** File extensions to consider when scanning for command files */
9
7
  extensions?: string[];
8
+ /** Regular expressions for patterns to ignore when scanning directories */
9
+ ignorePatterns?: RegExp[];
10
+ /** Logging verbosity level */
10
11
  logLevel?: 'info' | 'debug';
12
+ /** Prefix for log messages */
13
+ logPrefix?: string;
11
14
  }
12
15
  /**
13
16
  * Recursively scans a directory for command files
@@ -22,4 +25,4 @@ export interface ScanDirectoryOptions {
22
25
  * - File extensions (only includes matching files)
23
26
  * The scan is performed in parallel for better performance.
24
27
  */
25
- export declare const scanDirectory: (dirPath: string, options?: ScanDirectoryOptions) => Promise<string[]>;
28
+ export declare const scanDirectory: (dirPath: string, commandDir: string, options?: ScanDirectoryOptions) => Promise<string[]>;
@@ -13,56 +13,50 @@ import path, { join } from 'path';
13
13
  * - File extensions (only includes matching files)
14
14
  * The scan is performed in parallel for better performance.
15
15
  */
16
- export const scanDirectory = async (dirPath, options = {}) => {
17
- const { ignorePatterns = [], extensions = [], logLevel = 'info' } = options;
18
- const rootedDirPath = path.resolve(dirPath);
19
- if (logLevel === 'debug') {
20
- console.debug(`Inspecting directory for possible commands: ${rootedDirPath}`);
21
- }
22
- // Check if path should be ignored
23
- const shouldIgnore = ignorePatterns.some((pattern) => pattern.test(dirPath));
24
- if (shouldIgnore) {
25
- if (logLevel === 'debug') {
26
- console.debug(`Ignoring directory because of ignorePatterns match: ${rootedDirPath}, ${ignorePatterns
27
- .filter((pattern) => pattern.test(dirPath))
28
- .join(', ')}`);
29
- }
30
- return [];
31
- }
16
+ export const scanDirectory = async (dirPath, commandDir, options = {}) => {
17
+ const { ignorePatterns = [], extensions = ['.js', '.ts'], logLevel = 'info', logPrefix = '' } = options;
32
18
  try {
33
19
  const entries = await readdir(dirPath);
34
- if (logLevel === 'debug') {
35
- console.log(`Found ${entries.length} items in current directory`);
36
- }
37
- const nestedFilesPromises = entries.map(async (entry) => {
20
+ const commandPaths = [];
21
+ for (const entry of entries) {
38
22
  const fullPath = join(dirPath, entry);
23
+ const localPath = fullPath.replace(commandDir, '');
39
24
  // apply ignore pattern and early return if matched
40
- const shouldIgnore = ignorePatterns.some((pattern) => pattern.test(entry));
25
+ const shouldIgnore = ignorePatterns.some((pattern) => pattern.test(localPath));
41
26
  if (shouldIgnore) {
42
27
  if (logLevel === 'debug') {
43
- console.debug(`Ignoring file because of ignorePatterns match: ${path.resolve(fullPath)}, ${ignorePatterns
44
- .filter((pattern) => pattern.test(entry))
28
+ console.debug(`${logPrefix}${localPath} - ignoring because it matches ignorePattern: ${ignorePatterns
29
+ .filter((pattern) => pattern.test(localPath))
45
30
  .join(', ')}`);
46
31
  }
47
- return [];
32
+ continue;
48
33
  }
49
34
  const stats = await stat(fullPath);
50
35
  if (stats.isDirectory()) {
51
- return scanDirectory(fullPath, options);
36
+ if (logLevel === 'debug') {
37
+ console.debug(`${logPrefix}${localPath} - directory, scanning for commands:`);
38
+ }
39
+ commandPaths.push(...(await scanDirectory(fullPath, commandDir, {
40
+ ...options,
41
+ logPrefix: `${logPrefix} `
42
+ })));
43
+ continue;
52
44
  }
53
45
  const extension = path.extname(fullPath);
54
46
  if (!extensions.includes(extension)) {
55
- return [];
47
+ if (logLevel === 'debug') {
48
+ console.debug(`${logPrefix}${localPath} - ignoring as its extension, ${extension}, doesn't match required extension: ${extensions.join(', ')}`);
49
+ }
50
+ continue;
56
51
  }
57
52
  if (logLevel === 'debug') {
58
- console.debug(`Inspecting file as possible command: ${fullPath}`);
53
+ console.debug(`${logPrefix}${localPath} - possible command file`);
59
54
  }
60
- return [fullPath];
61
- });
62
- const nestedFiles = await Promise.all(nestedFilesPromises);
63
- return nestedFiles.flat();
55
+ commandPaths.push(fullPath);
56
+ }
57
+ return commandPaths;
64
58
  }
65
59
  catch (error) {
66
- throw new Error(`Failed to scan directory ${dirPath}: ${error}`);
60
+ throw new Error(`${logPrefix}Failed to scan directory ${dirPath}: ${error}`);
67
61
  }
68
62
  };
@@ -0,0 +1,46 @@
1
+ import assert from 'node:assert/strict';
2
+ import path from 'node:path';
3
+ import { describe, it } from 'node:test';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { scanDirectory } from '../lib/scanDirectory.js';
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ describe('scanDirectory', async () => {
8
+ await it('should find all command files in directory', async () => {
9
+ const commandsDir = path.join(__dirname, 'fixtures', 'commands');
10
+ console.log('Scan Directory: ', commandsDir);
11
+ const files = await scanDirectory(commandsDir, commandsDir, {
12
+ extensions: ['.js'],
13
+ logLevel: 'debug'
14
+ });
15
+ assert.equal(files.length, 2, `Should find two command files, instead found: ${files.join(', ')}`);
16
+ assert(files.some((f) => f.includes('health.js')), `Should find health.js, instead found: ${files.join(', ')}`);
17
+ assert(files.some((f) => f.includes('command.js')), `Should find command.js, instead found: ${files.join(', ')}`);
18
+ });
19
+ await it('should respect ignore patterns', async () => {
20
+ const commandsDir = path.join(__dirname, 'fixtures', 'commands');
21
+ console.log('Scan Directory: ', commandsDir);
22
+ const files = await scanDirectory(commandsDir, commandsDir, {
23
+ extensions: ['.js'],
24
+ ignorePatterns: [/health/],
25
+ logLevel: 'debug'
26
+ });
27
+ assert.equal(files.length, 1, `Should find one command file, instead found: ${files.join(', ')}`);
28
+ assert(files.some((f) => f.includes('command.js')), `Should find command.js, instead found: ${files.join(', ')}`);
29
+ assert(!files.some((f) => f.includes('health.js')), `Should not find health.js, instead found: ${files.join(', ')}`);
30
+ });
31
+ await it('should handle non-existent directories', async () => {
32
+ const nonExistentDir = path.join(__dirname, 'fixtures', 'non-existent');
33
+ try {
34
+ console.log('Scan Directory: ', nonExistentDir);
35
+ await scanDirectory(nonExistentDir, nonExistentDir, {
36
+ extensions: ['.js'],
37
+ logLevel: 'debug'
38
+ });
39
+ assert.fail('Should have thrown an error');
40
+ }
41
+ catch (error) {
42
+ assert(error instanceof Error);
43
+ assert(error.message.includes('ENOENT'), 'Error should indicate directory not found');
44
+ }
45
+ });
46
+ });
@@ -1,8 +1,8 @@
1
- import { test } from 'node:test';
1
+ import { describe, it } from 'node:test';
2
2
  import assert from 'assert';
3
3
  import { segmentPath } from './segmentPath.js';
4
- await test('segmentPath', async () => {
5
- await test('should segment a path correctly', () => {
4
+ describe('segmentPath', () => {
5
+ it('should segment a path correctly', () => {
6
6
  const fullPath = '/Users/username/Coding/Personal/yargs-file-commands/packages/yargs-file-commands/src/lib/segmentPath.ts';
7
7
  const baseDir = '/Users/username/Coding/Personal/yargs-file-commands/';
8
8
  const expected = [
@@ -16,7 +16,7 @@ await test('segmentPath', async () => {
16
16
  const result = segmentPath(fullPath, baseDir);
17
17
  assert.deepStrictEqual(result, expected);
18
18
  });
19
- await test('should handle paths with periods correctly', () => {
19
+ it('should handle paths with periods correctly', () => {
20
20
  const fullPath = '/Users/username/Coding/Personal/yargs-file-commands/packages/yargs-file-commands/src/lib/segmentPath.test.ts';
21
21
  const baseDir = '/Users/username/Coding/Personal/yargs-file-commands/';
22
22
  const expected = [
@@ -31,7 +31,7 @@ await test('segmentPath', async () => {
31
31
  const result = segmentPath(fullPath, baseDir);
32
32
  assert.deepStrictEqual(result, expected);
33
33
  });
34
- await test('should filter out "command" segments', () => {
34
+ it('should filter out "command" segments', () => {
35
35
  const fullPath = '/Users/username/Coding/Personal/yargs-file-commands/packages/yargs-file-commands/src/lib/commandPath.ts';
36
36
  const baseDir = '/Users/username/Coding/Personal/yargs-file-commands/';
37
37
  const expected = [
@@ -45,7 +45,7 @@ await test('segmentPath', async () => {
45
45
  const result = segmentPath(fullPath, baseDir);
46
46
  assert.deepStrictEqual(result, expected);
47
47
  });
48
- test('should handle empty segments correctly', () => {
48
+ it('should handle empty segments correctly', () => {
49
49
  const fullPath = '/Users/username/Coding/Personal/yargs-file-commands/packages/yargs-file-commands/src/lib/.hiddenFile';
50
50
  const baseDir = '/Users/username/Coding/Personal/yargs-file-commands/';
51
51
  const expected = [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "yargs-file-commands",
3
3
  "description": "A yargs helper function that lets you define your commands structure via directory and file naming conventions.",
4
- "version": "0.0.8",
4
+ "version": "0.0.10",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -19,13 +19,6 @@
19
19
  "nodejs",
20
20
  "typescript"
21
21
  ],
22
- "scripts": {
23
- "build": "tsc",
24
- "typecheck": "tsc --noEmit",
25
- "test": "tsc && node --test dist/**/*.test.js",
26
- "lint": "eslint --fix \"src/**/*.{ts,tsx}\"",
27
- "format": "prettier \"src/**/*.{js,jsx,css,md,html,ts,tsx,json,yaml}\" --check"
28
- },
29
22
  "homepage": "https://github.com/bhouston/yargs-file-commands#readme",
30
23
  "repository": {
31
24
  "type": "git",
@@ -54,5 +47,12 @@
54
47
  },
55
48
  "engines": {
56
49
  "node": ">=20.0.0"
50
+ },
51
+ "scripts": {
52
+ "build": "tsc",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "tsc && node --test dist/**/*.test.js --test-concurrency=1",
55
+ "lint": "eslint --fix \"src/**/*.{ts,tsx}\"",
56
+ "format": "prettier \"src/**/*.{js,jsx,css,md,html,ts,tsx,json,yaml}\" --check"
57
57
  }
58
- }
58
+ }
@@ -1,41 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import path from 'node:path';
3
- import { describe, it } from 'node:test';
4
- import { fileURLToPath } from 'node:url';
5
- import { scanDirectory } from '../lib/scanDirectory.js';
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- describe('scanDirectory', async () => {
8
- it('should find all command files in directory', async () => {
9
- const commandsDir = path.join(__dirname, 'fixtures', 'commands');
10
- const files = await scanDirectory(commandsDir, {
11
- extensions: ['.js'],
12
- ignorePatterns: []
13
- });
14
- assert.equal(files.length, 2, 'Should find two command files');
15
- assert(files.some((f) => f.includes('health.js')), 'Should find health.js');
16
- assert(files.some((f) => f.includes('command.js')), 'Should find command.js');
17
- });
18
- it('should respect ignore patterns', async () => {
19
- const commandsDir = path.join(__dirname, 'fixtures', 'commands');
20
- const files = await scanDirectory(commandsDir, {
21
- extensions: ['.js'],
22
- ignorePatterns: [/health/]
23
- });
24
- assert.equal(files.length, 1, 'Should find one command file');
25
- assert(files.some((f) => f.includes('command.js')), 'Should find command.js');
26
- assert(!files.some((f) => f.includes('health.js')), 'Should not find health.js');
27
- });
28
- it('should handle non-existent directories', async () => {
29
- const nonExistentDir = path.join(__dirname, 'fixtures', 'non-existent');
30
- try {
31
- await scanDirectory(nonExistentDir, {
32
- extensions: ['.js']
33
- });
34
- assert.fail('Should have thrown an error');
35
- }
36
- catch (error) {
37
- assert(error instanceof Error);
38
- assert(error.message.includes('ENOENT'), 'Error should indicate directory not found');
39
- }
40
- });
41
- });
File without changes
File without changes
File without changes
File without changes