diff --git a/packages/graphql-codegen-cli/src/codegen.ts b/packages/graphql-codegen-cli/src/codegen.ts index 3eaffe8919e..acbf3ad83bc 100644 --- a/packages/graphql-codegen-cli/src/codegen.ts +++ b/packages/graphql-codegen-cli/src/codegen.ts @@ -14,7 +14,7 @@ import { import { NoTypeDefinitionsFound } from '@graphql-tools/load'; import { DocumentNode, GraphQLError, GraphQLSchema } from 'graphql'; import { Listr, ListrTask } from 'listr2'; -import { CodegenContext, ensureContext, shouldEmitLegacyCommonJSImports } from './config.js'; +import { CodegenContext, ensureContext, shouldEmitLegacyCommonJSImports, shouldPreserveTSExtension } from './config.js'; import { getPluginByName } from './plugins.js'; import { getPresetByName } from './presets.js'; import { debugLog, printLogs } from './utils/debugging.js'; @@ -327,6 +327,7 @@ export async function executeCodegen( ? { value: outputFileTemplateConfig } : outputFileTemplateConfig), emitLegacyCommonJSImports: shouldEmitLegacyCommonJSImports(config), + preserveTSExtension: shouldPreserveTSExtension(config), }; const documentTransforms = Array.isArray(outputConfig.documentTransforms) @@ -379,6 +380,7 @@ export async function executeCodegen( ...outputArgs, // @ts-expect-error todo: fix 'emitLegacyCommonJSImports' does not exist in type 'GenerateOptions' emitLegacyCommonJSImports: shouldEmitLegacyCommonJSImports(config, outputArgs.filename), + preserveTSExtension: shouldPreserveTSExtension(config), cache, }); result.push({ diff --git a/packages/graphql-codegen-cli/src/config.ts b/packages/graphql-codegen-cli/src/config.ts index 913056851a1..3c1687543b7 100644 --- a/packages/graphql-codegen-cli/src/config.ts +++ b/packages/graphql-codegen-cli/src/config.ts @@ -38,6 +38,7 @@ export type YamlCliFlags = { debug?: boolean; ignoreNoDocuments?: boolean; emitLegacyCommonJSImports?: boolean; + preserveTSExtension?: boolean; }; export function generateSearchPlaces(moduleName: string) { @@ -322,6 +323,10 @@ export function updateContextWithCliFlags(context: CodegenContext, cliFlags: Yam config.emitLegacyCommonJSImports = cliFlags['emit-legacy-common-js-imports'] === true; } + if (cliFlags['preserve-ts-extension'] !== undefined) { + config.preserveTSExtension = cliFlags['preserve-ts-extension'] === true; + } + if (cliFlags.project) { context.useProject(cliFlags.project); } @@ -504,3 +509,7 @@ export function shouldEmitLegacyCommonJSImports(config: Types.Config): boolean { return globalValue; } + +export function shouldPreserveTSExtension(config: Types.Config): boolean { + return config.preserveTSExtension === true; +} diff --git a/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts b/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts index 5f83d587e6c..a255b8bfc02 100644 --- a/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts +++ b/packages/plugins/other/visitor-plugin-common/src/base-visitor.ts @@ -36,6 +36,7 @@ export interface ParsedConfig { allowEnumStringTypes: boolean; inlineFragmentTypes: InlineFragmentTypeOptions; emitLegacyCommonJSImports: boolean; + preserveTSExtension: boolean; printFieldsOnNewLines: boolean; includeExternalFragments: boolean; } @@ -365,6 +366,11 @@ export interface RawConfig { * Default it will be `true` this way it ensure that generated code works with [non-compliant bundlers](https://github.com/dotansimha/graphql-code-generator/issues/8065). */ emitLegacyCommonJSImports?: boolean; + /** + * @description A flag to preserve `.ts` extension to the output file. Default: `false`. + * @default false + */ + preserveTSExtension?: boolean; /** * @default false @@ -410,6 +416,7 @@ export class BaseVisitor 0) { if (this.config.importDocumentNodeExternallyFrom === 'near-operation-file' && this._documents.length === 1) { let documentPath = `./${this.clearExtension(basename(this._documents[0].location))}`; - if (!this.config.emitLegacyCommonJSImports) { + if (this.config.preserveTSExtension) { + documentPath += '.ts'; + } else if (!this.config.emitLegacyCommonJSImports) { documentPath += '.js'; } @@ -674,6 +680,7 @@ export class ClientSideBaseVisitor< ), }, emitLegacyCommonJSImports: this.config.emitLegacyCommonJSImports, + preserveTSExtension: this.config.preserveTSExtension, }) ) .filter(fragmentImport => fragmentImport.outputPath !== fragmentImport.importSource.path); diff --git a/packages/plugins/other/visitor-plugin-common/src/imports.ts b/packages/plugins/other/visitor-plugin-common/src/imports.ts index b9e4cbde6ba..4d4969373b9 100644 --- a/packages/plugins/other/visitor-plugin-common/src/imports.ts +++ b/packages/plugins/other/visitor-plugin-common/src/imports.ts @@ -8,6 +8,7 @@ export type ImportDeclaration = { baseDir: string; typesImport: boolean; emitLegacyCommonJSImports: boolean; + preserveTSExtension: boolean; }; export type ImportSource = { @@ -51,13 +52,21 @@ export function generateFragmentImportStatement( } export function generateImportStatement(statement: ImportDeclaration): string { - const { baseDir, importSource, outputPath, typesImport } = statement; + const { baseDir, importSource, outputPath, typesImport, preserveTSExtension } = statement; const importPath = resolveImportPath(baseDir, outputPath, importSource.path); const importNames = importSource.identifiers?.length ? `{ ${Array.from(new Set(importSource.identifiers)).join(', ')} }` : '*'; - const importExtension = - importPath.startsWith('/') || importPath.startsWith('.') ? (statement.emitLegacyCommonJSImports ? '' : '.js') : ''; + + let importExtension = ''; + if (importPath.startsWith('/') || importPath.startsWith('.')) { + if (preserveTSExtension) { + importExtension = '.ts'; + } else if (!statement.emitLegacyCommonJSImports) { + importExtension = '.js'; + } + } + const importAlias = importSource.namespace ? ` as ${importSource.namespace}` : ''; const importStatement = typesImport ? 'import type' : 'import'; return `${importStatement} ${importNames}${importAlias} from '${importPath}${importExtension}';${ diff --git a/packages/plugins/other/visitor-plugin-common/tests/client-side-base-visitor.spec.ts b/packages/plugins/other/visitor-plugin-common/tests/client-side-base-visitor.spec.ts index 79a47954a6f..b6202e614bd 100644 --- a/packages/plugins/other/visitor-plugin-common/tests/client-side-base-visitor.spec.ts +++ b/packages/plugins/other/visitor-plugin-common/tests/client-side-base-visitor.spec.ts @@ -81,6 +81,76 @@ describe('getImports', () => { expect(imports[0]).toBe(`import * as Operations from './${fileName}.js';`); }); }); + + describe('when preserveTSExtension is true', () => { + it('appends `.ts` to Operations import path', () => { + const fileName = 'fooBarQuery'; + const importPath = `src/queries/${fileName}`; + + const document = parse( + `query fooBarQuery { + a { + foo + bar + } + } + ` + ); + + const visitor = new ClientSideBaseVisitor( + schema, + [], + { + emitLegacyCommonJSImports: false, + preserveTSExtension: true, + importDocumentNodeExternallyFrom: 'near-operation-file', + documentMode: DocumentMode.external, + }, + {}, + [{ document, location: importPath }] + ); + + visitor.OperationDefinition(document.definitions[0] as OperationDefinitionNode); + + const imports = visitor.getImports(); + expect(imports[0]).toBe(`import * as Operations from './${fileName}.ts';`); + }); + }); + + describe('when preserveTSExtension is false and emitLegacyCommonJSImports is true', () => { + it('does not append extension to Operations import path', () => { + const fileName = 'fooBarQuery'; + const importPath = `src/queries/${fileName}`; + + const document = parse( + `query fooBarQuery { + a { + foo + bar + } + } + ` + ); + + const visitor = new ClientSideBaseVisitor( + schema, + [], + { + emitLegacyCommonJSImports: true, + preserveTSExtension: false, + importDocumentNodeExternallyFrom: 'near-operation-file', + documentMode: DocumentMode.external, + }, + {}, + [{ document, location: importPath }] + ); + + visitor.OperationDefinition(document.definitions[0] as OperationDefinitionNode); + + const imports = visitor.getImports(); + expect(imports[0]).toBe(`import * as Operations from './${fileName}';`); + }); + }); }); describe('when documentMode "external", importDocumentNodeExternallyFrom is relative path', () => { @@ -186,6 +256,7 @@ describe('getImports', () => { ], }, emitLegacyCommonJSImports: true, + preserveTSExtension: false, typesImport: false, }, ], @@ -259,6 +330,7 @@ describe('getImports', () => { ], }, emitLegacyCommonJSImports: true, + preserveTSExtension: false, typesImport: false, }, ], diff --git a/packages/presets/client/src/fragment-masking-plugin.ts b/packages/presets/client/src/fragment-masking-plugin.ts index f3e69e3c6bf..b869a3b153b 100644 --- a/packages/presets/client/src/fragment-masking-plugin.ts +++ b/packages/presets/client/src/fragment-masking-plugin.ts @@ -128,11 +128,19 @@ export const plugin: PluginFunction<{ augmentedModuleName?: string; unmaskFunctionName?: string; emitLegacyCommonJSImports?: boolean; + preserveTSExtension?: boolean; isStringDocumentMode?: boolean; }> = ( _, __, - { useTypeImports, augmentedModuleName, unmaskFunctionName, emitLegacyCommonJSImports, isStringDocumentMode }, + { + useTypeImports, + augmentedModuleName, + unmaskFunctionName, + emitLegacyCommonJSImports, + preserveTSExtension, + isStringDocumentMode, + }, _info ) => { const documentNodeImport = `${useTypeImports ? 'import type' : 'import'} { ResultOf, DocumentTypeDecoration${ @@ -141,7 +149,7 @@ export const plugin: PluginFunction<{ const deferFragmentHelperImports = `${useTypeImports ? 'import type' : 'import'} { Incremental${ isStringDocumentMode ? ', TypedDocumentString' : '' - } } from './graphql${emitLegacyCommonJSImports ? '' : '.js'}';\n`; + } } from './graphql${preserveTSExtension ? '.ts' : emitLegacyCommonJSImports ? '' : '.js'}';\n`; const fragmentDefinitionNodeImport = isStringDocumentMode ? '' diff --git a/packages/presets/client/src/index.ts b/packages/presets/client/src/index.ts index 73ee7dac239..312f7ab7849 100644 --- a/packages/presets/client/src/index.ts +++ b/packages/presets/client/src/index.ts @@ -273,6 +273,7 @@ export const preset: Types.OutputPreset = { useTypeImports: options.config.useTypeImports, unmaskFunctionName: fragmentMaskingConfig.unmaskFunctionName, emitLegacyCommonJSImports: options.config.emitLegacyCommonJSImports, + preserveTSExtension: options.config.preserveTSExtension, isStringDocumentMode: options.config.documentMode === DocumentMode.string, }, documents: [], @@ -282,7 +283,11 @@ export const preset: Types.OutputPreset = { let indexFileGenerateConfig: Types.GenerateOptions | null = null; - const reexportsExtension = options.config.emitLegacyCommonJSImports ? '' : '.js'; + const reexportsExtension = options.config.preserveTSExtension + ? '.ts' + : options.config.emitLegacyCommonJSImports + ? '' + : '.js'; if (reexports.length) { indexFileGenerateConfig = { diff --git a/packages/presets/graphql-modules/src/index.ts b/packages/presets/graphql-modules/src/index.ts index bcfab88a76a..d5bf93d4598 100644 --- a/packages/presets/graphql-modules/src/index.ts +++ b/packages/presets/graphql-modules/src/index.ts @@ -74,8 +74,8 @@ export const preset: Types.OutputPreset = { const baseTypesFilename = baseTypesPath.replace( /\.(js|ts|d.ts)$/, - // we need extension if ESM modules are used - options.config.emitLegacyCommonJSImports ? '' : '.js' + // we need extension if ESM modules are used or if preserving TS extension + options.config.preserveTSExtension ? '.ts' : options.config.emitLegacyCommonJSImports ? '' : '.js' ); const baseTypesDir = stripFilename(baseOutput.filename); diff --git a/packages/utils/plugins-helpers/src/types.ts b/packages/utils/plugins-helpers/src/types.ts index 87bcedc15dd..3033dabe733 100644 --- a/packages/utils/plugins-helpers/src/types.ts +++ b/packages/utils/plugins-helpers/src/types.ts @@ -472,6 +472,10 @@ export namespace Types { * @description A flag to disable adding `.js` extension to the output file. Default: `true`. */ emitLegacyCommonJSImports?: boolean; + /** + * @description A flag to preserve `.ts` extension to the output file. Default: `false`. + */ + preserveTSExtension?: boolean; /** * @description A flag to suppress printing errors when they occur. */ diff --git a/website/public/config.schema.json b/website/public/config.schema.json index c105a545acc..1c7c2fe5a9c 100644 --- a/website/public/config.schema.json +++ b/website/public/config.schema.json @@ -59,6 +59,10 @@ "description": "A flag to disable adding `.js` extension to the output file. Default: `true`.", "type": "boolean" }, + "preserveTSExtension": { + "description": "A flag to preserve `.ts` extension to the output file. Default: `false`.", + "type": "boolean" + }, "silent": { "description": "A flag to suppress printing errors when they occur.", "type": "boolean" }, "verbose": { "description": "A flag to output more detailed information about tasks", "type": "boolean" }, "debug": { "description": "A flag to output debug logs", "type": "boolean" }, @@ -656,6 +660,10 @@ "description": "Emit legacy common js imports.\nDefault it will be `true` this way it ensure that generated code works with [non-compliant bundlers](https://github.com/dotansimha/graphql-code-generator/issues/8065).\nDefault value: \"true\"", "type": "boolean" }, + "preserveTSExtension": { + "description": "A flag to preserve `.ts` extension to the output file. Default: `false`.\nDefault value: \"false\"", + "type": "boolean" + }, "extractAllFieldsToTypes": { "description": "Extract all field types to their own types, instead of inlining them.\nThis helps to reduce type duplication, and makes type errors more readable.\nIt can also significantly reduce the size of the generated code, the generation time,\nand the typechecking time.\nDefault value: \"false\"", "type": "boolean" @@ -849,6 +857,10 @@ "description": "Emit legacy common js imports.\nDefault it will be `true` this way it ensure that generated code works with [non-compliant bundlers](https://github.com/dotansimha/graphql-code-generator/issues/8065).\nDefault value: \"true\"", "type": "boolean" }, + "preserveTSExtension": { + "description": "A flag to preserve `.ts` extension to the output file. Default: `false`.\nDefault value: \"false\"", + "type": "boolean" + }, "extractAllFieldsToTypes": { "description": "Extract all field types to their own types, instead of inlining them.\nThis helps to reduce type duplication, and makes type errors more readable.\nIt can also significantly reduce the size of the generated code, the generation time,\nand the typechecking time.\nDefault value: \"false\"", "type": "boolean" @@ -1626,6 +1638,10 @@ "type": "boolean", "description": "If true, recursively goes through all object type's fields, checks if they have abstract types and generates expected types correctly.\nThis may not work for cases where provided default mapper types are also nested e.g. `defaultMapper: DeepPartial<{T}>` or `defaultMapper: Partial<{T}>`.\nDefault value: \"false\"" }, + "addInterfaceFieldResolverTypes": { + "description": "If true, add field resolver types to Interfaces.\nBy default, GraphQL Interfaces do not trigger any field resolvers,\nmeaning every implementing type must implement the same resolver for the shared fields.\n\nSome tools provide a way to change the default behaviour by making GraphQL Objects inherit\nmissing resolvers from their Interface types. In these cases, it is fine to turn this option to true.\n\nFor example, if you are using `@graphql-tools/schema#makeExecutableSchema` with `inheritResolversFromInterfaces: true`,\nyou can make `addInterfaceFieldResolverTypes: true` as well\nhttps://the-guild.dev/graphql/tools/docs/generate-schema#makeexecutableschema\nDefault value: \"false\"", + "type": "boolean" + }, "strictScalars": { "description": "Makes scalars strict.\n\nIf scalars are found in the schema that are not defined in `scalars`\nan error will be thrown during codegen.\nDefault value: \"false\"", "type": "boolean" @@ -1664,6 +1680,10 @@ "description": "Emit legacy common js imports.\nDefault it will be `true` this way it ensure that generated code works with [non-compliant bundlers](https://github.com/dotansimha/graphql-code-generator/issues/8065).\nDefault value: \"true\"", "type": "boolean" }, + "preserveTSExtension": { + "description": "A flag to preserve `.ts` extension to the output file. Default: `false`.\nDefault value: \"false\"", + "type": "boolean" + }, "extractAllFieldsToTypes": { "description": "Extract all field types to their own types, instead of inlining them.\nThis helps to reduce type duplication, and makes type errors more readable.\nIt can also significantly reduce the size of the generated code, the generation time,\nand the typechecking time.\nDefault value: \"false\"", "type": "boolean" @@ -2789,6 +2809,10 @@ "description": "Emit legacy common js imports.\nDefault it will be `true` this way it ensure that generated code works with [non-compliant bundlers](https://github.com/dotansimha/graphql-code-generator/issues/8065).\nDefault value: \"true\"", "type": "boolean" }, + "preserveTSExtension": { + "description": "A flag to preserve `.ts` extension to the output file. Default: `false`.\nDefault value: \"false\"", + "type": "boolean" + }, "extractAllFieldsToTypes": { "description": "Extract all field types to their own types, instead of inlining them.\nThis helps to reduce type duplication, and makes type errors more readable.\nIt can also significantly reduce the size of the generated code, the generation time,\nand the typechecking time.\nDefault value: \"false\"", "type": "boolean"