Skip to content

Commit a3309e6

Browse files
authored
transform documents hooks (#8723)
1 parent 4083f10 commit a3309e6

File tree

15 files changed

+711
-11
lines changed

15 files changed

+711
-11
lines changed

.changeset/short-toes-relax.md

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
---
2+
"@graphql-codegen/cli": minor
3+
"@graphql-codegen/core": minor
4+
"@graphql-codegen/plugin-helpers": minor
5+
"@graphql-codegen/client-preset": minor
6+
"@graphql-codegen/gql-tag-operations-preset": minor
7+
"@graphql-codegen/graphql-modules-preset": minor
8+
---
9+
10+
Introduce a new feature called DocumentTransform.
11+
12+
DocumentTransform is a functionality that allows you to modify `documents` before they are processed by plugins. You can use functions passed to the `documentTransforms` option to make changes to GraphQL documents.
13+
14+
To use this feature, you can write `documentTransforms` as follows:
15+
16+
```ts
17+
import type { CodegenConfig } from '@graphql-codegen/cli';
18+
19+
const config: CodegenConfig = {
20+
schema: 'https://localhost:4000/graphql',
21+
documents: ['src/**/*.tsx'],
22+
generates: {
23+
'./src/gql/': {
24+
preset: 'client',
25+
documentTransforms: [
26+
{
27+
transform: ({ documents }) => {
28+
// Make some changes to the documents
29+
return documents;
30+
},
31+
},
32+
],
33+
},
34+
},
35+
};
36+
export default config;
37+
```
38+
39+
For instance, to remove a `@localOnlyDirective` directive from `documents`, you can write the following code:
40+
41+
```js
42+
import type { CodegenConfig } from '@graphql-codegen/cli';
43+
import { visit } from 'graphql';
44+
45+
const config: CodegenConfig = {
46+
schema: 'https://localhost:4000/graphql',
47+
documents: ['src/**/*.tsx'],
48+
generates: {
49+
'./src/gql/': {
50+
preset: 'client',
51+
documentTransforms: [
52+
{
53+
transform: ({ documents }) => {
54+
return documents.map(documentFile => {
55+
documentFile.document = visit(documentFile.document, {
56+
Directive: {
57+
leave(node) {
58+
if (node.name.value === 'localOnlyDirective') return null;
59+
},
60+
},
61+
});
62+
return documentFile;
63+
});
64+
},
65+
},
66+
],
67+
},
68+
},
69+
};
70+
export default config;
71+
```
72+
73+
DocumentTransform can also be specified by file name. You can create a custom file for a specific transformation and pass it to `documentTransforms`.
74+
75+
Let's create the document transform as a file:
76+
77+
```js
78+
module.exports = {
79+
transform: ({ documents }) => {
80+
// Make some changes to the documents
81+
return documents;
82+
},
83+
};
84+
```
85+
86+
Then, you can specify the file name as follows:
87+
88+
```ts
89+
import type { CodegenConfig } from '@graphql-codegen/cli';
90+
91+
const config: CodegenConfig = {
92+
schema: 'https://localhost:4000/graphql',
93+
documents: ['src/**/*.tsx'],
94+
generates: {
95+
'./src/gql/': {
96+
preset: 'client',
97+
documentTransforms: ['./my-document-transform.js'],
98+
},
99+
},
100+
};
101+
export default config;
102+
```

packages/graphql-codegen-cli/src/codegen.ts

+15
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { CodegenContext, ensureContext, shouldEmitLegacyCommonJSImports } from '
1818
import { getPluginByName } from './plugins.js';
1919
import { getPresetByName } from './presets.js';
2020
import { debugLog, printLogs } from './utils/debugging.js';
21+
import { getDocumentTransform } from './documentTransforms.js';
2122

2223
/**
2324
* Poor mans ESM detection.
@@ -316,6 +317,18 @@ export async function executeCodegen(input: CodegenContext | Types.Config): Prom
316317
emitLegacyCommonJSImports: shouldEmitLegacyCommonJSImports(config),
317318
};
318319

320+
const documentTransforms = Array.isArray(outputConfig.documentTransforms)
321+
? await Promise.all(
322+
outputConfig.documentTransforms.map(async (config, index) => {
323+
return await getDocumentTransform(
324+
config,
325+
makeDefaultLoader(context.cwd),
326+
`the element at index ${index} of the documentTransforms`
327+
);
328+
})
329+
)
330+
: [];
331+
319332
const outputs: Types.GenerateOptions[] = preset
320333
? await context.profiler.run(
321334
async () =>
@@ -330,6 +343,7 @@ export async function executeCodegen(input: CodegenContext | Types.Config): Prom
330343
pluginMap,
331344
pluginContext,
332345
profiler: context.profiler,
346+
documentTransforms,
333347
}),
334348
`Build Generates Section: ${filename}`
335349
)
@@ -344,6 +358,7 @@ export async function executeCodegen(input: CodegenContext | Types.Config): Prom
344358
pluginMap,
345359
pluginContext,
346360
profiler: context.profiler,
361+
documentTransforms,
347362
},
348363
];
349364

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { resolve } from 'path';
2+
import { Types } from '@graphql-codegen/plugin-helpers';
3+
4+
export async function getDocumentTransform(
5+
documentTransform: Types.OutputDocumentTransform,
6+
loader: Types.PackageLoaderFn<Types.DocumentTransformObject>,
7+
defaultName: string
8+
): Promise<Types.ConfiguredDocumentTransform> {
9+
if (typeof documentTransform === 'string') {
10+
const transformObject = await getDocumentTransformByName(documentTransform, loader);
11+
return { name: documentTransform, transformObject };
12+
}
13+
if (isTransformObject(documentTransform)) {
14+
return { name: defaultName, transformObject: documentTransform };
15+
}
16+
if (isTransformFileConfig(documentTransform)) {
17+
const name = Object.keys(documentTransform)[0];
18+
const transformObject = await getDocumentTransformByName(name, loader);
19+
return { name, transformObject, config: Object.values(documentTransform)[0] };
20+
}
21+
throw new Error(
22+
`
23+
An unknown format document transform: '${defaultName}'.
24+
`
25+
);
26+
}
27+
28+
function isTransformObject(config: Types.OutputDocumentTransform): config is Types.DocumentTransformObject {
29+
return typeof config === 'object' && config.transform && typeof config.transform === 'function';
30+
}
31+
32+
function isTransformFileConfig(config: Types.OutputDocumentTransform): config is Types.DocumentTransformFileConfig {
33+
const keys = Object.keys(config);
34+
return keys.length === 1 && typeof keys[0] === 'string';
35+
}
36+
37+
export async function getDocumentTransformByName(
38+
name: string,
39+
loader: Types.PackageLoaderFn<Types.DocumentTransformObject>
40+
): Promise<Types.DocumentTransformObject> {
41+
const possibleNames = [
42+
`@graphql-codegen/${name}`,
43+
`@graphql-codegen/${name}-document-transform`,
44+
name,
45+
resolve(process.cwd(), name),
46+
];
47+
48+
const possibleModules = possibleNames.concat(resolve(process.cwd(), name));
49+
50+
for (const moduleName of possibleModules) {
51+
try {
52+
return await loader(moduleName);
53+
} catch (err) {
54+
if (err.code !== 'MODULE_NOT_FOUND' && err.code !== 'ERR_MODULE_NOT_FOUND') {
55+
throw new Error(
56+
`
57+
Unable to load document transform matching '${name}'.
58+
Reason:
59+
${err.message}
60+
`
61+
);
62+
}
63+
}
64+
}
65+
66+
const possibleNamesMsg = possibleNames
67+
.map(name =>
68+
`
69+
- ${name}
70+
`.trimEnd()
71+
)
72+
.join('');
73+
74+
throw new Error(
75+
`
76+
Unable to find document transform matching '${name}'
77+
Install one of the following packages:
78+
79+
${possibleNamesMsg}
80+
`
81+
);
82+
}

0 commit comments

Comments
 (0)