Skip to content

Commit 6d54350

Browse files
committed
feat: add casing support for generated filenames
This allows users to control the naming convention of generated resolver files, supporting common filename formats, while preserving original default functionality. Details: - Add change-case-all dependency to typescript-resolver-files package - Add fileOutputCasing config option with 'pascal-case' (default) and other casing support - Update preset config validation to include fileOutputCasing option - Add transformResolverFileName utility to convert resolver names to appropriate filename casing - Integrate filename transformation throughout the resolver generation pipeline - Add unit test coverage for filename transformation functionality - Add e2e test suite - Update documentation in README.md
1 parent 7f4ad8f commit 6d54350

File tree

13 files changed

+426
-12
lines changed

13 files changed

+426
-12
lines changed

packages/typescript-resolver-files-e2e/project.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,13 @@
165165
"rimraf -g \"{projectRoot}/src/test-complex-synth-generic-wrapper/**/*.generated.*\""
166166
],
167167
"parallel": false
168+
},
169+
"test-resolver-filename-case": {
170+
"commands": [
171+
"rimraf -g \"{projectRoot}/src/test-resolver-filename-case/**/resolvers/\"",
172+
"rimraf -g \"{projectRoot}/src/test-resolver-filename-case/**/*.generated.*\""
173+
],
174+
"parallel": false
168175
}
169176
}
170177
},
@@ -191,7 +198,8 @@
191198
"nx graphql-codegen typescript-resolver-files-e2e -c test-resolvers-auto-wireup --verbose",
192199
"nx graphql-codegen typescript-resolver-files-e2e -c test-federation --verbose",
193200
"nx graphql-codegen typescript-resolver-files-e2e -c test-deep-modules --verbose",
194-
"nx graphql-codegen typescript-resolver-files-e2e -c test-complex-synth-generic-wrapper --verbose"
201+
"nx graphql-codegen typescript-resolver-files-e2e -c test-complex-synth-generic-wrapper --verbose",
202+
"nx graphql-codegen typescript-resolver-files-e2e -c test-resolver-filename-case --verbose"
195203
],
196204
"parallel": false
197205
},
@@ -269,6 +277,9 @@
269277
},
270278
"test-complex-synth-generic-wrapper": {
271279
"configFile": "{projectRoot}/src/test-complex-synth-generic-wrapper/codegen.ts"
280+
},
281+
"test-resolver-filename-case": {
282+
"configFile": "{projectRoot}/src/test-resolver-filename-case/codegen.ts"
272283
}
273284
},
274285
"dependsOn": ["prepare-e2e-modules"]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { CodegenConfig } from '@graphql-codegen/cli';
2+
import { defineConfig } from '@eddeee888/gcg-typescript-resolver-files';
3+
4+
const config: CodegenConfig = {
5+
hooks: {
6+
afterAllFileWrite: ['prettier --write'],
7+
},
8+
generates: {
9+
'packages/typescript-resolver-files-e2e/src/test-filename-case-kebab/schema':
10+
defineConfig(
11+
{
12+
fileOutputCasing: 'kebab-case',
13+
},
14+
{
15+
schema: [
16+
'packages/typescript-resolver-files-e2e/src/test-filename-case-kebab/**/*.graphqls',
17+
'packages/typescript-resolver-files-e2e/src/test-filename-case-kebab/**/*.graphqls.ts',
18+
],
19+
}
20+
),
21+
},
22+
};
23+
24+
export default config;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
scalar DateTime
3+
scalar SomeRandomScalar
4+
5+
type Query {
6+
# Simple: "randomQuery" -> "random-query.ts"
7+
randomQuery: RandomScalar!
8+
# Strange: "api-random-query.ts"
9+
APIRandomQuery: APIRandomQuery!
10+
}
11+
12+
type Mutation {
13+
# Strange: "random-api-mutation.ts"
14+
RandomAPIMutation: RandomAPIMutationResult!
15+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import gql from 'graphql-tag';
2+
3+
export const userTypeDefs = gql`
4+
extend type Query {
5+
userProfile(id: ID!): UserProfile!
6+
user(id: ID!): UserPayload!
7+
}
8+
9+
# Single-word type: User -> user.ts
10+
type User {
11+
id: ID!
12+
}
13+
14+
# Multi-word type: UserProfile -> user-profile.ts
15+
type UserProfile {
16+
id: ID!
17+
}
18+
`;

packages/typescript-resolver-files/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,44 @@ Hint: To see why certain files are skipped, run codegen command with `DEBUG` tur
261261
DEBUG="@eddeee888/gcg-typescript-resolver-files" yarn graphql-codegen
262262
```
263263

264+
### fileOutputCasing
265+
266+
`string` (Default: `pascal-case`)
267+
268+
Controls the naming convention used for generated resolver filenames. By default, resolver files are generated using PascalCase (e.g., `UserProfile.ts`), but you can customize this to match your project's file naming conventions.
269+
270+
Supported options:
271+
- `pascal-case` (default): `UserProfile.ts`, `APIKey.ts`
272+
- `kebab-case` or `param-case`: `user-profile.ts`, `api-key.ts`
273+
- `camel-case`: `userProfile.ts`, `apiKey.ts`
274+
- `snake-case`: `user_profile.ts`, `api_key.ts`
275+
- `constant-case`: `USER_PROFILE.ts`, `API_KEY.ts`
276+
- `dot-case`: `user.profile.ts`, `api.key.ts`
277+
- `header-case`: `User-Profile.ts`, `Api-Key.ts`
278+
- `lower-case`: `userprofile.ts`, `apikey.ts`
279+
- `upper-case`: `USERPROFILE.ts`, `APIKEY.ts`
280+
- `no-case`: `user profile.ts`, `api key.ts`
281+
- `path-case`: `user/profile.ts`, `api/key.ts`
282+
- `sentence-case`: `User profile.ts`, `Api key.ts`
283+
- `title-case`: `UserProfile.ts`, `APIKey.ts`
284+
- `capital-case`: `User Profile.ts`, `Api Key.ts`
285+
286+
#### Example
287+
288+
```ts
289+
// codegen.ts
290+
defineConfig({
291+
fileOutputCasing: 'kebab-case',
292+
});
293+
```
294+
295+
This configuration will generate resolver files like:
296+
- `src/schema/user/resolvers/user-profile.ts` (instead of `UserProfile.ts`)
297+
- `src/schema/user/resolvers/Query/user-by-id.ts` (instead of `userById.ts`)
298+
- `src/schema/api/resolvers/api-key.ts` (instead of `APIKey.ts`)
299+
300+
Note: This option only affects the filename casing. The exported resolver variable names and TypeScript interfaces remain unchanged to maintain valid JavaScript identifiers.
301+
264302
### typeDefsFileMode
265303

266304
`merged` or `mergedWhitelisted` or `modules` (Default: `merged`)

packages/typescript-resolver-files/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@graphql-codegen/typescript-resolvers": "^4.4.2",
3636
"@graphql-tools/merge": "^9.0.4",
3737
"@graphql-tools/utils": "^10.0.0",
38+
"change-case-all": "^1.0.0",
3839
"micromatch": "^4.0.0",
3940
"ts-morph": "^22.0.0",
4041
"tslib": "^2.3.0"

packages/typescript-resolver-files/src/parseGraphQLSchema/parseGraphQLSchema.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
isNativeNamedType,
2525
isRootObjectType,
2626
relativeModulePath,
27+
transformResolverFileName,
2728
type RootObjectType,
2829
} from '../utils';
2930
import { parseLocationForOutputDir } from './parseLocationForOutputDir';
@@ -42,6 +43,7 @@ interface ParseGraphQLSchemaParams {
4243
resolverRelativeTargetDir: string;
4344
whitelistedModules: ParsedPresetConfig['whitelistedModules'];
4445
blacklistedModules: ParsedPresetConfig['blacklistedModules'];
46+
fileOutputCasing: ParsedPresetConfig['fileOutputCasing'];
4547
}
4648

4749
export interface ResolverDetails {
@@ -113,6 +115,7 @@ export const parseGraphQLSchema = async ({
113115
resolverRelativeTargetDir,
114116
whitelistedModules,
115117
blacklistedModules,
118+
fileOutputCasing,
116119
}: ParseGraphQLSchemaParams): Promise<ParsedGraphQLSchemaMeta> => {
117120
const scalarsModuleResolverMap = scalarsModule
118121
? await getScalarResolverMapFromModule(scalarsModule)
@@ -144,6 +147,7 @@ export const parseGraphQLSchema = async ({
144147
nestedDirs: [schemaType],
145148
location: fieldNode.astNode?.loc,
146149
resolverName: fieldName,
150+
fileOutputCasing,
147151
});
148152
if (!resolverDetails) {
149153
return;
@@ -187,6 +191,7 @@ export const parseGraphQLSchema = async ({
187191
namedType,
188192
schemaType,
189193
result: res,
194+
fileOutputCasing,
190195
});
191196
return res;
192197
}
@@ -220,6 +225,7 @@ export const parseGraphQLSchema = async ({
220225
nestedDirs: [],
221226
location: namedType.astNode?.loc,
222227
resolverName: namedType.name,
228+
fileOutputCasing,
223229
});
224230

225231
if (resolverDetails) {
@@ -358,6 +364,7 @@ const handleObjectType = ({
358364
namedType,
359365
schemaType,
360366
result,
367+
fileOutputCasing,
361368
}: {
362369
mode: ParseGraphQLSchemaParams['mode'];
363370
sourceMap: ParseGraphQLSchemaParams['sourceMap'];
@@ -369,6 +376,7 @@ const handleObjectType = ({
369376
namedType: GraphQLObjectType;
370377
schemaType: string;
371378
result: ParsedGraphQLSchemaMeta;
379+
fileOutputCasing: ParsedPresetConfig['fileOutputCasing'];
372380
}): void => {
373381
// parse for details
374382
const fieldsByGraphQLModule = Object.entries(namedType.getFields()).reduce<
@@ -420,6 +428,7 @@ const handleObjectType = ({
420428
nestedDirs: [],
421429
location: firstFieldLocation,
422430
resolverName: namedType.name,
431+
fileOutputCasing,
423432
});
424433

425434
if (!resolverDetails) {
@@ -457,6 +466,7 @@ const createResolverDetails = ({
457466
nestedDirs,
458467
location,
459468
resolverName,
469+
fileOutputCasing,
460470
}: {
461471
belongsToRootObject: RootObjectType | null;
462472
mode: ParseGraphQLSchemaParams['mode'];
@@ -470,6 +480,7 @@ const createResolverDetails = ({
470480
nestedDirs: string[];
471481
location: Location | undefined;
472482
resolverName: string;
483+
fileOutputCasing: ParsedPresetConfig['fileOutputCasing'];
473484
}): ResolverDetails | undefined => {
474485
const parsedDetails = parseLocationForOutputDir({
475486
nestedDirs,
@@ -494,16 +505,17 @@ const createResolverDetails = ({
494505
belongsToRootObject
495506
);
496507

508+
const transformedFileName = transformResolverFileName(resolverName, fileOutputCasing);
497509
const resolverFilePath = path.posix.join(
498510
resolversOutputDir,
499-
`${resolverName}.ts`
511+
`${transformedFileName}.ts`
500512
);
501513

502514
return {
503515
schemaType,
504516
moduleName,
505517
resolverFile: {
506-
name: resolverName,
518+
name: transformedFileName,
507519
path: resolverFilePath,
508520
isOnFilesystem: fs.existsSync(resolverFilePath),
509521
},

packages/typescript-resolver-files/src/preset.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export const preset: Types.OutputPreset<RawPresetConfig> = {
7171
tsMorphProjectOptions,
7272
fixObjectTypeResolvers,
7373
emitLegacyCommonJSImports,
74+
fileOutputCasing,
7475
} = validatePresetConfig(rawPresetConfig);
7576

7677
const resolverTypesPath = path.posix.join(
@@ -121,6 +122,7 @@ export const preset: Types.OutputPreset<RawPresetConfig> = {
121122
resolverRelativeTargetDir,
122123
whitelistedModules,
123124
blacklistedModules,
125+
fileOutputCasing,
124126
}),
125127
createProfilerRunName('parseGraphQLSchema')
126128
);

packages/typescript-resolver-files/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export * from './printImportLine';
99
export * from './normalizeRelativePath';
1010
export * from './relativeModulePath';
1111
export * from './isMatchResolverNamePattern';
12+
export * from './transformResolverFileName';

0 commit comments

Comments
 (0)