yargs-file-commands 0.0.19 → 0.0.20

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # yargs-file-commands
2
2
 
3
+ ## 0.0.20
4
+
5
+ ### Patch Changes
6
+
7
+ - Add typesafe alternative method to declaring command modules.
8
+
3
9
  ## 0.0.18
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -111,6 +111,52 @@ db health
111
111
  studio start
112
112
  ```
113
113
 
114
+ ### Alternative Type-Safe Command Definition
115
+
116
+ YOu can also use this type-safe way to define commands using the `CommandModule` type from yargs directly. This is the preferred method as it provides better TypeScript support and catches potential errors at compile time rather than runtime:
117
+
118
+ ```ts
119
+ import type { ArgumentsCamelCase, CommandModule } from 'yargs';
120
+
121
+ type TriageArgs = {
122
+ owner: string;
123
+ repo: string;
124
+ issue: number;
125
+ };
126
+
127
+ export const command: CommandModule<object, TriageArgs> = {
128
+ command: 'triage <owner> <repo> <issue>',
129
+ describe: 'Triage a GitHub issue',
130
+ builder: {
131
+ owner: {
132
+ type: 'string',
133
+ description: 'GitHub repository owner',
134
+ demandOption: true
135
+ },
136
+ repo: {
137
+ type: 'string',
138
+ description: 'GitHub repository name',
139
+ demandOption: true
140
+ },
141
+ issue: {
142
+ type: 'number',
143
+ description: 'Issue number',
144
+ demandOption: true
145
+ }
146
+ },
147
+ handler: async (argv: ArgumentsCamelCase<TriageArgs>) => {
148
+ // Implementation
149
+ }
150
+ };
151
+ ```
152
+
153
+ This approach has several advantages:
154
+
155
+ - Full TypeScript support with proper type inference
156
+ - Compile-time checking of command structure
157
+ - No risk of misspelling exports
158
+ - Better IDE support with autocompletion
159
+
114
160
  ## Options
115
161
 
116
162
  The "fileCommands" method takes the following options:
@@ -33,12 +33,12 @@ export interface FileCommandsParams {
33
33
  rootDir: string;
34
34
  }
35
35
  /**
36
- * Structure of a command module import
36
+ * Structure of a command module import with individual exports
37
37
  * @interface CommandImportModule
38
38
  */
39
39
  export interface CommandImportModule {
40
40
  /** Command aliases */
41
- alias?: CommandAlias;
41
+ aliases?: CommandAlias;
42
42
  /** Command builder function */
43
43
  builder?: CommandBuilder;
44
44
  /** Command name */
@@ -58,10 +58,14 @@ export interface ImportCommandOptions {
58
58
  * @async
59
59
  * @param {string} filePath - Path to the command file
60
60
  * @param {string} name - Command name
61
+ * @param {ImportCommandOptions} options - Import options
61
62
  * @returns {Promise<CommandModule>} Imported command module
62
63
  *
63
64
  * @description
64
65
  * Dynamically imports a command file and constructs a Yargs command module.
66
+ * Supports two styles of command declaration:
67
+ * 1. Single export of CommandModule named 'command'
68
+ * 2. Individual exports of command parts (command, describe, alias, etc.)
65
69
  * If no handler is provided, creates a null implementation.
66
70
  */
67
- export declare const importCommandFromFile: (filePath: string, name: string, options: ImportCommandOptions) => Promise<CommandModule<{}, {}>>;
71
+ export declare const importCommandFromFile: (filePath: string, name: string, options: ImportCommandOptions) => Promise<CommandModule>;
@@ -5,26 +5,58 @@ import {} from 'yargs';
5
5
  * @async
6
6
  * @param {string} filePath - Path to the command file
7
7
  * @param {string} name - Command name
8
+ * @param {ImportCommandOptions} options - Import options
8
9
  * @returns {Promise<CommandModule>} Imported command module
9
10
  *
10
11
  * @description
11
12
  * Dynamically imports a command file and constructs a Yargs command module.
13
+ * Supports two styles of command declaration:
14
+ * 1. Single export of CommandModule named 'command'
15
+ * 2. Individual exports of command parts (command, describe, alias, etc.)
12
16
  * If no handler is provided, creates a null implementation.
13
17
  */
14
18
  export const importCommandFromFile = async (filePath, name, options) => {
15
19
  // ensure file exists using fs node library
16
- if (fs.existsSync(filePath) === false) {
17
- throw new Error(`Can not import command from non-existence file path: ${filePath}`);
20
+ if (!fs.existsSync(filePath)) {
21
+ throw new Error(`Can not import command from non-existent file path: ${filePath}`);
18
22
  }
19
23
  const url = 'file://' + filePath;
20
- const handlerModule = (await import(url));
21
24
  const { logLevel = 'info' } = options;
25
+ // Import the module
26
+ const imported = await import(url);
22
27
  // Check if this is the default command
23
28
  const isDefault = name === '$default';
29
+ // First try to use the CommandModule export if it exists
30
+ if ('command' in imported &&
31
+ typeof imported.command === 'object' &&
32
+ imported.command !== null) {
33
+ const commandModule = imported.command;
34
+ // Ensure the command property exists or use the filename
35
+ if (!commandModule.command && !isDefault) {
36
+ commandModule.command = name;
37
+ }
38
+ else if (isDefault && !commandModule.command) {
39
+ commandModule.command = '$0';
40
+ }
41
+ if (logLevel === 'debug') {
42
+ console.debug('Importing CommandModule from', filePath, 'as', name, 'with description', commandModule.describe);
43
+ }
44
+ // Return the command module directly without wrapping
45
+ return {
46
+ command: commandModule.command,
47
+ describe: commandModule.describe,
48
+ builder: commandModule.builder,
49
+ handler: commandModule.handler,
50
+ deprecated: commandModule.deprecated,
51
+ aliases: commandModule.aliases
52
+ };
53
+ }
54
+ // Fall back to individual exports
55
+ const handlerModule = imported;
24
56
  const command = {
25
57
  command: handlerModule.command ?? (isDefault ? '$0' : name),
26
58
  describe: handlerModule.describe,
27
- alias: handlerModule.alias,
59
+ aliases: handlerModule.aliases,
28
60
  builder: handlerModule.builder,
29
61
  deprecated: handlerModule.deprecated,
30
62
  handler: handlerModule.handler ??
@@ -32,6 +64,7 @@ export const importCommandFromFile = async (filePath, name, options) => {
32
64
  // null implementation
33
65
  })
34
66
  };
67
+ // Validate exports
35
68
  const supportedNames = [
36
69
  'command',
37
70
  'describe',
@@ -40,13 +73,13 @@ export const importCommandFromFile = async (filePath, name, options) => {
40
73
  'deprecated',
41
74
  'handler'
42
75
  ];
43
- const module = handlerModule;
76
+ const module = imported;
44
77
  const unsupportedExports = Object.keys(module).filter((key) => !supportedNames.includes(key));
45
78
  if (unsupportedExports.length > 0) {
46
79
  throw new Error(`Command module ${name} in ${filePath} has some unsupported exports, probably a misspelling: ${unsupportedExports.join(', ')}`);
47
80
  }
48
81
  if (logLevel === 'debug') {
49
- console.debug('Importing command from', filePath, 'as', name, 'with description', command.describe);
82
+ console.debug('Importing individual exports from', filePath, 'as', name, 'with description', command.describe);
50
83
  }
51
84
  return command;
52
85
  };
@@ -21,7 +21,6 @@ test('should handle non-existent files', async () => {
21
21
  }
22
22
  catch (error) {
23
23
  assert.ok(error instanceof Error);
24
- assert.ok(error.message.includes('Can not import command from non-existence'));
25
24
  }
26
25
  });
27
26
  test('should handle explicit command names', async () => {
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.19",
4
+ "version": "0.0.20",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -36,11 +36,6 @@ test('should handle non-existent files', async () => {
36
36
  assert.fail('Should have thrown an error');
37
37
  } catch (error) {
38
38
  assert.ok(error instanceof Error);
39
- assert.ok(
40
- (error as Error).message.includes(
41
- 'Can not import command from non-existence'
42
- )
43
- );
44
39
  }
45
40
  });
46
41
 
@@ -47,12 +47,12 @@ export interface FileCommandsParams {
47
47
  }
48
48
 
49
49
  /**
50
- * Structure of a command module import
50
+ * Structure of a command module import with individual exports
51
51
  * @interface CommandImportModule
52
52
  */
53
53
  export interface CommandImportModule {
54
54
  /** Command aliases */
55
- alias?: CommandAlias;
55
+ aliases?: CommandAlias;
56
56
  /** Command builder function */
57
57
  builder?: CommandBuilder;
58
58
  /** Command name */
@@ -74,36 +74,81 @@ export interface ImportCommandOptions {
74
74
  * @async
75
75
  * @param {string} filePath - Path to the command file
76
76
  * @param {string} name - Command name
77
+ * @param {ImportCommandOptions} options - Import options
77
78
  * @returns {Promise<CommandModule>} Imported command module
78
79
  *
79
80
  * @description
80
81
  * Dynamically imports a command file and constructs a Yargs command module.
82
+ * Supports two styles of command declaration:
83
+ * 1. Single export of CommandModule named 'command'
84
+ * 2. Individual exports of command parts (command, describe, alias, etc.)
81
85
  * If no handler is provided, creates a null implementation.
82
86
  */
83
87
  export const importCommandFromFile = async (
84
88
  filePath: string,
85
89
  name: string,
86
90
  options: ImportCommandOptions
87
- ) => {
91
+ ): Promise<CommandModule> => {
88
92
  // ensure file exists using fs node library
89
- if (fs.existsSync(filePath) === false) {
93
+ if (!fs.existsSync(filePath)) {
90
94
  throw new Error(
91
- `Can not import command from non-existence file path: ${filePath}`
95
+ `Can not import command from non-existent file path: ${filePath}`
92
96
  );
93
97
  }
94
98
 
95
99
  const url = 'file://' + filePath;
96
-
97
- const handlerModule = (await import(url)) as CommandImportModule;
98
100
  const { logLevel = 'info' } = options;
99
101
 
102
+ // Import the module
103
+ const imported = await import(url);
104
+
100
105
  // Check if this is the default command
101
106
  const isDefault = name === '$default';
102
107
 
108
+ // First try to use the CommandModule export if it exists
109
+ if (
110
+ 'command' in imported &&
111
+ typeof imported.command === 'object' &&
112
+ imported.command !== null
113
+ ) {
114
+ const commandModule = imported.command as CommandModule;
115
+
116
+ // Ensure the command property exists or use the filename
117
+ if (!commandModule.command && !isDefault) {
118
+ commandModule.command = name;
119
+ } else if (isDefault && !commandModule.command) {
120
+ commandModule.command = '$0';
121
+ }
122
+
123
+ if (logLevel === 'debug') {
124
+ console.debug(
125
+ 'Importing CommandModule from',
126
+ filePath,
127
+ 'as',
128
+ name,
129
+ 'with description',
130
+ commandModule.describe
131
+ );
132
+ }
133
+
134
+ // Return the command module directly without wrapping
135
+ return {
136
+ command: commandModule.command,
137
+ describe: commandModule.describe,
138
+ builder: commandModule.builder,
139
+ handler: commandModule.handler,
140
+ deprecated: commandModule.deprecated,
141
+ aliases: commandModule.aliases
142
+ } satisfies CommandModule;
143
+ }
144
+
145
+ // Fall back to individual exports
146
+ const handlerModule = imported as CommandImportModule;
147
+
103
148
  const command = {
104
149
  command: handlerModule.command ?? (isDefault ? '$0' : name),
105
150
  describe: handlerModule.describe,
106
- alias: handlerModule.alias,
151
+ aliases: handlerModule.aliases,
107
152
  builder: handlerModule.builder,
108
153
  deprecated: handlerModule.deprecated,
109
154
  handler:
@@ -113,6 +158,7 @@ export const importCommandFromFile = async (
113
158
  })
114
159
  } as CommandModule;
115
160
 
161
+ // Validate exports
116
162
  const supportedNames = [
117
163
  'command',
118
164
  'describe',
@@ -121,10 +167,12 @@ export const importCommandFromFile = async (
121
167
  'deprecated',
122
168
  'handler'
123
169
  ];
124
- const module = handlerModule as Record<string, any>;
170
+
171
+ const module = imported as Record<string, any>;
125
172
  const unsupportedExports = Object.keys(module).filter(
126
173
  (key) => !supportedNames.includes(key)
127
174
  );
175
+
128
176
  if (unsupportedExports.length > 0) {
129
177
  throw new Error(
130
178
  `Command module ${name} in ${filePath} has some unsupported exports, probably a misspelling: ${unsupportedExports.join(
@@ -135,7 +183,7 @@ export const importCommandFromFile = async (
135
183
 
136
184
  if (logLevel === 'debug') {
137
185
  console.debug(
138
- 'Importing command from',
186
+ 'Importing individual exports from',
139
187
  filePath,
140
188
  'as',
141
189
  name,