yargs-file-commands 0.0.1
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.
- package/LICENSE +9 -0
- package/README.md +142 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/Command.d.ts +6 -0
- package/dist/lib/Command.js +1 -0
- package/dist/lib/buildSegmentTree.d.ts +14 -0
- package/dist/lib/buildSegmentTree.js +65 -0
- package/dist/lib/fileCommands.d.ts +8 -0
- package/dist/lib/fileCommands.js +33 -0
- package/dist/lib/importCommand.d.ts +18 -0
- package/dist/lib/importCommand.js +15 -0
- package/dist/lib/scanDirectory.d.ts +4 -0
- package/dist/lib/scanDirectory.js +31 -0
- package/dist/lib/segmentPath.d.ts +1 -0
- package/dist/lib/segmentPath.js +15 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# MIT LICENSE
|
|
2
|
+
|
|
3
|
+
Copyright 2024, Ben Houston
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Yargs File Commands
|
|
2
|
+
|
|
3
|
+
[![NPM Package][npm]][npm-url]
|
|
4
|
+
[![Build Size][build-size]][build-size-url]
|
|
5
|
+
[![NPM Downloads][npm-downloads]][npmtrends-url]
|
|
6
|
+
|
|
7
|
+
This Yargs helper function lets you define all your commands as individual files and their file names and directory structure defines via implication your nested command structure.
|
|
8
|
+
|
|
9
|
+
Supports both JavaScript and TypeScript (on Node 22+.)
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
_NOTE: This is an ESM-only package._
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install yargs-file-commands
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Example
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createRequire } from 'module';
|
|
23
|
+
import path from 'path';
|
|
24
|
+
import yargs from 'yargs';
|
|
25
|
+
import { fileCommands } from 'yargs-file-commands';
|
|
26
|
+
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
|
|
29
|
+
export const main = async () => {
|
|
30
|
+
const rootCommandDir = path.join(
|
|
31
|
+
import.meta.url.replace('file://', ''),
|
|
32
|
+
'../commands'
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return yargs(process.argv)
|
|
36
|
+
.command(await fileCommands({ rootDirs: [rootCommandDir] }))
|
|
37
|
+
.help().argv;
|
|
38
|
+
};
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
You can use any combination of file names and directories. We support either [NextJS](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes) or [Remix](https://remix.run/docs/en/main/file-conventions/routes) conventions for interpreting filenames and directories.
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
/commands
|
|
45
|
+
├── db
|
|
46
|
+
│ ├── migration
|
|
47
|
+
│ │ └── command.ts // the "db migration" command
|
|
48
|
+
│ └── health.ts // the "db health" command
|
|
49
|
+
└── studio.start.ts // the "studio start" command
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Inside each route handler file, you make the default export the route handler. Here is a simple example:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// commands/studio.start.ts
|
|
56
|
+
|
|
57
|
+
import type { ArgumentsCamelCase, Argv } from 'yargs';
|
|
58
|
+
import type { BaseOptions } from '../options.js';
|
|
59
|
+
|
|
60
|
+
export interface Options extends BaseOptions {
|
|
61
|
+
port?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const describe = 'Studio web interface';
|
|
65
|
+
|
|
66
|
+
export const builder = (args: Argv): Argv<Options> => {
|
|
67
|
+
const result = args.option('port', {
|
|
68
|
+
alias: 'p',
|
|
69
|
+
type: 'number',
|
|
70
|
+
describe: 'Port to listen on'
|
|
71
|
+
});
|
|
72
|
+
return result;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const handler = async (args: ArgumentsCamelCase<Options>) => {
|
|
76
|
+
const config = await getConfig();
|
|
77
|
+
// Implementation
|
|
78
|
+
};
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The above will result in these commands being registered:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
db migration
|
|
85
|
+
db health
|
|
86
|
+
studio start
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Options
|
|
90
|
+
|
|
91
|
+
The "fileCommands" method takes the following options:
|
|
92
|
+
|
|
93
|
+
**routesDirs**
|
|
94
|
+
|
|
95
|
+
- An array of directories where the routes are located relative to the build root folder.
|
|
96
|
+
- Required
|
|
97
|
+
|
|
98
|
+
**extensions**
|
|
99
|
+
|
|
100
|
+
- An array of file extensions for the route files. Files without matching extensions are ignored
|
|
101
|
+
- Default: `[".js", ".ts"]`
|
|
102
|
+
|
|
103
|
+
** ignorePatterns?: RegExp[];
|
|
104
|
+
**
|
|
105
|
+
|
|
106
|
+
- An array of regexs which if matched against a filename or directory, lead it to being ignored/skipped over.
|
|
107
|
+
- Default: `[ /^[\.|_].*/, /\.(test|spec)\.[jt]s$/, /__(test|spec)__/, /\.d\.ts$/ ]`
|
|
108
|
+
|
|
109
|
+
**logLevel**
|
|
110
|
+
|
|
111
|
+
- The verbosity level for the plugin, either `debug` or `info`
|
|
112
|
+
- Default: `"info"`
|
|
113
|
+
|
|
114
|
+
## Plugin Development (for Contributors only)
|
|
115
|
+
|
|
116
|
+
If you want to contribute, just check out [this git project](https://github.com/bhouston/yargs-file-commands) and run the following commands to get going:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
# install dependencies
|
|
120
|
+
npm install
|
|
121
|
+
|
|
122
|
+
# build everything
|
|
123
|
+
npm run build
|
|
124
|
+
|
|
125
|
+
# prettify
|
|
126
|
+
npm run format
|
|
127
|
+
|
|
128
|
+
# eslint
|
|
129
|
+
npm run lint
|
|
130
|
+
|
|
131
|
+
# build and run tests
|
|
132
|
+
npm run test
|
|
133
|
+
|
|
134
|
+
# clean everything, should be like doing a fresh git checkout of the repo.
|
|
135
|
+
npm run clean
|
|
136
|
+
|
|
137
|
+
# publish the npm package
|
|
138
|
+
npm run publish
|
|
139
|
+
|
|
140
|
+
# run example cli
|
|
141
|
+
npx example-cli
|
|
142
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib/fileCommands.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './lib/fileCommands.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CommandModule } from 'yargs';
|
|
2
|
+
import type { Command } from './Command';
|
|
3
|
+
type CommandTreeNode = {
|
|
4
|
+
segmentName: string;
|
|
5
|
+
} & ({
|
|
6
|
+
type: 'internal';
|
|
7
|
+
children: CommandTreeNode[];
|
|
8
|
+
} | {
|
|
9
|
+
type: 'leaf';
|
|
10
|
+
command: Command;
|
|
11
|
+
});
|
|
12
|
+
export declare const buildSegmentTree: (commands: Command[]) => CommandTreeNode[];
|
|
13
|
+
export declare const createCommand: (treeNode: CommandTreeNode) => CommandModule;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const buildSegmentTree = (commands) => {
|
|
2
|
+
const rootTreeNodes = [];
|
|
3
|
+
for (const command of commands) {
|
|
4
|
+
insertIntoTree(rootTreeNodes, command, 0);
|
|
5
|
+
}
|
|
6
|
+
return rootTreeNodes;
|
|
7
|
+
};
|
|
8
|
+
function insertIntoTree(treeNodes, command, depth) {
|
|
9
|
+
// If we've processed all segments, we shouldn't be here
|
|
10
|
+
if (depth >= command.segments.length) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const currentSegmentName = command.segments[depth];
|
|
14
|
+
let currentSegment = treeNodes.find((s) => s.segmentName === currentSegmentName);
|
|
15
|
+
// If this is the last segment, create a leaf node
|
|
16
|
+
if (depth === command.segments.length - 1) {
|
|
17
|
+
if (currentSegment == null) {
|
|
18
|
+
treeNodes.push({
|
|
19
|
+
type: 'leaf',
|
|
20
|
+
segmentName: currentSegmentName,
|
|
21
|
+
command
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
else if (currentSegment.type === 'internal') {
|
|
25
|
+
throw new Error(`Conflict: ${currentSegmentName} is both a directory and a command`);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// Creating or ensuring we have an internal node
|
|
30
|
+
if (currentSegment == null) {
|
|
31
|
+
currentSegment = {
|
|
32
|
+
type: 'internal',
|
|
33
|
+
segmentName: currentSegmentName,
|
|
34
|
+
children: []
|
|
35
|
+
};
|
|
36
|
+
treeNodes.push(currentSegment);
|
|
37
|
+
}
|
|
38
|
+
else if (currentSegment.type === 'leaf') {
|
|
39
|
+
throw new Error(`Conflict: ${currentSegmentName} is both a directory and a command`);
|
|
40
|
+
}
|
|
41
|
+
// Recurse into children
|
|
42
|
+
insertIntoTree(currentSegment.children, command, depth + 1);
|
|
43
|
+
}
|
|
44
|
+
export const createCommand = (treeNode) => {
|
|
45
|
+
if (treeNode.type === 'leaf') {
|
|
46
|
+
return treeNode.command.commandModule;
|
|
47
|
+
}
|
|
48
|
+
const name = treeNode.segmentName;
|
|
49
|
+
// For internal nodes, create a command that registers all children
|
|
50
|
+
const command = {
|
|
51
|
+
command: name,
|
|
52
|
+
describe: `${name} commands`,
|
|
53
|
+
builder: (yargs) => {
|
|
54
|
+
// Register all child segments as subcommands
|
|
55
|
+
yargs.command(treeNode.children.map((child) => createCommand(child)));
|
|
56
|
+
// Demand a subcommand unless we're at the root
|
|
57
|
+
yargs.demandCommand(1, `You must specify a ${name} subcommand`);
|
|
58
|
+
return yargs;
|
|
59
|
+
},
|
|
60
|
+
handler: async () => {
|
|
61
|
+
// Internal nodes don't need handlers as they'll demand subcommands
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return command;
|
|
65
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type FileCommandsOptions = {
|
|
2
|
+
rootDirs: string[];
|
|
3
|
+
extensions?: string[];
|
|
4
|
+
ignorePatterns?: RegExp[];
|
|
5
|
+
logLevel?: 'info' | 'debug';
|
|
6
|
+
};
|
|
7
|
+
export declare const DefaultFileCommandsOptions: Partial<FileCommandsOptions>;
|
|
8
|
+
export declare const fileCommands: (options: FileCommandsOptions) => Promise<import("yargs").CommandModule<{}, {}>[]>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { buildSegmentTree, createCommand } from './buildSegmentTree.js';
|
|
2
|
+
import { importCommandFromFile } from './importCommand.js';
|
|
3
|
+
import { scanDirectory } from './scanDirectory.js';
|
|
4
|
+
import { segmentPath } from './segmentPath.js';
|
|
5
|
+
export const DefaultFileCommandsOptions = {
|
|
6
|
+
extensions: ['.js', '.ts'],
|
|
7
|
+
ignorePatterns: [
|
|
8
|
+
/^[\.|_].*/,
|
|
9
|
+
/\.(test|spec)\.[jt]s$/,
|
|
10
|
+
/__(test|spec)__/,
|
|
11
|
+
/\.d\.ts$/
|
|
12
|
+
],
|
|
13
|
+
logLevel: 'info'
|
|
14
|
+
};
|
|
15
|
+
export const fileCommands = async (options) => {
|
|
16
|
+
const fullOptions = { ...DefaultFileCommandsOptions, ...options };
|
|
17
|
+
const commands = [];
|
|
18
|
+
await Promise.all(options.rootDirs.map(async (rootCommandDir) => {
|
|
19
|
+
const filePaths = await scanDirectory(rootCommandDir, fullOptions);
|
|
20
|
+
const rootDirCommands = await Promise.all(filePaths.map(async (filePath) => {
|
|
21
|
+
const segments = segmentPath(filePath, rootCommandDir);
|
|
22
|
+
return {
|
|
23
|
+
fullPath: filePath,
|
|
24
|
+
segments: segmentPath(filePath, rootCommandDir),
|
|
25
|
+
commandModule: await importCommandFromFile(filePath, segments[segments.length - 1])
|
|
26
|
+
};
|
|
27
|
+
}));
|
|
28
|
+
commands.push(...rootDirCommands);
|
|
29
|
+
}));
|
|
30
|
+
const commandRootNodes = buildSegmentTree(commands);
|
|
31
|
+
const rootCommands = commandRootNodes.map((node) => createCommand(node));
|
|
32
|
+
return rootCommands;
|
|
33
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ArgumentsCamelCase, type CommandBuilder, type CommandModule } from 'yargs';
|
|
2
|
+
export type CommandAlias = readonly string[] | string | undefined;
|
|
3
|
+
export type CommandName = readonly string[] | string | undefined;
|
|
4
|
+
export type CommandDeprecated = boolean | string | undefined;
|
|
5
|
+
export type CommandDescribe = string | false | undefined;
|
|
6
|
+
export type CommandHandler = (args: ArgumentsCamelCase<any>) => void | Promise<any>;
|
|
7
|
+
export interface FileCommandsParams {
|
|
8
|
+
rootDir: string;
|
|
9
|
+
}
|
|
10
|
+
export interface CommandImportModule {
|
|
11
|
+
alias?: CommandAlias;
|
|
12
|
+
builder?: CommandBuilder;
|
|
13
|
+
command?: CommandName;
|
|
14
|
+
deprecated?: CommandDeprecated;
|
|
15
|
+
describe?: CommandDescribe;
|
|
16
|
+
handler?: CommandHandler;
|
|
17
|
+
}
|
|
18
|
+
export declare const importCommandFromFile: (filePath: string, name: string) => Promise<CommandModule<{}, {}>>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {} from 'yargs';
|
|
2
|
+
export const importCommandFromFile = async (filePath, name) => {
|
|
3
|
+
const handlerModule = (await import(filePath));
|
|
4
|
+
return {
|
|
5
|
+
command: name,
|
|
6
|
+
describe: handlerModule.describe,
|
|
7
|
+
alias: handlerModule.alias,
|
|
8
|
+
builder: handlerModule.builder,
|
|
9
|
+
deprecated: handlerModule.deprecated,
|
|
10
|
+
handler: handlerModule.handler ??
|
|
11
|
+
(async (args) => {
|
|
12
|
+
// null implementation
|
|
13
|
+
})
|
|
14
|
+
};
|
|
15
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readdir, stat } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
export const scanDirectory = async (dirPath, options = {}) => {
|
|
4
|
+
const { ignorePatterns = [] } = options;
|
|
5
|
+
// Check if path should be ignored
|
|
6
|
+
const shouldIgnore = ignorePatterns.some((pattern) => pattern.test(dirPath));
|
|
7
|
+
if (shouldIgnore) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const entries = await readdir(dirPath);
|
|
12
|
+
const nestedFilesPromises = entries.map(async (entry) => {
|
|
13
|
+
// apply ignore pattern and early return if matched
|
|
14
|
+
const shouldIgnore = ignorePatterns.some((pattern) => pattern.test(entry));
|
|
15
|
+
if (shouldIgnore) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const fullPath = join(dirPath, entry);
|
|
19
|
+
const stats = await stat(fullPath);
|
|
20
|
+
if (stats.isDirectory()) {
|
|
21
|
+
return scanDirectory(fullPath, options);
|
|
22
|
+
}
|
|
23
|
+
return [fullPath];
|
|
24
|
+
});
|
|
25
|
+
const nestedFiles = await Promise.all(nestedFilesPromises);
|
|
26
|
+
return nestedFiles.flat();
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
throw new Error(`Failed to scan directory ${dirPath}: ${error}`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const segmentPath: (fullPath: string, baseDir: string) => string[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const segmentPath = (fullPath, baseDir) => {
|
|
2
|
+
// Remove base directory and normalize slashes
|
|
3
|
+
const relativePath = fullPath.replace(baseDir, '').replace(/^[/\\]+/, '');
|
|
4
|
+
// Split into path segments and filename
|
|
5
|
+
const allSegments = relativePath.split(/[/\\]/);
|
|
6
|
+
// Process all segments including filename (without extension)
|
|
7
|
+
const processedSegments = allSegments
|
|
8
|
+
// Remove extension from the last segment (filename)
|
|
9
|
+
.map((segment, index, array) => index === array.length - 1 ? segment.replace(/\.[^/.]+$/, '') : segment)
|
|
10
|
+
// Split segments containing periods
|
|
11
|
+
.flatMap((segment) => segment.split('.'))
|
|
12
|
+
// Filter out empty segments and 'command'
|
|
13
|
+
.filter((segment) => segment !== '' && segment !== 'command');
|
|
14
|
+
return processedSegments;
|
|
15
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "yargs-file-commands",
|
|
3
|
+
"description": "A yargs helper function that lets you define your commands structure via directory and file naming conventions.",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"source": "./src/index.ts",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "Ben Houston <neuralsoft@gmail.com> (https://benhouston3d.com)",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"yargs",
|
|
13
|
+
"file-router",
|
|
14
|
+
"files",
|
|
15
|
+
"directory",
|
|
16
|
+
"automatic",
|
|
17
|
+
"commandDir",
|
|
18
|
+
"helper",
|
|
19
|
+
"nodejs",
|
|
20
|
+
"typescript"
|
|
21
|
+
],
|
|
22
|
+
"homepage": "https://github.com/bhouston/yargs-file-commands#readme",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/bhouston/yargs-file-commands"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/bhouston/yargs-file-commands/issues"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"package.json",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"yargs": "^17"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.10.0",
|
|
41
|
+
"@types/yargs": "^17.0.33"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"registry": "https://registry.npmjs.org/",
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=20.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|