Skip to content

Commit 2477bb3

Browse files
committed
chore: tests
1 parent 529e361 commit 2477bb3

File tree

25 files changed

+494
-340
lines changed

25 files changed

+494
-340
lines changed

features/test-implementations/0.world.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export interface TestWorld<Parameters = unknown[]> extends IWorld<Parameters> {
4949
*/
5050
export const ProjectRoot = new URL('../../', import.meta.url);
5151

52-
export const DevRunFile = new URL('./bin/dev.js', ProjectRoot);
52+
export const DevRunFile = new URL('./src/entrypoints/apify.ts', ProjectRoot);
5353

5454
export const TestTmpRoot = new URL('./test/tmp/', ProjectRoot);
5555

src/entrypoints/_shared.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,9 @@ yargonaut //
1313

1414
export const cli = yargs()
1515
.scriptName('apify')
16-
.version(version)
17-
// This needs to be manually handled, as setting it here will override any `-v` flags used in commands (e.g: `apify runs info -v`)
18-
// .alias('v', 'version')
19-
// TODO: we can override the help message by disabling the built in help flag, then implementing it on the commands
20-
// .help()
21-
// TODO: if we set `h` here, no commands can use `h` as a char for a flag, unless we manually handle help messages ourselves
22-
.alias('h', 'help')
23-
.wrap(Math.max(80, process.stdout.columns || 80))
16+
.version(false)
17+
.help(false)
18+
// .wrap(Math.max(80, process.stdout.columns || 80))
2419
.parserConfiguration({
2520
// Disables the automatic conversion of `--foo-bar` to `fooBar` (we handle it manually)
2621
'camel-case-expansion': false,
@@ -29,10 +24,6 @@ export const cli = yargs()
2924
// We parse numbers manually
3025
'parse-numbers': false,
3126
'parse-positional-numbers': false,
32-
})
33-
.strictCommands()
34-
.updateStrings({
35-
'Positionals:': 'Arguments:',
3627
});
3728

3829
// @ts-expect-error @types/yargs is outdated -.-

src/entrypoints/apify.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ for (const CommandClass of apifyCommands) {
3939
const parsed = await cli.parse(process.argv.slice(2));
4040

4141
if (parsed._.length === 0) {
42-
if (parsed.v === true) {
42+
if (parsed.v === true || parsed.version === true) {
4343
printCLIVersionAndExit();
4444
}
4545

src/lib/apify-oclif-help.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.

src/lib/command-framework/apify-command.ts

Lines changed: 158 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,50 @@ type InferArgTypeFromArg<Builder extends TaggedArgBuilder<ArgTag, unknown>> = Bu
2525
: unknown;
2626

2727
type If<T, Y, N> = T extends true ? Y : N;
28-
29-
type InferFlagTypeFromFlag<Builder extends TaggedFlagBuilder<FlagTag, unknown, unknown>> =
30-
// Handle special case where there can be no choices
31-
Builder extends TaggedFlagBuilder<infer ReturnedType, never, infer Required>
32-
? If<Required, FlagTagToTSType[ReturnedType], FlagTagToTSType[ReturnedType] | undefined>
33-
: // Might have choices, in which case we branch based on that
34-
Builder extends TaggedFlagBuilder<infer ReturnedType, infer ChoiceType, infer Required>
35-
? // If choices is a valid array
36-
ChoiceType extends unknown[] | readonly unknown[]
37-
? // Then assert on required status
28+
type IfNotUnknown<T, Y, N> = T extends unknown ? Y : N;
29+
30+
type InferFlagTypeFromFlag<
31+
Builder extends TaggedFlagBuilder<FlagTag, unknown, unknown, unknown>,
32+
OptionalIfHasDefault = false,
33+
> = Builder extends TaggedFlagBuilder<infer ReturnedType, never, infer Required, infer HasDefault> // Handle special case where there can be no choices
34+
? If<
35+
// If we want to mark flags as optional if they have a default
36+
OptionalIfHasDefault,
37+
// If the flag actually has a default value, assert on that
38+
IfNotUnknown<
39+
HasDefault,
40+
FlagTagToTSType[ReturnedType] | undefined,
41+
// Otherwise fall back to required status
42+
If<Required, FlagTagToTSType[ReturnedType], FlagTagToTSType[ReturnedType] | undefined>
43+
>,
44+
// fallback to required status
45+
If<Required, FlagTagToTSType[ReturnedType], FlagTagToTSType[ReturnedType] | undefined>
46+
>
47+
: // Might have choices, in which case we branch based on that
48+
Builder extends TaggedFlagBuilder<infer ReturnedType, infer ChoiceType, infer Required, infer HasDefault>
49+
? // If choices is a valid array
50+
ChoiceType extends unknown[] | readonly unknown[]
51+
? // If we want optional flags to stay as optional
52+
If<
53+
OptionalIfHasDefault,
54+
ChoiceType[number] | undefined,
55+
// fallback to required status
3856
If<Required, ChoiceType[number], ChoiceType[number] | undefined>
39-
: If<Required, FlagTagToTSType[ReturnedType], FlagTagToTSType[ReturnedType] | undefined>
40-
: unknown;
57+
>
58+
: If<
59+
// If we want to mark flags as optional if they have a default
60+
OptionalIfHasDefault,
61+
// If the flag actually has a default value, assert on that
62+
IfNotUnknown<
63+
HasDefault,
64+
FlagTagToTSType[ReturnedType] | undefined,
65+
// Fallback to required status
66+
If<Required, FlagTagToTSType[ReturnedType], FlagTagToTSType[ReturnedType] | undefined>
67+
>,
68+
// fallback to required status
69+
If<Required, FlagTagToTSType[ReturnedType], FlagTagToTSType[ReturnedType] | undefined>
70+
>
71+
: unknown;
4172

4273
// Adapted from https://gist.github.com/kuroski/9a7ae8e5e5c9e22985364d1ddbf3389d to support kebab-case and "string a"
4374
type CamelCase<S extends string> = S extends
@@ -51,16 +82,23 @@ type _InferArgsFromCommand<O extends Record<string, TaggedArgBuilder<ArgTag, unk
5182
[K in keyof O as CamelCase<string & K>]: InferArgTypeFromArg<O[K]>;
5283
};
5384

54-
type _InferFlagsFromCommand<O extends Record<string, TaggedFlagBuilder<FlagTag, unknown, unknown>>> = {
55-
[K in keyof O as CamelCase<string & K>]: InferFlagTypeFromFlag<O[K]>;
85+
type _InferFlagsFromCommand<
86+
O extends Record<string, TaggedFlagBuilder<FlagTag, unknown, unknown, unknown>>,
87+
OptionalIfHasDefault = false,
88+
> = {
89+
[K in keyof O as CamelCase<string & K>]: InferFlagTypeFromFlag<O[K], OptionalIfHasDefault>;
5690
};
5791

5892
type InferArgsFromCommand<O extends Record<string, TaggedArgBuilder<ArgTag, unknown>> | undefined> = O extends undefined
5993
? Record<string, unknown>
6094
: _InferArgsFromCommand<Exclude<O, undefined>>;
6195

62-
type InferFlagsFromCommand<O extends Record<string, TaggedFlagBuilder<FlagTag, unknown, unknown>> | undefined> =
63-
(O extends undefined ? Record<string, unknown> : _InferFlagsFromCommand<Exclude<O, undefined>>) & { json: boolean };
96+
type InferFlagsFromCommand<
97+
O extends Record<string, TaggedFlagBuilder<FlagTag, unknown, unknown, unknown>> | undefined,
98+
OptionalIfHasDefault = false,
99+
> = (O extends undefined
100+
? Record<string, unknown>
101+
: _InferFlagsFromCommand<Exclude<O, undefined>, OptionalIfHasDefault>) & { json: boolean };
64102

65103
function camelCaseString(str: string): string {
66104
return str.replace(/[-_\s](.)/g, (_, group1) => group1.toUpperCase());
@@ -74,16 +112,12 @@ function camelCaseToKebabCase(str: string): string {
74112
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
75113
}
76114

77-
function kebabCaseToSnakeCase(str: string): string {
78-
return str.replaceAll('-', '_').toLowerCase();
79-
}
80-
81115
export abstract class ApifyCommand<T extends typeof BuiltApifyCommand = typeof BuiltApifyCommand> {
82116
static args?: Record<string, TaggedArgBuilder<ArgTag, unknown>> & {
83117
json?: 'Do not use json as the key of an argument, as it will prevent the --json flag from working';
84118
};
85119

86-
static flags?: Record<string, TaggedFlagBuilder<FlagTag, unknown, unknown>> & {
120+
static flags?: Record<string, TaggedFlagBuilder<FlagTag, unknown, unknown, unknown>> & {
87121
json?: 'Do not use json as the key of a flag, override the enableJsonFlag static property instead';
88122
};
89123

@@ -101,7 +135,7 @@ export abstract class ApifyCommand<T extends typeof BuiltApifyCommand = typeof B
101135

102136
static hiddenAliases?: string[];
103137

104-
protected telemetryData!: Record<string, unknown>;
138+
protected telemetryData: Record<string, unknown> = {};
105139

106140
protected flags!: InferFlagsFromCommand<T['flags']>;
107141
protected args!: InferArgsFromCommand<T['args']>;
@@ -116,6 +150,10 @@ export abstract class ApifyCommand<T extends typeof BuiltApifyCommand = typeof B
116150
return amount === 1 ? singular : plural;
117151
}
118152

153+
protected printHelp() {
154+
console.log(`TODO!!! Command ${this.ctor.name} wants to print help manually`);
155+
}
156+
119157
private async _run(rawArgs: ArgumentsCamelCase) {
120158
// Cheating a bit here with the types, but its fine
121159
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- makes parsing easier
@@ -193,11 +231,15 @@ export abstract class ApifyCommand<T extends typeof BuiltApifyCommand = typeof B
193231
}
194232
}
195233

234+
// TODO: remove me
235+
// console.log({ rawInput: rawArgs, args: this.args, flags: this.flags });
236+
196237
try {
197238
await this.run();
198-
} catch (err) {
239+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
240+
} catch (err: any) {
199241
// TODO: handle errors gracefully with a logger
200-
console.error(err);
242+
console.error(err.message);
201243
}
202244
}
203245

@@ -256,10 +298,8 @@ export abstract class ApifyCommand<T extends typeof BuiltApifyCommand = typeof B
256298
}
257299

258300
const flagKey = kebabCaseString(camelCaseToKebabCase(key)).toLowerCase();
259-
const camelCaseKey = camelCaseString(key);
260-
const snakeCaseKey = kebabCaseToSnakeCase(flagKey);
261301

262-
finalYargs = internalBuilderData.builder(finalYargs, flagKey, [camelCaseKey, snakeCaseKey]);
302+
finalYargs = internalBuilderData.builder(finalYargs, flagKey);
263303
}
264304
}
265305

@@ -315,6 +355,96 @@ export abstract class ApifyCommand<T extends typeof BuiltApifyCommand = typeof B
315355
}
316356
}
317357

