cli-forge 0.10.1 → 0.12.0

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.
Files changed (103) hide show
  1. package/.eslintrc.json +35 -0
  2. package/LICENSE.md +5 -0
  3. package/README.md +181 -5
  4. package/cli.js +9 -0
  5. package/dist/bin/cli.d.ts +23 -0
  6. package/{bin → dist/bin}/cli.js +2 -2
  7. package/dist/bin/cli.js.map +1 -0
  8. package/dist/bin/commands/generate-documentation.d.ts +14 -0
  9. package/{bin → dist/bin}/commands/generate-documentation.js +58 -11
  10. package/dist/bin/commands/generate-documentation.js.map +1 -0
  11. package/{bin → dist/bin}/commands/init.d.ts +11 -11
  12. package/{bin → dist/bin}/commands/init.js +11 -6
  13. package/dist/bin/commands/init.js.map +1 -0
  14. package/dist/bin/utils/fs.js.map +1 -0
  15. package/{src → dist}/index.d.ts +2 -1
  16. package/dist/index.js.map +1 -0
  17. package/dist/lib/cli-option-groups.js.map +1 -0
  18. package/dist/lib/composable-builder.d.ts +24 -0
  19. package/dist/lib/composable-builder.js +17 -0
  20. package/dist/lib/composable-builder.js.map +1 -0
  21. package/dist/lib/configuration-providers.js.map +1 -0
  22. package/{src → dist}/lib/documentation.d.ts +3 -3
  23. package/dist/lib/documentation.js.map +1 -0
  24. package/dist/lib/format-help.js.map +1 -0
  25. package/{src → dist}/lib/interactive-shell.d.ts +1 -1
  26. package/{src → dist}/lib/interactive-shell.js +6 -3
  27. package/dist/lib/interactive-shell.js.map +1 -0
  28. package/{src → dist}/lib/internal-cli.d.ts +38 -21
  29. package/{src → dist}/lib/internal-cli.js +48 -3
  30. package/dist/lib/internal-cli.js.map +1 -0
  31. package/dist/lib/public-api.d.ts +332 -0
  32. package/dist/lib/public-api.js.map +1 -0
  33. package/dist/lib/test-harness.js.map +1 -0
  34. package/dist/lib/utils.js.map +1 -0
  35. package/dist/middleware/zod.d.ts +4 -0
  36. package/dist/middleware/zod.js +18 -0
  37. package/dist/middleware/zod.js.map +1 -0
  38. package/dist/middleware.d.ts +1 -0
  39. package/dist/middleware.js +5 -0
  40. package/dist/middleware.js.map +1 -0
  41. package/package.json +29 -10
  42. package/project.json +7 -0
  43. package/src/bin/cli.ts +17 -0
  44. package/src/bin/commands/generate-documentation.ts +403 -0
  45. package/src/bin/commands/init.ts +320 -0
  46. package/src/bin/utils/fs.ts +11 -0
  47. package/src/index.ts +12 -0
  48. package/src/lib/cli-option-groups.ts +69 -0
  49. package/src/lib/composable-builder.ts +57 -0
  50. package/src/lib/configuration-providers.ts +36 -0
  51. package/src/lib/documentation.spec.ts +156 -0
  52. package/src/lib/documentation.ts +107 -0
  53. package/src/lib/format-help.ts +149 -0
  54. package/src/lib/interactive-shell.ts +115 -0
  55. package/src/lib/internal-cli.spec.ts +345 -0
  56. package/src/lib/internal-cli.ts +689 -0
  57. package/src/lib/public-api.ts +943 -0
  58. package/src/lib/test-harness.spec.ts +29 -0
  59. package/src/lib/test-harness.ts +69 -0
  60. package/src/lib/utils.spec.ts +25 -0
  61. package/src/lib/utils.ts +144 -0
  62. package/src/middleware/zod.ts +21 -0
  63. package/src/middleware.ts +1 -0
  64. package/tsconfig.json +23 -0
  65. package/tsconfig.lib.json +20 -0
  66. package/tsconfig.lib.json.tsbuildinfo +1 -0
  67. package/tsconfig.spec.json +26 -0
  68. package/vitest.config.mts +18 -0
  69. package/bin/cli.d.ts +0 -6
  70. package/bin/cli.js.map +0 -1
  71. package/bin/commands/generate-documentation.d.ts +0 -14
  72. package/bin/commands/generate-documentation.js.map +0 -1
  73. package/bin/commands/init.js.map +0 -1
  74. package/bin/utils/fs.js.map +0 -1
  75. package/src/index.js.map +0 -1
  76. package/src/lib/cli-option-groups.js.map +0 -1
  77. package/src/lib/composable-builder.d.ts +0 -3
  78. package/src/lib/composable-builder.js +0 -7
  79. package/src/lib/composable-builder.js.map +0 -1
  80. package/src/lib/configuration-providers.js.map +0 -1
  81. package/src/lib/documentation.js.map +0 -1
  82. package/src/lib/format-help.js.map +0 -1
  83. package/src/lib/interactive-shell.js.map +0 -1
  84. package/src/lib/internal-cli.js.map +0 -1
  85. package/src/lib/public-api.d.ts +0 -215
  86. package/src/lib/public-api.js.map +0 -1
  87. package/src/lib/test-harness.js.map +0 -1
  88. package/src/lib/utils.js.map +0 -1
  89. /package/{bin → dist/bin}/utils/fs.d.ts +0 -0
  90. /package/{bin → dist/bin}/utils/fs.js +0 -0
  91. /package/{src → dist}/index.js +0 -0
  92. /package/{src → dist}/lib/cli-option-groups.d.ts +0 -0
  93. /package/{src → dist}/lib/cli-option-groups.js +0 -0
  94. /package/{src → dist}/lib/configuration-providers.d.ts +0 -0
  95. /package/{src → dist}/lib/configuration-providers.js +0 -0
  96. /package/{src → dist}/lib/documentation.js +0 -0
  97. /package/{src → dist}/lib/format-help.d.ts +0 -0
  98. /package/{src → dist}/lib/format-help.js +0 -0
  99. /package/{src → dist}/lib/public-api.js +0 -0
  100. /package/{src → dist}/lib/test-harness.d.ts +0 -0
  101. /package/{src → dist}/lib/test-harness.js +0 -0
  102. /package/{src → dist}/lib/utils.d.ts +0 -0
  103. /package/{src → dist}/lib/utils.js +0 -0
