Skip to content

Commit 0fb16ea

Browse files
committed
feat(codegen): add docs generation for ORM, React Query, and multi-target support
- Move docs config to top-level GraphQLSDKConfigTarget (removed from CliConfig) - Create shared docs-utils.ts with common utilities - Add ORM docs generator (README, AGENTS.md, MCP tools, skills) - Add React Query hooks docs generator (README, AGENTS.md, MCP tools, skills) - Add per-target README and combined MCP config generators - Add root-root README for multi-target configs - Add generateMulti() to core for multi-target orchestration - Update CLI entry point to use core generateMulti() - Wire all docs generation into generate.ts - 32 tests, 31 snapshots covering all doc formats
1 parent 1fe5255 commit 0fb16ea

11 files changed

Lines changed: 4535 additions & 1826 deletions

File tree

graphql/codegen/src/__tests__/codegen/__snapshots__/cli-generator.test.ts.snap

Lines changed: 2727 additions & 1550 deletions
Large diffs are not rendered by default.

graphql/codegen/src/__tests__/codegen/cli-generator.test.ts

Lines changed: 160 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,28 @@
11
import { generateCli } from '../../core/codegen/cli';
2+
import {
3+
generateReadme as generateCliReadme,
4+
generateAgentsDocs as generateCliAgentsDocs,
5+
getCliMcpTools,
6+
generateSkills as generateCliSkills,
7+
} from '../../core/codegen/cli/docs-generator';
8+
import { resolveDocsConfig } from '../../core/codegen/docs-utils';
9+
import {
10+
generateOrmReadme,
11+
generateOrmAgentsDocs,
12+
getOrmMcpTools,
13+
generateOrmSkills,
14+
} from '../../core/codegen/orm/docs-generator';
15+
import {
16+
generateHooksReadme,
17+
generateHooksAgentsDocs,
18+
getHooksMcpTools,
19+
generateHooksSkills,
20+
} from '../../core/codegen/hooks-docs-generator';
21+
import {
22+
generateTargetReadme,
23+
generateCombinedMcpConfig,
24+
generateRootRootReadme,
25+
} from '../../core/codegen/target-docs-generator';
226
import type {
327
CleanFieldType,
428
CleanOperation,
@@ -129,34 +153,10 @@ describe('cli-generator', () => {
129153
customQueries: 1,
130154
customMutations: 1,
131155
infraFiles: 3,
132-
totalFiles: 10,
156+
totalFiles: 8,
133157
});
134158
});
135159

