diff --git a/.changeset/short-toes-relax.md b/.changeset/short-toes-relax.md new file mode 100644 index 00000000000..d352658a3ed --- /dev/null +++ b/.changeset/short-toes-relax.md @@ -0,0 +1,102 @@ +--- +"@graphql-codegen/cli": minor +"@graphql-codegen/core": minor +"@graphql-codegen/plugin-helpers": minor +"@graphql-codegen/client-preset": minor +"@graphql-codegen/gql-tag-operations-preset": minor +"@graphql-codegen/graphql-modules-preset": minor +--- + +Introduce a new feature called DocumentTransform. + +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. + +To use this feature, you can write `documentTransforms` as follows: + +```ts +import type { CodegenConfig } from '@graphql-codegen/cli'; + +const config: CodegenConfig = { + schema: 'https://localhost:4000/graphql', + documents: ['src/**/*.tsx'], + generates: { + './src/gql/': { + preset: 'client', + documentTransforms: [ + { + transform: ({ documents }) => { + // Make some changes to the documents + return documents; + }, + }, + ], + }, + }, +}; +export default config; +``` + +For instance, to remove a `@localOnlyDirective` directive from `documents`, you can write the following code: + +```js +import type { CodegenConfig } from '@graphql-codegen/cli'; +import { visit } from 'graphql'; + +const config: CodegenConfig = { + schema: 'https://localhost:4000/graphql', + documents: ['src/**/*.tsx'], + generates: { + './src/gql/': { + preset: 'client', + documentTransforms: [ + { + transform: ({ documents }) => { + return documents.map(documentFile => { + documentFile.document = visit(documentFile.document, { + Directive: { + leave(node) { + if (node.name.value === 'localOnlyDirective') return null; + }, + }, + }); + return documentFile; + }); + }, + }, + ], + }, + }, +}; +export default config; +``` + +DocumentTransform can also be specified by file name. You can create a custom file for a specific transformation and pass it to `documentTransforms`. + +Let's create the document transform as a file: + +```js +module.exports = { + transform: ({ documents }) => { + // Make some changes to the documents + return documents; + }, +}; +``` + +Then, you can specify the file name as follows: + +```ts +import type { CodegenConfig } from '@graphql-codegen/cli'; + +const config: CodegenConfig = { + schema: 'https://localhost:4000/graphql', + documents: ['src/**/*.tsx'], + generates: { + './src/gql/': { + preset: 'client', + documentTransforms: ['./my-document-transform.js'], + }, + }, +}; +export default config; +``` diff --git a/packages/graphql-codegen-cli/src/codegen.ts b/packages/graphql-codegen-cli/src/codegen.ts index 34e848a9f61..9dc115ef394 100644 --- a/packages/graphql-codegen-cli/src/codegen.ts +++ b/packages/graphql-codegen-cli/src/codegen.ts @@ -18,6 +18,7 @@ import { CodegenContext, ensureContext, shouldEmitLegacyCommonJSImports } from ' import { getPluginByName } from './plugins.js'; import { getPresetByName } from './presets.js'; import { debugLog, printLogs } from './utils/debugging.js'; +import { getDocumentTransform } from './documentTransforms.js'; /** * Poor mans ESM detection. @@ -316,6 +317,18 @@ export async function executeCodegen(input: CodegenContext | Types.Config): Prom emitLegacyCommonJSImports: shouldEmitLegacyCommonJSImports(config), }; + const documentTransforms = Array.isArray(outputConfig.documentTransforms) + ? await Promise.all( + outputConfig.documentTransforms.map(async (config, index) => { + return await getDocumentTransform( + config, + makeDefaultLoader(context.cwd), + `the element at index ${index} of the documentTransforms` + ); + }) + ) + : []; + const outputs: Types.GenerateOptions[] = preset ? await context.profiler.run( async () => @@ -330,6 +343,7 @@ export async function executeCodegen(input: CodegenContext | Types.Config): Prom pluginMap, pluginContext, profiler: context.profiler, + documentTransforms, }), `Build Generates Section: ${filename}` ) @@ -344,6 +358,7 @@ export async function executeCodegen(input: CodegenContext | Types.Config): Prom pluginMap, pluginContext, profiler: context.profiler, + documentTransforms, }, ]; diff --git a/packages/graphql-codegen-cli/src/documentTransforms.ts b/packages/graphql-codegen-cli/src/documentTransforms.ts new file mode 100644 index 00000000000..542020cfc7d --- /dev/null +++ b/packages/graphql-codegen-cli/src/documentTransforms.ts @@ -0,0 +1,82 @@ +import { resolve } from 'path'; +import { Types } from '@graphql-codegen/plugin-helpers'; + +export async function getDocumentTransform( + documentTransform: Types.OutputDocumentTransform, + loader: Types.PackageLoaderFn, + defaultName: string +): Promise { + if (typeof documentTransform === 'string') { + const transformObject = await getDocumentTransformByName(documentTransform, loader); + return { name: documentTransform, transformObject }; + } + if (isTransformObject(documentTransform)) { + return { name: defaultName, transformObject: documentTransform }; + } + if (isTransformFileConfig(documentTransform)) { + const name = Object.keys(documentTransform)[0]; + const transformObject = await getDocumentTransformByName(name, loader); + return { name, transformObject, config: Object.values(documentTransform)[0] }; + } + throw new Error( + ` + An unknown format document transform: '${defaultName}'. + ` + ); +} + +function isTransformObject(config: Types.OutputDocumentTransform): config is Types.DocumentTransformObject { + return typeof config === 'object' && config.transform && typeof config.transform === 'function'; +} + +function isTransformFileConfig(config: Types.OutputDocumentTransform): config is Types.DocumentTransformFileConfig { + const keys = Object.keys(config); + return keys.length === 1 && typeof keys[0] === 'string'; +} + +export async function getDocumentTransformByName( + name: string, + loader: Types.PackageLoaderFn +): Promise { + const possibleNames = [ + `@graphql-codegen/${name}`, + `@graphql-codegen/${name}-document-transform`, + name, + resolve(process.cwd(), name), + ]; + + const possibleModules = possibleNames.concat(resolve(process.cwd(), name)); + + for (const moduleName of possibleModules) { + try { + return await loader(moduleName); + } catch (err) { + if (err.code !== 'MODULE_NOT_FOUND' && err.code !== 'ERR_MODULE_NOT_FOUND') { + throw new Error( + ` + Unable to load document transform matching '${name}'. + Reason: + ${err.message} + ` + ); + } + } + } + + const possibleNamesMsg = possibleNames + .map(name => + ` + - ${name} + `.trimEnd() + ) + .join(''); + + throw new Error( + ` + Unable to find document transform matching '${name}' + Install one of the following packages: + + ${possibleNamesMsg} + ` + ); +} diff --git a/packages/graphql-codegen-cli/tests/codegen.spec.ts b/packages/graphql-codegen-cli/tests/codegen.spec.ts index 079bd23f1a5..e15a70bbf45 100644 --- a/packages/graphql-codegen-cli/tests/codegen.spec.ts +++ b/packages/graphql-codegen-cli/tests/codegen.spec.ts @@ -1,8 +1,9 @@ import { join } from 'path'; import { useMonorepo } from '@graphql-codegen/testing'; import { mergeTypeDefs } from '@graphql-tools/merge'; -import { buildASTSchema, buildSchema, GraphQLObjectType, parse, print } from 'graphql'; +import { buildASTSchema, buildSchema, GraphQLObjectType, parse, print, OperationDefinitionNode, Kind } from 'graphql'; import { createContext, executeCodegen } from '../src/index.js'; +import { Types } from '@graphql-codegen/plugin-helpers'; const SHOULD_NOT_THROW_STRING = 'SHOULD_NOT_THROW'; const SIMPLE_TEST_SCHEMA = `type MyType { f: String } type Query { f: String }`; @@ -164,7 +165,7 @@ describe('Codegen Executor', () => { } }); - it.only('Should throw when one output has no plugins or preset defined', async () => { + it('Should throw when one output has no plugins or preset defined', async () => { expect.assertions(1); try { await executeCodegen({ @@ -178,7 +179,7 @@ describe('Codegen Executor', () => { } }); - it.only('Should throw when one output has no plugins defined', async () => { + it('Should throw when one output has no plugins defined', async () => { expect.assertions(1); try { await executeCodegen({ @@ -194,7 +195,7 @@ describe('Codegen Executor', () => { } }); - it.only('Should succeed when one output has no plugins but preset defined', async () => { + it('Should succeed when one output has no plugins but preset defined', async () => { await executeCodegen({ schema: SIMPLE_TEST_SCHEMA, generates: { @@ -1110,4 +1111,198 @@ describe('Codegen Executor', () => { const output = await executeCodegen(config); expect(output[0].content).toContain('DocumentNode'); }); + + describe('Document Transform', () => { + it('Should transform documents', async () => { + const transform: Types.DocumentTransformFunction = ({ documents }) => { + const newDocuments = [ + { + document: { + ...documents[0].document, + definitions: [ + { + ...documents[0].document.definitions[0], + name: { kind: Kind.NAME, value: 'bar' }, + } as OperationDefinitionNode, + ], + }, + }, + ]; + return newDocuments; + }; + + const output = await executeCodegen({ + schema: SIMPLE_TEST_SCHEMA, + documents: `query foo { f }`, + generates: { + 'out1.ts': { + plugins: ['typescript', 'typescript-operations'], + documentTransforms: [{ transform }], + }, + }, + }); + + expect(output.length).toBe(1); + expect(output[0].content).toContain('export type BarQuery'); + }); + + it('Should allow users to set config', async () => { + const generateDocumentTransform: (config: { queryName: string }) => Types.DocumentTransformObject = ({ + queryName, + }) => { + return { + transform: ({ documents }) => { + const newDocuments = [ + { + document: { + ...documents[0].document, + definitions: [ + { + ...documents[0].document.definitions[0], + name: { kind: Kind.NAME, value: queryName }, + } as OperationDefinitionNode, + ], + }, + }, + ]; + return newDocuments; + }, + }; + }; + + const output = await executeCodegen({ + schema: SIMPLE_TEST_SCHEMA, + documents: `query foo { f }`, + generates: { + 'out1.ts': { + plugins: ['typescript', 'typescript-operations'], + documentTransforms: [generateDocumentTransform({ queryName: 'test' })], + }, + }, + }); + + expect(output.length).toBe(1); + expect(output[0].content).toContain('export type TestQuery'); + }); + + it('Should transform documents when specifying files', async () => { + const output = await executeCodegen({ + schema: SIMPLE_TEST_SCHEMA, + documents: `query root { f }`, + generates: { + 'out1.ts': { + plugins: ['typescript', 'typescript-operations'], + documentTransforms: ['./tests/custom-document-transforms/document-transform.js'], + }, + }, + }); + + expect(output.length).toBe(1); + expect(output[0].content).toContain('export type BarQuery'); + }); + + it('Should allow users to set config when specifying files', async () => { + const output = await executeCodegen({ + schema: SIMPLE_TEST_SCHEMA, + documents: `query root { f }`, + generates: { + 'out1.ts': { + plugins: ['typescript', 'typescript-operations'], + documentTransforms: [ + { + './tests/custom-document-transforms/test-config.js': { + queryName: 'test', + }, + }, + ], + }, + }, + }); + + expect(output.length).toBe(1); + expect(output[0].content).toContain('export type TestQuery'); + }); + + it('Should allow plugin context to be accessed and modified', async () => { + const output = await executeCodegen({ + schema: SIMPLE_TEST_SCHEMA, + documents: `query root { f }`, + generates: { + 'out1.ts': { + documentTransforms: [ + { + transform: ({ pluginContext, documents }) => { + pluginContext.myPluginInfo = 'world'; + return documents; + }, + }, + ], + plugins: ['./tests/custom-plugins/document-transform-context.js'], + }, + }, + }); + + expect(output.length).toBe(1); + expect(output[0].content).toContain('Hello world!'); + }); + + it('should throw an understandable error if it fails.', async () => { + try { + await executeCodegen({ + schema: SIMPLE_TEST_SCHEMA, + documents: `query foo { f }`, + generates: { + 'out1.ts': { + plugins: ['typescript'], + documentTransforms: [ + { + transform: () => { + throw new Error('Something Wrong!'); + }, + }, + ], + }, + }, + }); + throw new Error(SHOULD_NOT_THROW_STRING); + } catch (e) { + expect(e.message).not.toBe(SHOULD_NOT_THROW_STRING); + expect(e.message).toContain('DocumentTransform "the element at index 0 of the documentTransforms" failed'); + expect(e.message).toContain('Something Wrong!'); + } + }); + + it('Should transform documents with client-preset', async () => { + const transform: Types.DocumentTransformFunction = ({ documents }) => { + const newDocuments = [ + { + document: { + ...documents[0].document, + definitions: [ + { + ...documents[0].document.definitions[0], + name: { kind: Kind.NAME, value: 'bar' }, + } as OperationDefinitionNode, + ], + }, + }, + ]; + return newDocuments; + }; + + const output = await executeCodegen({ + schema: SIMPLE_TEST_SCHEMA, + documents: `query foo { f }`, + generates: { + './src/gql/': { + preset: 'client', + documentTransforms: [{ transform }], + }, + }, + }); + + const fileOutput = output.find(file => file.filename === './src/gql/graphql.ts'); + expect(fileOutput.content).toContain('export type BarQuery'); + }); + }); }); diff --git a/packages/graphql-codegen-cli/tests/custom-document-transforms/document-transform.js b/packages/graphql-codegen-cli/tests/custom-document-transforms/document-transform.js new file mode 100644 index 00000000000..347e0ea6992 --- /dev/null +++ b/packages/graphql-codegen-cli/tests/custom-document-transforms/document-transform.js @@ -0,0 +1,18 @@ +module.exports = { + transform: ({ documents }) => { + const newDocuments = [ + { + document: { + ...documents[0].document, + definitions: [ + { + ...documents[0].document.definitions[0], + name: { kind: 'Name', value: 'bar' }, + }, + ], + }, + }, + ]; + return newDocuments; + }, +}; diff --git a/packages/graphql-codegen-cli/tests/custom-document-transforms/test-config.js b/packages/graphql-codegen-cli/tests/custom-document-transforms/test-config.js new file mode 100644 index 00000000000..5944d539b1b --- /dev/null +++ b/packages/graphql-codegen-cli/tests/custom-document-transforms/test-config.js @@ -0,0 +1,18 @@ +module.exports = { + transform: ({ documents, config }) => { + const newDocuments = [ + { + document: { + ...documents[0].document, + definitions: [ + { + ...documents[0].document.definitions[0], + name: { kind: 'Name', value: config.queryName }, + }, + ], + }, + }, + ]; + return newDocuments; + }, +}; diff --git a/packages/graphql-codegen-cli/tests/custom-plugins/document-transform-context.js b/packages/graphql-codegen-cli/tests/custom-plugins/document-transform-context.js new file mode 100644 index 00000000000..2d61040fef4 --- /dev/null +++ b/packages/graphql-codegen-cli/tests/custom-plugins/document-transform-context.js @@ -0,0 +1,5 @@ +module.exports = { + plugin: (_schema, _documents, _config, { pluginContext }) => { + return `Hello ${pluginContext.myPluginInfo}!`; + }, +}; diff --git a/packages/graphql-codegen-core/src/codegen.ts b/packages/graphql-codegen-core/src/codegen.ts index b8d8d64ae87..e1da67b94ee 100644 --- a/packages/graphql-codegen-core/src/codegen.ts +++ b/packages/graphql-codegen-core/src/codegen.ts @@ -19,6 +19,7 @@ import { shouldValidateDocumentsAgainstSchema, shouldValidateDuplicateDocuments, } from './utils.js'; +import { transformDocuments } from './transform-document.js'; export async function codegen(options: Types.GenerateOptions): Promise { const documents = options.documents || []; @@ -71,7 +72,20 @@ export async function codegen(options: Types.GenerateOptions): Promise { const schemaDocumentNode = mergeNeeded || !options.schema ? getCachedDocumentNodeFromSchema(schemaInstance) : options.schema; - if (schemaInstance && documents.length > 0 && shouldValidateDocumentsAgainstSchema(skipDocumentsValidation)) { + const documentTransforms = Array.isArray(options.documentTransforms) ? options.documentTransforms : []; + const transformedDocuments = await transformDocuments({ + ...options, + documentTransforms, + schema: schemaDocumentNode, + schemaAst: schemaInstance, + profiler, + }); + + if ( + schemaInstance && + transformedDocuments.length > 0 && + shouldValidateDocumentsAgainstSchema(skipDocumentsValidation) + ) { const ignored = ['NoUnusedFragments', 'NoUnusedVariables', 'KnownDirectives']; if (typeof skipDocumentsValidation === 'object' && skipDocumentsValidation.ignoreRules) { ignored.push(...asArray(skipDocumentsValidation.ignoreRules)); @@ -87,18 +101,18 @@ export async function codegen(options: Types.GenerateOptions): Promise { const rules = specifiedRules.filter(rule => !ignored.some(ignoredRule => rule.name.startsWith(ignoredRule))); const schemaHash = extractHashFromSchema(schemaInstance); - if (!schemaHash || !options.cache || documents.some(d => typeof d.hash !== 'string')) { + if (!schemaHash || !options.cache || transformedDocuments.some(d => typeof d.hash !== 'string')) { return Promise.resolve( validateGraphQlDocuments( schemaInstance, - [...documents.flatMap(d => d.document), ...fragments.flatMap(f => f.document)], + [...transformedDocuments.flatMap(d => d.document), ...fragments.flatMap(f => f.document)], rules ) ); } const cacheKey = [schemaHash] - .concat(documents.map(doc => doc.hash)) + .concat(transformedDocuments.map(doc => doc.hash)) .concat(JSON.stringify(fragments)) .join(','); @@ -106,7 +120,7 @@ export async function codegen(options: Types.GenerateOptions): Promise { Promise.resolve( validateGraphQlDocuments( schemaInstance, - [...documents.flatMap(d => d.document), ...fragments.flatMap(f => f.document)], + [...transformedDocuments.flatMap(d => d.document), ...fragments.flatMap(f => f.document)], rules ) ) @@ -141,7 +155,7 @@ export async function codegen(options: Types.GenerateOptions): Promise { parentConfig: options.config, schema: schemaDocumentNode, schemaAst: schemaInstance, - documents: options.documents, + documents: transformedDocuments, outputFilename: options.filename, allPlugins: options.plugins, skipDocumentsValidation: options.skipDocumentsValidation, diff --git a/packages/graphql-codegen-core/src/transform-document.ts b/packages/graphql-codegen-core/src/transform-document.ts new file mode 100644 index 00000000000..68244350071 --- /dev/null +++ b/packages/graphql-codegen-core/src/transform-document.ts @@ -0,0 +1,45 @@ +import { createNoopProfiler, Types } from '@graphql-codegen/plugin-helpers'; + +export async function transformDocuments(options: Types.GenerateOptions): Promise { + const documentTransforms = options.documentTransforms || []; + let documents = options.documents; + if (documentTransforms.length === 0 || options.documents.length === 0) { + return documents; + } + + const profiler = options.profiler ?? createNoopProfiler(); + + for (const documentTransform of documentTransforms) { + const config = + typeof documentTransform.config === 'object' + ? { + ...options.config, + ...documentTransform.config, + } + : {}; + const { transform } = documentTransform.transformObject; + if (transform && typeof transform === 'function') { + const name = documentTransform.name; + try { + await profiler.run(async () => { + documents = await transform({ + documents, + schema: options.schema, + config, + pluginContext: options.pluginContext, + }); + }, `DocumentTransform "${name}" execution`); + } catch (e) { + throw new Error( + `DocumentTransform "${name}" failed: \n + ${e.message} + ` + ); + } + } else { + throw new Error(`Missing 'transform' function in "${documentTransform.name}"`); + } + } + + return documents; +} diff --git a/packages/presets/client/src/index.ts b/packages/presets/client/src/index.ts index 06dc482ba2f..f72e1b6392c 100644 --- a/packages/presets/client/src/index.ts +++ b/packages/presets/client/src/index.ts @@ -240,6 +240,7 @@ export const preset: Types.OutputPreset = { unmaskFunctionName: fragmentMaskingConfig.unmaskFunctionName, }, documents: [], + documentTransforms: options.documentTransforms, }; } @@ -266,6 +267,7 @@ export const preset: Types.OutputPreset = { schema: options.schema, config: {}, documents: [], + documentTransforms: options.documentTransforms, }; } @@ -280,6 +282,7 @@ export const preset: Types.OutputPreset = { ...forwardedConfig, }, documents: sources, + documentTransforms: options.documentTransforms, }, { filename: `${options.baseOutputDir}gql${gqlArtifactFileExtension}`, @@ -291,6 +294,7 @@ export const preset: Types.OutputPreset = { gqlTagName: options.presetConfig.gqlTagName || 'graphql', }, documents: sources, + documentTransforms: options.documentTransforms, }, ...(isPersistedOperations ? [ @@ -314,6 +318,7 @@ export const preset: Types.OutputPreset = { schema: options.schema, config: {}, documents: sources, + documentTransforms: options.documentTransforms, }, ] : []), diff --git a/packages/presets/gql-tag-operations/src/index.ts b/packages/presets/gql-tag-operations/src/index.ts index f6338fcf46b..44ce94d7d13 100644 --- a/packages/presets/gql-tag-operations/src/index.ts +++ b/packages/presets/gql-tag-operations/src/index.ts @@ -203,6 +203,7 @@ export const preset: Types.OutputPreset = { unmaskFunctionName: fragmentMaskingConfig.unmaskFunctionName, }, documents: [], + documentTransforms: options.documentTransforms, }; } @@ -226,6 +227,7 @@ export const preset: Types.OutputPreset = { schema: options.schema, config: {}, documents: [], + documentTransforms: options.documentTransforms, }; } @@ -237,6 +239,7 @@ export const preset: Types.OutputPreset = { schema: options.schema, config, documents: sources, + documentTransforms: options.documentTransforms, }, { filename: `${options.baseOutputDir}/gql${gqlArtifactFileExtension}`, @@ -249,6 +252,7 @@ export const preset: Types.OutputPreset = { gqlTagName: options.presetConfig.gqlTagName || 'gql', }, documents: sources, + documentTransforms: options.documentTransforms, }, ...(fragmentMaskingFileGenerateConfig ? [fragmentMaskingFileGenerateConfig] : []), ...(indexFileGenerateConfig ? [indexFileGenerateConfig] : []), diff --git a/packages/presets/graphql-modules/src/index.ts b/packages/presets/graphql-modules/src/index.ts index 0361d669380..a17717572eb 100644 --- a/packages/presets/graphql-modules/src/index.ts +++ b/packages/presets/graphql-modules/src/index.ts @@ -69,6 +69,7 @@ export const preset: Types.OutputPreset = { enumsAsTypes: true, }, schemaAst: options.schemaAst!, + documentTransforms: options.documentTransforms, }; const baseTypesFilename = baseTypesPath.replace(/\.(js|ts|d.ts)$/, ''); @@ -120,6 +121,7 @@ export const preset: Types.OutputPreset = { }, config: options.config, schemaAst: options.schemaAst, + documentTransforms: options.documentTransforms, }; }); diff --git a/packages/utils/plugins-helpers/src/types.ts b/packages/utils/plugins-helpers/src/types.ts index 8bbc7ea3b13..60446962171 100644 --- a/packages/utils/plugins-helpers/src/types.ts +++ b/packages/utils/plugins-helpers/src/types.ts @@ -19,6 +19,7 @@ export namespace Types { pluginContext?: { [key: string]: any }; profiler?: Profiler; cache?(namespace: string, key: string, factory: () => Promise): Promise; + documentTransforms?: ConfiguredDocumentTransform[]; } export type FileOutput = { @@ -316,6 +317,10 @@ export namespace Types { * For more details: https://graphql-code-generator.com/docs/config-reference/lifecycle-hooks */ hooks?: Partial; + /** + * @description DocumentTransform changes documents before executing plugins. + */ + documentTransforms?: OutputDocumentTransform[]; } /* Output Builder Preset */ @@ -340,6 +345,7 @@ export namespace Types { }; profiler?: Profiler; cache?(namespace: string, key: string, factory: () => Promise): Promise; + documentTransforms?: ConfiguredDocumentTransform[]; }; export type OutputPreset = { @@ -610,6 +616,28 @@ export namespace Types { skipValidationAgainstSchema?: boolean; } | boolean; + + export type DocumentTransformFunction = (options: { + documents: Types.DocumentFile[]; + schema: DocumentNode; + config: Config; + pluginContext?: { [key: string]: any }; + }) => Types.Promisable; + + export type DocumentTransformObject = { + transform: DocumentTransformFunction; + }; + + export type DocumentTransformFileName = string; + export type DocumentTransformFileConfig = { [name: DocumentTransformFileName]: T }; + export type DocumentTransformFile = DocumentTransformFileName | DocumentTransformFileConfig; + + export type OutputDocumentTransform = DocumentTransformObject | DocumentTransformFile; + export type ConfiguredDocumentTransform = { + name: string; + transformObject: DocumentTransformObject; + config?: T; + }; } export function isComplexPluginOutput(obj: Types.PluginOutput): obj is Types.ComplexPluginOutput { diff --git a/website/src/pages/docs/advanced/_meta.json b/website/src/pages/docs/advanced/_meta.json index 2d72cd594ab..4ca35c9e8ba 100644 --- a/website/src/pages/docs/advanced/_meta.json +++ b/website/src/pages/docs/advanced/_meta.json @@ -2,5 +2,6 @@ "generated-files-colocation": "Generated files colocation", "programmatic-usage": "Programmatic Usage", "how-does-it-work": "How does it work?", - "profiler": "Profiler" + "profiler": "Profiler", + "document-transform": "Document Transform" } diff --git a/website/src/pages/docs/advanced/document-transform.mdx b/website/src/pages/docs/advanced/document-transform.mdx new file mode 100644 index 00000000000..475acb99e50 --- /dev/null +++ b/website/src/pages/docs/advanced/document-transform.mdx @@ -0,0 +1,166 @@ +import { Callout } from '@theguild/components' + +# Document Transform + +Document transform is a feature that allows you to modify `documents` before they are used by plugins. +You can use functions passed to the `documentTransforms` option to make changes to GraphQL `documents`. + +## Basic Usage + +Document transform has `transform` function. + +Let's specify an object containing those functions as the `documentTransforms` option as follows: + +```ts {9-17} +import type { CodegenConfig } from '@graphql-codegen/cli' + +const config: CodegenConfig = { + schema: 'https://localhost:4000/graphql', + documents: ['src/**/*.tsx'], + generates: { + './src/gql/': { + preset: 'client', + documentTransforms: [ + { + transform: ({ documents }) => { + // Make some changes to the documents + return documents + } + } + ] + } + } +} +export default config +``` + +For instance, to remove a `@localOnlyDirective` directive from `documents`, you can write the following code: + +```ts {2, 13-22} +import type { CodegenConfig } from '@graphql-codegen/cli' +import { visit } from 'graphql' + +const config: CodegenConfig = { + schema: 'https://localhost:4000/graphql', + documents: ['src/**/*.tsx'], + generates: { + './src/gql/': { + preset: 'client', + documentTransforms: [ + { + transform: ({ documents }) => { + return documents.map(documentFile => { + documentFile.document = visit(documentFile.document, { + Directive: { + leave(node) { + if (node.name.value === 'localOnlyDirective') return null + } + } + }) + return documentFile + }) + } + } + ] + } + } +} +export default config +``` + +## How to specify by name + +The document transform can also be specified by file name. You can create a custom file and pass it to `documentTransforms`. + +Let's create a file named `./my-document-transform.js`: + +```js +module.exports = { + transform: ({ documents }) => { + // Make some changes to the documents + return documents + } +} +``` + +Then, you can specify the file name as follows: + +```ts {9} +import type { CodegenConfig } from '@graphql-codegen/cli' + +const config: CodegenConfig = { + schema: 'https://localhost:4000/graphql', + documents: ['src/**/*.tsx'], + generates: { + './src/gql/': { + preset: 'client', + documentTransforms: ['./my-document-transform.js'] + } + } +} +export default config +``` + +## Configuration + +If you want to change the behavior of a document transform at runtime, you can simply prepare a function that generates the document transform and write the configuration in its argument. + +```ts {3-10, 18} +import type { CodegenConfig } from '@graphql-codegen/cli' + +const generateDocumentTransform: (config: { queryName: string }) => Types.DocumentTransformObject = ({ queryName }) => { + return { + transform: ({ documents }) => { + // Modify something in documents using `queryName`. + return documents + } + } +} + +const config: CodegenConfig = { + schema: 'https://localhost:4000/graphql', + documents: ['src/**/*.tsx'], + generates: { + './src/gql/': { + preset: 'client', + documentTransforms: [generateDocumentTransform({ queryName: 'test' })] + } + } +} +export default config +``` + +If you want to specify the document transform by file, do the following: + +```ts {11-13} +import type { CodegenConfig } from '@graphql-codegen/cli' + +const config: CodegenConfig = { + schema: 'https://localhost:4000/graphql', + documents: ['src/**/*.tsx'], + generates: { + './src/gql/': { + preset: 'client', + documentTransforms: [ + { + './my-document-transform.js': { + queryName: 'test' + } + } + ] + } + } +} +export default config +``` + +In this case, you can retrieve the `queryName` from the `config` argument of the `transform` function as follows: + +```js +module.exports = { + transform: ({ documents, config }) => { + // Modify something in documents using `config.queryName`. + return documents + } +} +```