@@ -0,0 +1,320 @@
1
+ import type { ParsedArgs } from '@cli-forge/parser';
2
+
3
+ import { execSync } from 'node:child_process';
4
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
5
+ import { dirname, join, relative } from 'node:path';
6
+
7
+ import cli, { CLI } from '../..';
8
+
9
+ import { ensureDirSync } from '../utils/fs';
10
+
11
+ const CLI_FORGE_PACKAGE_JSON = (() => {
12
+ let path = __dirname;
13
+ while (!existsSync(join(path, 'package.json'))) {
14
+ path = dirname(path);
15
+ }
16
+ return JSON.parse(readFileSync(join(path, 'package.json'), 'utf-8'));
17
+ })();
18
+
19
+ const CLI_FORGE_VERSION = CLI_FORGE_PACKAGE_JSON.version;
20
+
21
+ /**
22
+ * These are peer dependencies that **we** will call require/import on,
23
+ * but are not actually required at runtime. These are mostly optional,
24
+ * and used when running `cli-forge` commands rather than the user's CLI.
25
+ */
26
+ const DEV_PEER_DEPS = Object.entries(
27
+ CLI_FORGE_PACKAGE_JSON.peerDependencies
28
+ ).reduce((acc, [dep, version]) => {
29
+ // The dev prop doesn't actually do anything for npm/pnpm/yarn,
30
+ // but we are using it to mark when a peer dep is only used at dev time.
31
+ // In these cases, we can safely add them to the devDependencies of the
32
+ // generated CLI.
33
+ const meta =
34
+ CLI_FORGE_PACKAGE_JSON.peerDependenciesMeta[
35
+ dep as keyof typeof CLI_FORGE_PACKAGE_JSON.peerDependenciesMeta
36
+ ];
37
+ if (meta && 'dev' in meta && meta.dev) {
38
+ acc[dep] = version as string;
39
+ }
40
+ return acc;
41
+ }, {} as Record<string, string>);
42
+
43
+ export function withInitArgs<T extends ParsedArgs>(cmd: CLI<T>) {
44
+ return cmd
45
+ .positional('cliName', {
46
+ type: 'string',
47
+ description: 'Name of the CLI to generate.',
48
+ required: true,
49
+ })
50
+ .option('output', {
51
+ alias: ['o'],
52
+ type: 'string',
53
+ description: 'Where should the CLI be created?',
54
+ })
55
+ .option('format', {
56
+ type: 'string',
57
+ default: 'ts',
58
+ description: 'What format should the CLI be in?',
59
+ choices: ['js', 'ts'],
60
+ })
61
+ .option('initialVersion', {
62
+ type: 'string',
63
+ default: '0.0.1',
64
+ description:
65
+ 'Initial version used when creating the package.json for the new CLI.',
66
+ });
67
+ }
68
+
69
+ export const initCommand = cli('init', {
70
+ description: 'Generate a new CLI',
71
+ builder: (b) => withInitArgs(b),
72
+ handler: async (args) => {
73
+ args.output ??= join(process.cwd(), args.cliName);
74
+ ensureDirSync(args.output);
75
+ const packageJsonPath = join(args.output, 'package.json');
76
+ const cliPathWithoutExtension = join(args.output, 'bin', `${args.cliName}`);
77
+ const cliPath = [cliPathWithoutExtension, args.format].join('.');
78
+
79
+ let packageJsonContent: PackageJson = readJsonOr(packageJsonPath, {
80
+ name: args.cliName,
81
+ version: args.initialVersion,
82
+ });
83
+ packageJsonContent = mergePackageJsonContents(packageJsonContent, {
84
+ name: args.cliName,
85
+ version: args.initialVersion,
86
+ bin: {
87
+ [args.cliName]: relative(args.output, cliPathWithoutExtension),
88
+ },
89
+ dependencies: {
90
+ 'cli-forge': CLI_FORGE_VERSION,
91
+ },
92
+ });
93
+ if (args.format === 'ts') {
94
+ const latestTypescriptVersion = execSync('npm show typescript version')
95
+ .toString()
96
+ .trim();
97
+ const latestTsConfigNodeVersion = execSync(
98
+ 'npm show @tsconfig/node-lts version'
99
+ )
100
+ .toString()
101
+ .trim();
102
+ packageJsonContent = mergePackageJsonContents(packageJsonContent, {
103
+ scripts: {
104
+ build: 'tsx scripts/build.ts',
105
+ },
106
+ devDependencies: Object.fromEntries(
107
+ Object.entries({
108
+ typescript: latestTypescriptVersion,
109
+ '@tsconfig/node-lts': latestTsConfigNodeVersion,
110
+ ...DEV_PEER_DEPS,
111
+ }).sort(([a], [b]) => a.localeCompare(b))
112
+ ),
113
+ });
114
+ ensureDirSync(join(args.output, 'scripts'));
115
+ writeFileSync(
116
+ join(args.output, 'scripts/build.ts'),
117
+ `import { execSync } from 'node:child_process';
118
+ import { cpSync } from 'node:fs';
119
+
120
+ execSync('tsc --build tsconfig.json', { stdio: 'inherit' });
121
+ cpSync('package.json', 'dist/package.json');
122
+ `
123
+ );
124
+ writeFileSync(
125
+ join(args.output, 'tsconfig.json'),
126
+ JSON.stringify(
127
+ {
128
+ extends: '@tsconfig/node-lts',
129
+ compilerOptions: {
130
+ rootDir: '.',
131
+ outDir: 'dist',
132
+ strict: true,
133
+ },
134
+ include: ['src/**/*.ts', 'bin/**/*.ts'],
135
+ exclude: ['**/*.{spec,test}.ts'],
136
+ },
137
+ null,
138
+ 2
139
+ )
140
+ );
141
+ }
142
+ writeFileSync(
143
+ packageJsonPath,
144
+ JSON.stringify(
145
+ orderKeysInJson(packageJsonContent, [
146
+ 'name',
147
+ 'version',
148
+ 'scripts',
149
+ 'bin',
150
+ 'dependencies',
151
+ 'devDependencies',
152
+ ]),
153
+ null,
154
+ 2
155
+ )
156
+ );
157
+ ensureDirSync(dirname(cliPath));
158
+ writeFileSync(
159
+ cliPath,
160
+ args.format === 'ts'
161
+ ? TS_CLI_CONTENTS(args.cliName)
162
+ : JS_CLI_CONTENTS(args.cliName)
163
+ );
164
+ writeFileSync(
165
+ join(args.output, 'README.md'),
166
+ README_CONTENTS(args.cliName, args.format)
167
+ );
168
+ const installCommand = existsSync(join(args.output, 'yarn.lock'))
169
+ ? 'yarn'
170
+ : existsSync(join(args.output, 'pnpm-lock.yaml'))
171
+ ? 'pnpm'
172
+ : existsSync(join(args.output, 'bun.lockb'))
173
+ ? 'bun'
174
+ : 'npm';
175
+
176
+ execSync(`${installCommand} install`, {
177
+ cwd: args.output,
178
+ });
179
+ },
180
+ });
181
+
182
+ const COMMON_CONTENTS = (name: string) => `const myCLI = cli('${name}')
183
+ .command('hello', {
184
+ builder: (args) => args.positional('name', {type: 'string'}),
185
+ handler: (args) => {
186
+ console.log('hello', args.name);
187
+ }
188
+ })`;
189
+
190
+ const README_CONTENTS = (name: string, format: 'js' | 'ts') => `# ${name}
191
+
192
+ ${
193
+ format === 'ts' ? 'TypeScript' : 'JavaScript'
194
+ } CLI generated by [cli-forge](https://craigory.dev/cli-forge)
195
+
196
+ ## Usage
197
+
198
+ // Fill this in with usage instructions
199
+
200
+ ## Development
201
+
202
+ ${format === 'ts' ? 'To build the CLI, run `npm run build`' : ''}
203
+
204
+ To run the CLI, use the following command:
205
+
206
+ \`\`\`shell
207
+ ${
208
+ format === 'ts' ? 'npm run build && node ./dist/bin' : 'node ./bin'
209
+ }/${name} hello world
210
+ \`\`\`
211
+
212
+ ${
213
+ format === 'ts'
214
+ ? `> Hint: you can also use \`npx tsx ./bin/${name} hello world\` to run the CLI without building it first`
215
+ : ''
216
+ }
217
+ `;
218
+
219
+ const JS_CLI_CONTENTS = (name: string) => `const { cli } = require('cli-forge');
220
+
221
+ ${COMMON_CONTENTS(name)}
222
+
223
+ module.exports = myCLI;
224
+
225
+ if (require.main === module) {
226
+ myCLI.forge();
227
+ }
228
+ `;
229
+
230
+ const TS_CLI_CONTENTS = (name: string) => `import { cli } from 'cli-forge';
231
+
232
+ ${COMMON_CONTENTS(name)}
233
+
234
+ export default myCLI;
235
+
236
+ if (require.main === module) {
237
+ myCLI.forge();
238
+ }
239
+ `;
240
+
241
+ function readJsonOr<T>(filePath: string, alt: T): T {
242
+ try {
243
+ const contents = readFileSync(filePath, 'utf-8');
244
+ return JSON.parse(contents);
245
+ } catch {
246
+ return alt;
247
+ }
248
+ }
249
+
250
+ type PackageJson = {
251
+ name: string;
252
+ version?: string;
253
+ bin?: {
254
+ [cmd: string]: string;
255
+ };
256
+ scripts?: Record<string, string>;
257
+ dependencies?: Record<string, string>;
258
+ devDependencies?: Record<string, string>;
259
+ };
260
+
261
+ function mergePackageJsonContents(
262
+ original: PackageJson,
263
+ updates: Partial<PackageJson>,
264
+ overwriteExistingValues = false
265
+ ): PackageJson {
266
+ const first = overwriteExistingValues ? original : updates;
267
+ const second = overwriteExistingValues ? updates : original;
268
+
269
+ const merged: PackageJson = {
270
+ name: original.name ?? updates.name,
271
+ ...first,
272
+ ...second,
273
+ };
274
+
275
+ if (first.bin && second.bin) {
276
+ merged.bin = {
277
+ ...first.bin,
278
+ ...second.bin,
279
+ };
280
+ }
281
+
282
+ if (first.dependencies && second.dependencies) {
283
+ merged.dependencies = {
284
+ ...first.dependencies,
285
+ ...second.dependencies,
286
+ };
287
+ }
288
+
289
+ if (first.devDependencies && second.devDependencies) {
290
+ merged.devDependencies = {
291
+ ...first.devDependencies,
292
+ ...second.devDependencies,
293
+ };
294
+ }
295
+
296
+ return merged;
297
+ }
298
+
299
+ function orderKeysInJson<T extends Record<string, unknown>>(
300
+ obj: T,
301
+ order: Array<keyof T & string>
302
+ ): T {
303
+ const values = new Map(Object.entries(obj));
304
+ const keys = new Set(Object.keys(obj));
305
+ const returnObj = {} as T;
306
+
307
+ for (const key of order) {
308
+ const value = values.get(key);
309
+ if (value !== undefined) {
310
+ returnObj[key] = value as T[typeof key];
311
+ keys.delete(key);
312
+ }
313
+ }
314
+
315
+ for (const key of keys) {
316
+ (returnObj as any)[key] = values.get(key) as T[typeof key];
317
+ }
318
+
319
+ return returnObj;
320
+ }
@@ -0,0 +1,11 @@
1
+ import { existsSync, mkdirSync } from 'node:fs';
2
+
3
+ export function ensureDirSync(dir: string) {
4
+ try {
5
+ mkdirSync(dir, { recursive: true });
6
+ } catch (e) {
7
+ if (!existsSync(dir)) {
8
+ throw new Error(`Could not create directory: ${dir}`, { cause: e });
9
+ }
10
+ }
11
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export { TestHarness } from './lib/test-harness';
2
+ export * from './lib/public-api';
3
+ export { default } from './lib/public-api';
4
+ export { chain } from '@cli-forge/parser';
5
+ export { makeComposableBuilder } from './lib/composable-builder';
6
+ export type {
7
+ ComposableBuilder,
8
+ ExtractArgs,
9
+ ExtractChildren,
10
+ } from './lib/composable-builder';
11
+ export type { ArgumentsOf } from './lib/utils';
12
+ export { ConfigurationProviders } from './lib/configuration-providers';
@@ -0,0 +1,69 @@
1
+ import { InternalOptionConfig } from '@cli-forge/parser';
2
+ import { InternalCLI } from './internal-cli';
3
+
4
+ export function readOptionGroupsForCLI(parentCLI: InternalCLI<any>) {
5
+ function registerGroupsFromCLI(cli: InternalCLI) {
6
+ for (const { label, keys, sortOrder } of cli.registeredOptionGroups) {
7
+ groups[label] ??= {
8
+ keys: new Set(),
9
+ sortOrder: Number.MAX_SAFE_INTEGER,
10
+ };
11
+ if (sortOrder) {
12
+ groups[label].sortOrder = sortOrder;
13
+ }
14
+ for (const key of keys) {
15
+ groups[label].keys.add(key);
16
+ }
17
+ }
18
+ }
19
+
20
+ const groups: Record<string, { keys: Set<string>; sortOrder: number }> = {};
21
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
22
+ let command: InternalCLI<any> = parentCLI;
23
+ registerGroupsFromCLI(command);
24
+ for (const subcommand of parentCLI.commandChain) {
25
+ command = command?.registeredCommands[subcommand];
26
+ registerGroupsFromCLI(command);
27
+ }
28
+ const parserOptions: Record<string, InternalOptionConfig> =
29
+ parentCLI.parser.configuredOptions;
30
+
31
+ for (const key in parserOptions) {
32
+ const option = parserOptions[key];
33
+ if (option.group) {
34
+ groups[option.group] ??= {
35
+ keys: new Set(),
36
+ sortOrder: Number.MAX_SAFE_INTEGER,
37
+ };
38
+ groups[option.group].keys.add(key);
39
+ }
40
+ }
41
+
42
+ const groupedOptions: Array<{
43
+ label: string;
44
+ sortOrder: number;
45
+ keys: Array<InternalOptionConfig>;
46
+ }> = [];
47
+
48
+ for (const label in groups) {
49
+ const entry = {
50
+ sortOrder: groups[label].sortOrder,
51
+ keys: [] as InternalOptionConfig[],
52
+ label,
53
+ };
54
+ for (const key of groups[label].keys) {
55
+ const option = parserOptions[key];
56
+ entry.keys.push(option);
57
+ delete parserOptions[key];
58
+ }
59
+ groupedOptions.push(entry);
60
+ }
61
+ groupedOptions.sort((a, b) => {
62
+ if (a.sortOrder === b.sortOrder) {
63
+ return a.label.localeCompare(b.label);
64
+ } else {
65
+ return a.sortOrder - b.sortOrder;
66
+ }
67
+ });
68
+ return groupedOptions;
69
+ }
@@ -0,0 +1,57 @@
1
+ import { ParsedArgs } from '@cli-forge/parser';
2
+ import { CLI } from './public-api';
3
+
4
+ /**
5
+ * Extracts the TChildren type parameter from a CLI type.
6
+ */
7
+ export type ExtractChildren<T> = T extends CLI<any, any, infer C, any>
8
+ ? C
9
+ : never;
10
+
11
+ /**
12
+ * Extracts the TArgs type parameter from a CLI type.
13
+ */
14
+ export type ExtractArgs<T> = T extends CLI<infer A, any, any, any> ? A : never;
15
+
16
+ /**
17
+ * Type for a composable builder function that transforms a CLI.
18
+ * Used with `chain` to compose multiple builders.
19
+ */
20
+ export type ComposableBuilder<
21
+ TArgs2 extends ParsedArgs,
22
+ // eslint-disable-next-line @typescript-eslint/ban-types
23
+ TAddedChildren = {}
24
+ > = <TInit extends ParsedArgs, THandlerReturn, TChildren, TParent>(
25
+ init: CLI<TInit, THandlerReturn, TChildren, TParent>
26
+ ) => CLI<TInit & TArgs2, THandlerReturn, TChildren & TAddedChildren, TParent>;
27
+
28
+ /**
29
+ * Creates a composable builder function that can be used with `chain`.
30
+ * Can be used to add options, commands, or any other CLI modifications.
31
+ * Children added by the builder function are properly tracked in the type.
32
+ *
33
+ * @typeParam TArgs2 - The args type after the builder runs
34
+ * @typeParam TChildren2 - The children type added by the builder
35
+ */
36
+ export function makeComposableBuilder<
37
+ TArgs2 extends ParsedArgs,
38
+ // eslint-disable-next-line @typescript-eslint/ban-types
39
+ TChildren2 = {}
40
+ >(
41
+ fn: (
42
+ // eslint-disable-next-line @typescript-eslint/ban-types
43
+ init: CLI<ParsedArgs, any, {}, any>
44
+ ) => CLI<TArgs2, any, TChildren2, any>
45
+ ) {
46
+ return <TInit extends ParsedArgs, THandlerReturn, TChildren, TParent>(
47
+ init: CLI<TInit, THandlerReturn, TChildren, TParent>
48
+ ) =>
49
+ // eslint-disable-next-line @typescript-eslint/ban-types
50
+ fn(init as unknown as CLI<ParsedArgs, any, {}, any>) as unknown as CLI<
51
+ TInit & TArgs2,
52
+ THandlerReturn,
53
+ TChildren & TChildren2,
54
+ TParent
55
+ >;
56
+ }
57
+
@@ -0,0 +1,36 @@
1
+ import { ConfigurationFiles } from '@cli-forge/parser';
2
+
3
+ /**
4
+ * A collection of built-in configuration provider factories. These should be invoked and passed to
5
+ * {@link CLI.config} to load configuration from various sources. For custom configuration providers, see
6
+ * https://craigory.dev/cli-forge/api/parser/namespaces/ConfigurationFiles/type-aliases/ConfigurationProvider
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { cli, ConfigurationProviders } from 'cli-forge';
11
+ *
12
+ * cli(...).config(ConfigurationProviders.PackageJson('myConfig'));
13
+ * ```
14
+ */
15
+ export const ConfigurationProviders = {
16
+ /**
17
+ * Load configuration from a package.json file.
18
+ *
19
+ * @param key The key in the package.json file to load as configuration.
20
+ */
21
+ PackageJson<T>(key: string) {
22
+ return ConfigurationFiles.getPackageJsonConfigurationLoader<T>(key);
23
+ },
24
+
25
+ /**
26
+ * Load configuration from a JSON file.
27
+ *
28
+ * @param filename The filename of the JSON file to load.
29
+ * @param key The key in the JSON file to load as configuration. By default, the entire JSON object is loaded.
30
+ */
31
+ JsonFile<T>(filename: string, key?: string) {
32
+ return ConfigurationFiles.getJsonFileConfigLoader<T>(filename, (json) =>
33
+ key ? json[key] : json
34
+ );
35
+ },
36
+ };
@@ -0,0 +1,156 @@
1
+ import { InternalCLI } from './internal-cli';
2
+ import { generateDocumentation } from './documentation';
3
+ import cli from './public-api';
4
+
5
+ describe('generateDocumentation', () => {
6
+ it('should not document hidden commands and options', () => {
7
+ const docs = generateDocumentation(
8
+ cli('test')
9
+ .command('foo', {
10
+ hidden: true,
11
+ builder: (argv) => argv.option('bar', { type: 'string' }),
12
+ handler: () => {
13
+ // not executed.
14
+ },
15
+ })
16
+ .command('bar', {
17
+ builder: (argv) =>
18
+ argv
19
+ .option('a', { type: 'string' })
20
+ .option('b', { type: 'string', hidden: true }),
21
+ handler: () => {
22
+ // not executed.
23
+ },
24
+ }) as unknown as InternalCLI
25
+ );
26
+ expect(docs.subcommands.find((d) => d.name === 'foo')).toBeUndefined();
27
+
28
+ const barDocs = docs.subcommands.find((d) => d.name === 'bar');
29
+ expect(barDocs).toBeDefined();
30
+ expect(barDocs?.options?.['a']).toBeDefined();
31
+ expect(barDocs?.options?.['b']).toBeUndefined();
32
+ });
33
+
34
+ it('should not execute command handlers', () => {
35
+ let ran = false;
36
+ generateDocumentation(
37
+ cli('test').command('foo', {
38
+ builder: (argv) => argv.option('bar', { type: 'string' }),
39
+ handler: () => {
40
+ ran = true;
41
+ },
42
+ }) as unknown as InternalCLI
43
+ );
44
+ expect(ran).toBe(false);
45
+ });
46
+
47
+ it('should not contain sibling command arguments in current command documentation', () => {
48
+ const docs = generateDocumentation(
49
+ cli('test')
50
+ .command('foo', {
51
+ builder: (argv) => argv.option('bar', { type: 'string' }),
52
+ handler: () => {
53
+ // not executed.
54
+ },
55
+ })
56
+ .command('bar', {
57
+ builder: (argv) => argv.option('baz', { type: 'string' }),
58
+ handler: () => {
59
+ // not executed.
60
+ },
61
+ }) as unknown as InternalCLI
62
+ );
63
+ const fooDocs = docs.subcommands.find((d) => d.name === 'foo');
64
+ expect(fooDocs).toBeDefined();
65
+ // Baz is only registered on the 'bar' command.
66
+ expect(fooDocs?.options?.['baz']).toBeUndefined();
67
+ // Bar is only registered on the 'foo' command.
68
+ expect(fooDocs?.options?.['bar']).toBeDefined();
69
+
70
+ const barDocs = docs.subcommands.find((d) => d.name === 'bar');
71
+ expect(barDocs).toBeDefined();
72
+ // Bar is only registered on the 'foo' command.
73
+ expect(barDocs?.options?.['bar']).toBeUndefined();
74
+ // Baz is only registered on the 'bar' command.
75
+ expect(barDocs?.options?.['baz']).toBeDefined();
76
+ });
77
+
78
+ it('should group options by group', () => {
79
+ const docs = generateDocumentation(
80
+ cli('test')
81
+ .option('baz', { type: 'string' })
82
+ .option('bar', { type: 'string' })
83
+ .option('foo', {
84
+ type: 'string',
85
+ })
86
+ .group('Advanced', ['foo', 'bar']) as unknown as InternalCLI
87
+ );
88
+ const advanced = docs.groupedOptions.find((g) => g.label === 'Advanced');
89
+ expect(advanced).toBeDefined();
90
+ expect(advanced?.keys.map((k) => k.key)).toEqual(['foo', 'bar']);
91
+ });
92
+
93
+ // TODO: This test should not fail.
94
+ it.skip('should contain parent command arguments in subcommand documentation', () => {
95
+ const docs = generateDocumentation(
96
+ cli('test')
97
+ .option('baz', { type: 'string' })
98
+ .command('format', {
99
+ builder: (argv) =>
100
+ argv.option('bar', { type: 'string' }).command('check', {
101
+ builder: (argv) => argv.option('foo', { type: 'string' }),
102
+ handler: () => {
103
+ // not executed.
104
+ },
105
+ }),
106
+ handler: () => {
107
+ // not executed.
108
+ },
109
+ }) as unknown as InternalCLI
110
+ );
111
+ const formatDocs = docs.subcommands.find((d) => d.name === 'format');
112
+ expect(formatDocs).toBeDefined();
113
+ expect(formatDocs?.options?.['baz']).toBeDefined();
114
+ expect(formatDocs?.options?.['bar']).toBeDefined();
115
+ expect(formatDocs?.options?.['foo']).toBeUndefined();
116
+
117
+ const checkDocs = formatDocs?.subcommands.find((d) => d.name === 'check');
118
+ expect(checkDocs).toBeDefined();
119
+ expect(checkDocs?.options?.['baz']).toBeDefined();
120
+ expect(checkDocs?.options?.['bar']).toBeDefined();
121
+ expect(checkDocs?.options?.['foo']).toBeDefined();
122
+ });
123
+
124
+ it('should document epilogue, which inherits from parent command', () => {
125
+ const docs = generateDocumentation(
126
+ cli('test')
127
+ .command('foo', {
128
+ epilogue: 'foo epilogue',
129
+ builder: (argv) =>
130
+ argv.option('bar', { type: 'string' }).command('subcommand', {}),
131
+ handler: () => {
132
+ // not executed.
133
+ },
134
+ })
135
+ .command('bar', {
136
+ builder: (argv) => argv.option('baz', { type: 'string' }),
137
+ handler: () => {
138
+ // not executed.
139
+ },
140
+ }) as unknown as InternalCLI
141
+ );
142
+ const fooDocs = docs.subcommands.find((d) => d.name === 'foo');
143
+ expect(fooDocs).toBeDefined();
144
+ expect(fooDocs?.epilogue).toBe('foo epilogue');
145
+
146
+ const subcommandDocs = fooDocs?.subcommands.find(
147
+ (d) => d.name === 'subcommand'
148
+ );
149
+ expect(subcommandDocs).toBeDefined();
150
+ expect(subcommandDocs?.epilogue).toBe('foo epilogue');
151
+
152
+ const barDocs = docs.subcommands.find((d) => d.name === 'bar');
153
+ expect(barDocs).toBeDefined();
154
+ expect(barDocs?.epilogue).toBeUndefined();
155
+ });
156
+ });