136-
it('generates README.md by default', () => {
137-
const file = result.files.find((f) => f.fileName === 'README.md');
138-
expect(file).toBeDefined();
139-
expect(file!.content).toMatchSnapshot();
140-
});
141-
142-
it('generates AGENTS.md by default', () => {
143-
const file = result.files.find((f) => f.fileName === 'AGENTS.md');
144-
expect(file).toBeDefined();
145-
expect(file!.content).toMatchSnapshot();
146-
});
147-
148-
it('does not generate mcp.json by default', () => {
149-
const file = result.files.find((f) => f.fileName === 'mcp.json');
150-
expect(file).toBeUndefined();
151-
});
152-
153-
it('does not generate skills by default', () => {
154-
const skillFiles = result.files.filter((f) =>
155-
f.fileName.startsWith('skills/'),
156-
);
157-
expect(skillFiles).toHaveLength(0);
158-
});
159-
160160
it('generates executor.ts', () => {
161161
const file = result.files.find((f) => f.fileName === 'executor.ts');
162162
expect(file).toBeDefined();
@@ -241,8 +241,6 @@ describe('cli-generator', () => {
241241
it('generates correct file names', () => {
242242
const fileNames = result.files.map((f) => f.fileName).sort();
243243
expect(fileNames).toEqual([
244-
'AGENTS.md',
245-
'README.md',
246244
'commands.ts',
247245
'commands/auth.ts',
248246
'commands/car.ts',
@@ -255,91 +253,159 @@ describe('cli-generator', () => {
255253
});
256254
});
257255

258-
describe('cli-generator docs: true (all formats)', () => {
259-
const result = generateCli({
260-
tables: [carTable, driverTable],
261-
customOperations: {
262-
queries: [currentUserQuery],
263-
mutations: [loginMutation],
264-
},
265-
config: {
266-
cli: { toolName: 'myapp', docs: true },
267-
},
256+
const allCustomOps: CleanOperation[] = [currentUserQuery, loginMutation];
257+
258+
describe('cli docs generator', () => {
259+
it('generates CLI README', () => {
260+
const readme = generateCliReadme([carTable, driverTable], allCustomOps, 'myapp');
261+
expect(readme.fileName).toBe('README.md');
262+
expect(readme.content).toMatchSnapshot();
268263
});
269264

270-
it('generates mcp.json', () => {
271-
const file = result.files.find((f) => f.fileName === 'mcp.json');
272-
expect(file).toBeDefined();
273-
expect(file!.content).toMatchSnapshot();
265+
it('generates CLI AGENTS.md', () => {
266+
const agents = generateCliAgentsDocs([carTable, driverTable], allCustomOps, 'myapp');
267+
expect(agents.fileName).toBe('AGENTS.md');
268+
expect(agents.content).toMatchSnapshot();
274269
});
275270

276-
it('generates skill files', () => {
277-
const skillFiles = result.files.filter((f) =>
278-
f.fileName.startsWith('skills/'),
279-
);
280-
expect(skillFiles.length).toBeGreaterThan(0);
281-
for (const sf of skillFiles) {
271+
it('generates CLI MCP tools', () => {
272+
const tools = getCliMcpTools([carTable, driverTable], allCustomOps, 'myapp');
273+
expect(tools.length).toBeGreaterThan(0);
274+
for (const tool of tools) {
275+
expect(tool.name).toBeDefined();
276+
expect(tool.description).toBeDefined();
277+
expect(tool.inputSchema).toBeDefined();
278+
}
279+
});
280+
281+
it('generates CLI skill files', () => {
282+
const skills = generateCliSkills([carTable, driverTable], allCustomOps, 'myapp');
283+
expect(skills.length).toBeGreaterThan(0);
284+
for (const sf of skills) {
282285
expect(sf.content).toMatchSnapshot();
283286
}
284287
});
288+
});
285289

286-
it('generates correct file names with all docs', () => {
287-
const fileNames = result.files.map((f) => f.fileName).sort();
288-
expect(fileNames).toEqual([
289-
'AGENTS.md',
290-
'README.md',
291-
'commands.ts',
292-
'commands/auth.ts',
293-
'commands/car.ts',
294-
'commands/context.ts',
295-
'commands/current-user.ts',
296-
'commands/driver.ts',
297-
'commands/login.ts',
298-
'executor.ts',
299-
'mcp.json',
300-
'skills/auth.md',
301-
'skills/car.md',
302-
'skills/context.md',
303-
'skills/current-user.md',
304-
'skills/driver.md',
305-
'skills/login.md',
306-
]);
290+
describe('orm docs generator', () => {
291+
it('generates ORM README', () => {
292+
const readme = generateOrmReadme([carTable, driverTable], allCustomOps);
293+
expect(readme.fileName).toBe('README.md');
294+
expect(readme.content).toMatchSnapshot();
307295
});
308296

309-
it('mcp.json has valid tool definitions', () => {
310-
const file = result.files.find((f) => f.fileName === 'mcp.json');
311-
const parsed = JSON.parse(file!.content);
312-
expect(parsed.name).toBe('myapp');
313-
expect(parsed.tools).toBeDefined();
314-
expect(parsed.tools.length).toBeGreaterThan(0);
315-
for (const tool of parsed.tools) {
297+
it('generates ORM AGENTS.md', () => {
298+
const agents = generateOrmAgentsDocs([carTable, driverTable], allCustomOps);
299+
expect(agents.fileName).toBe('AGENTS.md');
300+
expect(agents.content).toMatchSnapshot();
301+
});
302+
303+
it('generates ORM MCP tools', () => {
304+
const tools = getOrmMcpTools([carTable, driverTable], allCustomOps);
305+
expect(tools.length).toBeGreaterThan(0);
306+
for (const tool of tools) {
316307
expect(tool.name).toBeDefined();
317308
expect(tool.description).toBeDefined();
318309
expect(tool.inputSchema).toBeDefined();
319310
}
320311
});
312+
313+
it('generates ORM skill files', () => {
314+
const skills = generateOrmSkills([carTable, driverTable], allCustomOps);
315+
expect(skills.length).toBeGreaterThan(0);
316+
for (const sf of skills) {
317+
expect(sf.content).toMatchSnapshot();
318+
}
319+
});
321320
});
322321

323-
describe('cli-generator docs: false', () => {
324-
const result = generateCli({
325-
tables: [carTable, driverTable],
326-
customOperations: {
327-
queries: [currentUserQuery],
328-
mutations: [loginMutation],
329-
},
330-
config: {
331-
cli: { toolName: 'myapp', docs: false },
332-
},
322+
describe('hooks docs generator', () => {
323+
it('generates hooks README', () => {
324+
const readme = generateHooksReadme([carTable, driverTable], allCustomOps);
325+
expect(readme.fileName).toBe('README.md');
326+
expect(readme.content).toMatchSnapshot();
333327
});
334328

335-
it('generates no doc files', () => {
336-
const docFiles = result.files.filter(
337-
(f) =>
338-
f.fileName === 'README.md' ||
339-
f.fileName === 'AGENTS.md' ||
340-
f.fileName === 'mcp.json' ||
341-
f.fileName.startsWith('skills/'),
342-
);
343-
expect(docFiles).toHaveLength(0);
329+
it('generates hooks AGENTS.md', () => {
330+
const agents = generateHooksAgentsDocs([carTable, driverTable], allCustomOps);
331+
expect(agents.fileName).toBe('AGENTS.md');
332+
expect(agents.content).toMatchSnapshot();
333+
});
334+
335+
it('generates hooks MCP tools', () => {
336+
const tools = getHooksMcpTools([carTable, driverTable], allCustomOps);
337+
expect(tools.length).toBeGreaterThan(0);
338+
for (const tool of tools) {
339+
expect(tool.name).toBeDefined();
340+
expect(tool.description).toBeDefined();
341+
expect(tool.inputSchema).toBeDefined();
342+
}
343+
});
344+
345+
it('generates hooks skill files', () => {
346+
const skills = generateHooksSkills([carTable, driverTable], allCustomOps);
347+
expect(skills.length).toBeGreaterThan(0);
348+
for (const sf of skills) {
349+
expect(sf.content).toMatchSnapshot();
350+
}
351+
});
352+
});
353+
354+
describe('target docs generator', () => {
355+
it('generates per-target README', () => {
356+
const readme = generateTargetReadme({
357+
hasOrm: true,
358+
hasHooks: true,
359+
hasCli: true,
360+
tableCount: 2,
361+
customQueryCount: 1,
362+
customMutationCount: 1,
363+
config: { cli: { toolName: 'myapp' } },
364+
});
365+
expect(readme.fileName).toBe('README.md');
366+
expect(readme.content).toMatchSnapshot();
367+
});
368+
369+
it('generates combined MCP config', () => {
370+
const cliTools = getCliMcpTools([carTable, driverTable], allCustomOps, 'myapp');
371+
const ormTools = getOrmMcpTools([carTable, driverTable], allCustomOps);
372+
const allTools = [...cliTools, ...ormTools];
373+
const mcp = generateCombinedMcpConfig(allTools, 'myapp');
374+
expect(mcp.fileName).toBe('mcp.json');
375+
const parsed = JSON.parse(mcp.content);
376+
expect(parsed.name).toBe('myapp');
377+
expect(parsed.tools.length).toBe(allTools.length);
378+
expect(mcp.content).toMatchSnapshot();
379+
});
380+
381+
it('generates root-root README for multi-target', () => {
382+
const readme = generateRootRootReadme([
383+
{ name: 'auth', output: './generated/auth', endpoint: 'http://auth.localhost/graphql', generators: ['ORM'] },
384+
{ name: 'app', output: './generated/app', endpoint: 'http://app.localhost/graphql', generators: ['ORM', 'React Query', 'CLI'] },
385+
]);
386+
expect(readme.fileName).toBe('README.md');
387+
expect(readme.content).toMatchSnapshot();
388+
});
389+
});
390+
391+
describe('resolveDocsConfig', () => {
392+
it('defaults to readme + agents', () => {
393+
const config = resolveDocsConfig(undefined);
394+
expect(config).toEqual({ readme: true, agents: true, mcp: false, skills: false });
395+
});
396+
397+
it('docs: true enables all', () => {
398+
const config = resolveDocsConfig(true);
399+
expect(config).toEqual({ readme: true, agents: true, mcp: true, skills: true });
400+
});
401+
402+
it('docs: false disables all', () => {
403+
const config = resolveDocsConfig(false);
404+
expect(config).toEqual({ readme: false, agents: false, mcp: false, skills: false });
405+
});
406+
407+
it('partial config fills defaults', () => {
408+
const config = resolveDocsConfig({ mcp: true });
409+
expect(config).toEqual({ readme: true, agents: true, mcp: true, skills: false });
344410
});
345411
});

graphql/codegen/src/cli/index.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { CLI, CLIOptions, getPackageJson, Inquirerer } from 'inquirerer';
99

1010
import { findConfigFile, loadConfigFile } from '../core/config';
11-
import { generate } from '../core/generate';
11+
import { generate, generateMulti } from '../core/generate';
1212
import { mergeConfig, type GraphQLSDKConfigTarget } from '../types/config';
1313
import {
1414
buildDbConfig,
@@ -96,7 +96,6 @@ export const commands = async (
9696

9797
if (isMulti) {
9898
const targets = config as Record<string, GraphQLSDKConfigTarget>;
99-
const names = targetName ? [targetName] : Object.keys(targets);
10099

101100
if (targetName && !targets[targetName]) {
102101
console.error(
@@ -111,14 +110,19 @@ export const commands = async (
111110
camelizeArgv(argv as Record<string, any>),
112111
),
113112
);
114-
let hasError = false;
115-
for (const name of names) {
113+
114+
const selectedTargets = targetName
115+
? { [targetName]: targets[targetName] }
116+
: targets;
117+
118+
const { results, hasError } = await generateMulti({
119+
configs: selectedTargets,
120+
cliOverrides: cliOptions as Partial<GraphQLSDKConfigTarget>,
121+
});
122+
123+
for (const { name, result } of results) {
116124
console.log(`\n[${name}]`);
117-
const result = await generate(
118-
mergeConfig(targets[name], cliOptions as GraphQLSDKConfigTarget),
119-
);
120125
printResult(result);
121-
if (!result.success) hasError = true;
122126
}
123127

124128
prompter.close();

0 commit comments

Comments
 (0)