Skip to content

Commit f8efc64

Browse files
author
unknown
committed
refactor(codegen): move CLI utils to template-copy pattern
Move CLI utils code from a string template in utils-generator.ts to a real TypeScript file at templates/cli-utils.ts, following the same pattern as ORM templates (orm-client.ts, query-builder.ts, etc.). Benefits: - Type-checked at build time (was just a string before) - IDE support (syntax highlighting, autocomplete, refactoring) - Testable directly as a real .ts module - Easier to maintain (no escaping backticks, no string concatenation) - Consistent with existing template-copy pattern in the codebase
1 parent 91d58ad commit f8efc64

2 files changed

Lines changed: 184 additions & 125 deletions

File tree

Lines changed: 40 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,61 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
14
import { getGeneratedFileHeader } from '../utils';
25
import type { GeneratedFile } from './executor-generator';
36

47
/**
5-
* Generate a utils.ts file with runtime helpers for CLI commands.
6-
* Includes type coercion (string CLI args -> proper GraphQL types),
7-
* field filtering (strip extra minimist fields like _ and tty),
8-
* and mutation input parsing.
9-
*/
10-
export function generateUtilsFile(): GeneratedFile {
11-
const header = getGeneratedFileHeader(
12-
'CLI utility functions for type coercion and input handling',
13-
);
14-
15-
const code = `
16-
export type FieldType = 'string' | 'boolean' | 'int' | 'float' | 'json' | 'uuid' | 'enum';
17-
18-
export interface FieldSchema {
19-
[fieldName: string]: FieldType;
20-
}
21-
22-
/**
23-
* Coerce CLI string arguments to their proper GraphQL types based on a field schema.
24-
* CLI args always arrive as strings from minimist, but GraphQL expects
25-
* Boolean, Int, Float, JSON, etc.
8+
* Find the cli-utils template file path.
9+
* Templates are at ../templates/ relative to this file in both src/ and dist/.
2610
*/
27-
export function coerceAnswers(
28-
answers: Record<string, unknown>,
29-
schema: FieldSchema,
30-
): Record<string, unknown> {
31-
const result: Record<string, unknown> = { ...answers };
32-
33-
for (const [key, value] of Object.entries(result)) {
34-
const fieldType = schema[key];
35-
if (!fieldType || value === undefined || value === null) continue;
11+
function findTemplateFile(templateName: string): string {
12+
const templatePath = path.join(__dirname, '../templates', templateName);
3613

37-
const strValue = String(value);
38-
39-
// Empty strings become undefined for non-string types
40-
if (strValue === '' && fieldType !== 'string') {
41-
result[key] = undefined;
42-
continue;
43-
}
44-
45-
switch (fieldType) {
46-
case 'boolean':
47-
if (typeof value === 'boolean') break;
48-
result[key] = strValue === 'true' || strValue === '1' || strValue === 'yes';
49-
break;
50-
case 'int':
51-
if (typeof value === 'number') break;
52-
{
53-
const parsed = parseInt(strValue, 10);
54-
result[key] = isNaN(parsed) ? undefined : parsed;
55-
}
56-
break;
57-
case 'float':
58-
if (typeof value === 'number') break;
59-
{
60-
const parsed = parseFloat(strValue);
61-
result[key] = isNaN(parsed) ? undefined : parsed;
62-
}
63-
break;
64-
case 'json':
65-
if (typeof value === 'object') break;
66-
if (strValue === '') {
67-
result[key] = undefined;
68-
} else {
69-
try {
70-
result[key] = JSON.parse(strValue);
71-
} catch {
72-
result[key] = undefined;
73-
}
74-
}
75-
break;
76-
case 'uuid':
77-
// Empty UUIDs become undefined
78-
if (strValue === '') {
79-
result[key] = undefined;
80-
}
81-
break;
82-
case 'enum':
83-
// Enums stay as strings but empty ones become undefined
84-
if (strValue === '') {
85-
result[key] = undefined;
86-
}
87-
break;
88-
default:
89-
// String type: empty strings also become undefined to avoid
90-
// sending empty strings for optional fields
91-
if (strValue === '') {
92-
result[key] = undefined;
93-
}
94-
break;
95-
}
14+
if (fs.existsSync(templatePath)) {
15+
return templatePath;
9616
}
9717

98-
return result;
18+
throw new Error(
19+
`Could not find template file: ${templateName}. ` +
20+
`Searched in: ${templatePath}`,
21+
);
9922
}
10023

10124
/**
102-
* Strip undefined values and filter to only schema-defined keys.
103-
* This removes extra fields injected by minimist (like _, tty, etc.)
104-
* and any fields that were coerced to undefined.
25+
* Read a template file and replace the header with generated file header.
26+
* Follows the same pattern as ORM client-generator.ts readTemplateFile().
10527
*/
106-
export function stripUndefined(
107-
obj: Record<string, unknown>,
108-
schema?: FieldSchema,
109-
): Record<string, unknown> {
110-
const result: Record<string, unknown> = {};
111-
const allowedKeys = schema ? new Set(Object.keys(schema)) : null;
112-
113-
for (const [key, value] of Object.entries(obj)) {
114-
if (value === undefined) continue;
115-
if (allowedKeys && !allowedKeys.has(key)) continue;
116-
result[key] = value;
117-
}
28+
function readTemplateFile(templateName: string, description: string): string {
29+
const templatePath = findTemplateFile(templateName);
30+
let content = fs.readFileSync(templatePath, 'utf-8');
31+
32+
// Replace the source file header comment with the generated file header
33+
// Match the header pattern used in template files
34+
const headerPattern =
35+
/\/\*\*[\s\S]*?\* NOTE: This file is read at codegen time and written to output\.[\s\S]*?\*\/\n*/;
36+
37+
content = content.replace(
38+
headerPattern,
39+
getGeneratedFileHeader(description) + '\n',
40+
);
11841

119-
return result;
42+
return content;
12043
}
12144

12245
/**
123-
* Parse mutation input from CLI.
124-
* Custom mutation commands receive an \`input\` field as a JSON string
125-
* from the CLI prompt. This parses it into a proper object.
46+
* Generate a utils.ts file with runtime helpers for CLI commands.
47+
* Reads from the templates directory (cli-utils.ts) for proper type checking.
48+
*
49+
* Includes type coercion (string CLI args -> proper GraphQL types),
50+
* field filtering (strip extra minimist fields like _ and tty),
51+
* and mutation input parsing.
12652
*/
127-
export function parseMutationInput(
128-
answers: Record<string, unknown>,
129-
): Record<string, unknown> {
130-
if (typeof answers.input === 'string') {
131-
try {
132-
const parsed = JSON.parse(answers.input);
133-
return { ...answers, input: parsed };
134-
} catch {
135-
return answers;
136-
}
137-
}
138-
return answers;
139-
}
140-
`;
141-
53+
export function generateUtilsFile(): GeneratedFile {
14254
return {
14355
fileName: 'utils.ts',
144-
content: header + '\n' + code,
56+
content: readTemplateFile(
57+
'cli-utils.ts',
58+
'CLI utility functions for type coercion and input handling',
59+
),
14560
};
14661
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* CLI utility functions for type coercion and input handling
3+
*
4+
* This is the RUNTIME code that gets copied to generated output.
5+
* Provides helpers for CLI commands: type coercion (string CLI args -> proper
6+
* GraphQL types), field filtering (strip extra minimist fields), and
7+
* mutation input parsing.
8+
*
9+
* NOTE: This file is read at codegen time and written to output.
10+
* Any changes here will affect all generated CLI utils.
11+
*/
12+
13+
export type FieldType =
14+
| 'string'
15+
| 'boolean'
16+
| 'int'
17+
| 'float'
18+
| 'json'
19+
| 'uuid'
20+
| 'enum';
21+
22+
export interface FieldSchema {
23+
[fieldName: string]: FieldType;
24+
}
25+
26+
/**
27+
* Coerce CLI string arguments to their proper GraphQL types based on a field schema.
28+
* CLI args always arrive as strings from minimist, but GraphQL expects
29+
* Boolean, Int, Float, JSON, etc.
30+
*/
31+
export function coerceAnswers(
32+
answers: Record<string, unknown>,
33+
schema: FieldSchema,
34+
): Record<string, unknown> {
35+
const result: Record<string, unknown> = { ...answers };
36+
37+
for (const [key, value] of Object.entries(result)) {
38+
const fieldType = schema[key];
39+
if (!fieldType || value === undefined || value === null) continue;
40+
41+
const strValue = String(value);
42+
43+
// Empty strings become undefined for non-string types
44+
if (strValue === '' && fieldType !== 'string') {
45+
result[key] = undefined;
46+
continue;
47+
}
48+
49+
switch (fieldType) {
50+
case 'boolean':
51+
if (typeof value === 'boolean') break;
52+
result[key] =
53+
strValue === 'true' || strValue === '1' || strValue === 'yes';
54+
break;
55+
case 'int':
56+
if (typeof value === 'number') break;
57+
{
58+
const parsed = parseInt(strValue, 10);
59+
result[key] = isNaN(parsed) ? undefined : parsed;
60+
}
61+
break;
62+
case 'float':
63+
if (typeof value === 'number') break;
64+
{
65+
const parsed = parseFloat(strValue);
66+
result[key] = isNaN(parsed) ? undefined : parsed;
67+
}
68+
break;
69+
case 'json':
70+
if (typeof value === 'object') break;
71+
if (strValue === '') {
72+
result[key] = undefined;
73+
} else {
74+
try {
75+
result[key] = JSON.parse(strValue);
76+
} catch {
77+
result[key] = undefined;
78+
}
79+
}
80+
break;
81+
case 'uuid':
82+
// Empty UUIDs become undefined
83+
if (strValue === '') {
84+
result[key] = undefined;
85+
}
86+
break;
87+
case 'enum':
88+
// Enums stay as strings but empty ones become undefined
89+
if (strValue === '') {
90+
result[key] = undefined;
91+
}
92+
break;
93+
default:
94+
// String type: empty strings also become undefined to avoid
95+
// sending empty strings for optional fields
96+
if (strValue === '') {
97+
result[key] = undefined;
98+
}
99+
break;
100+
}
101+
}
102+
103+
return result;
104+
}
105+
106+
/**
107+
* Strip undefined values and filter to only schema-defined keys.
108+
* This removes extra fields injected by minimist (like _, tty, etc.)
109+
* and any fields that were coerced to undefined.
110+
*/
111+
export function stripUndefined(
112+
obj: Record<string, unknown>,
113+
schema?: FieldSchema,
114+
): Record<string, unknown> {
115+
const result: Record<string, unknown> = {};
116+
const allowedKeys = schema ? new Set(Object.keys(schema)) : null;
117+
118+
for (const [key, value] of Object.entries(obj)) {
119+
if (value === undefined) continue;
120+
if (allowedKeys && !allowedKeys.has(key)) continue;
121+
result[key] = value;
122+
}
123+
124+
return result;
125+
}
126+
127+
/**
128+
* Parse mutation input from CLI.
129+
* Custom mutation commands receive an `input` field as a JSON string
130+
* from the CLI prompt. This parses it into a proper object.
131+
*/
132+
export function parseMutationInput(
133+
answers: Record<string, unknown>,
134+
): Record<string, unknown> {
135+
if (typeof answers.input === 'string') {
136+
try {
137+
const parsed = JSON.parse(answers.input);
138+
return { ...answers, input: parsed };
139+
} catch {
140+
return answers;
141+
}
142+
}
143+
return answers;
144+
}

0 commit comments

Comments
 (0)