358+
// Utility type to extract only the keys that are optional
359+
type ExtractOptionalFlagKeys<Cmd extends typeof BuiltApifyCommand> = {
360+
[K in keyof InferFlagsFromCommand<Cmd['flags'], true>]: [undefined] extends [
361+
InferFlagsFromCommand<Cmd['flags'], true>[K],
362+
]
363+
? K
364+
: never;
365+
}[keyof InferFlagsFromCommand<Cmd['flags'], true>];
366+
367+
type ExtractOptionalArgKeys<Cmd extends typeof BuiltApifyCommand> = {
368+
[K in keyof InferArgsFromCommand<Cmd['args']>]: [undefined] extends [InferArgsFromCommand<Cmd['args']>[K]]
369+
? K
370+
: never;
371+
}[keyof InferArgsFromCommand<Cmd['args']>];
372+
373+
// Messy type...
374+
type StaticArgsFlagsInput<Cmd extends typeof BuiltApifyCommand> = Omit<
375+
{
376+
// This ensures we only get the required args
377+
[K in Exclude<
378+
keyof InferArgsFromCommand<Cmd['args']>,
379+
ExtractOptionalArgKeys<Cmd>
380+
> as `args_${string & K}`]: InferArgsFromCommand<Cmd['args']>[K];
381+
},
382+
// Omit args_json as it is used only to throw an error if the user provides it
383+
'args_json'
384+
> &
385+
Omit<
386+
{
387+
// Fill in the rest of the args, this will not override what the code above does
388+
[K in keyof InferArgsFromCommand<Cmd['args']> as `args_${string & K}`]?: InferArgsFromCommand<
389+
Cmd['args']
390+
>[K];
391+
},
392+
'args_json'
393+
> &
394+
Omit<
395+
{
396+
// This ensures we only ever get the required flags into this object, as `key?: type` and `key: type | undefined` are not the same (one is optionally present, the other is mandatory)
397+
[K in Exclude<
398+
keyof InferFlagsFromCommand<Cmd['flags'], true>,
399+
ExtractOptionalFlagKeys<Cmd>
400+
> as `flags_${string & K}`]: InferFlagsFromCommand<Cmd['flags'], true>[K];
401+
},
402+
// Omit flags_json as it is used only to throw an error if the user provides it
403+
'flags_json'
404+
> &
405+
Omit<
406+
{
407+
// Fill in the rest of the flags, this will not override what the code above does
408+
[K in keyof InferFlagsFromCommand<Cmd['flags'], true> as `flags_${string & K}`]?: InferFlagsFromCommand<
409+
Cmd['flags'],
410+
true
411+
>[K];
412+
},
413+
// Omit flags_json as it is used only to throw an error if the user provides it
414+
'flags_json'
415+
> & {
416+
// Define it at the end exactly like it is
417+
flags_json?: boolean;
418+
};
419+
420+
export async function runCommand<Cmd extends typeof BuiltApifyCommand>(
421+
command: Cmd,
422+
argsFlags: StaticArgsFlagsInput<Cmd>,
423+
) {
424+
// This is very much yolo'd in, but its purpose is for testing only
425+
const rawObject: ArgumentsCamelCase = {
426+
_: [],
427+
$0: 'apify',
428+
};
429+
430+
for (const [key, value] of Object.entries(argsFlags)) {
431+
const [type, rawKey] = key.split('_');
432+
433+
if (type === 'args') {
434+
rawObject[rawKey] = value;
435+
} else {
436+
const yargsFlagName = kebabCaseString(camelCaseToKebabCase(rawKey)).toLowerCase();
437+
438+
rawObject[yargsFlagName] = value;
439+
}
440+
}
441+
442+
const instance = new (command as typeof BuiltApifyCommand)();
443+
444+
// eslint-disable-next-line dot-notation
445+
await instance['_run'](rawObject);
446+
}
447+
318448
export declare class BuiltApifyCommand extends ApifyCommand {
319-
override run(): void;
449+
override run(): Awaitable<void>;
320450
}

0 commit comments

Comments
 (0)