Skip to content

Commit 9e70bcb

Browse files
feat: add importExtension configuration option (#10510)
* feat: add importExtension configuration option * feat: normalize import extension handling across plugins --------- Co-authored-by: Eddy Nguyen <ch@eddeee888.me>
1 parent b7f90b0 commit 9e70bcb

File tree

20 files changed

+646
-60
lines changed

20 files changed

+646
-60
lines changed

.changeset/sixty-sheep-tease.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@graphql-codegen/gql-tag-operations': minor
3+
'@graphql-codegen/visitor-plugin-common': minor
4+
'@graphql-codegen/graphql-modules-preset': minor
5+
'@graphql-codegen/plugin-helpers': minor
6+
'@graphql-codegen/cli': minor
7+
'@graphql-codegen/client-preset': minor
8+
---
9+
10+
add importExtension configuration option

examples/typescript-esm/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ yarn start
1313

1414
## Explanation
1515

16-
In ESM the file extension `.js` must be appended to named imports.
17-
This can be achieved by setting the codegen config `emitLegacyCommonJSImports` to `false` (see `codegen.yml`).
16+
In ESM the file extension must be appended to named imports.
17+
This can be achieved by setting the codegen config `importExtension` to `'.js'` or `'.ts'` (see `codegen.yml`).
1818

1919
TypeScript introduced a new module resolution algorithm for ESM in version 4.7. We set the `moduleResolution` to `node16` and the (output) module type to `node16` (see `tsconfig.json`).
2020
Additionally, within the `package.json` we specify the `type` property with the value `module` in order to instruct Node.js, bundlers and other tools that all `.js` files within this folder should be treated as ESM modules.

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import {
77
CodegenPlugin,
88
getCachedDocumentNodeFromSchema,
99
normalizeConfig,
10+
normalizeImportExtension,
1011
normalizeInstanceOrArray,
1112
normalizeOutputParam,
1213
Types,
1314
} from '@graphql-codegen/plugin-helpers';
1415
import { NoTypeDefinitionsFound } from '@graphql-tools/load';
1516
import { DocumentNode, GraphQLError, GraphQLSchema } from 'graphql';
1617
import { Listr, ListrTask } from 'listr2';
17-
import { CodegenContext, ensureContext, shouldEmitLegacyCommonJSImports } from './config.js';
18+
import { CodegenContext, ensureContext } from './config.js';
1819
import { getPluginByName } from './plugins.js';
1920
import { getPresetByName } from './presets.js';
2021
import { debugLog, printLogs } from './utils/debugging.js';
@@ -321,12 +322,18 @@ export async function executeCodegen(
321322
})
322323
);
323324

325+
const importExtension = normalizeImportExtension({
326+
emitLegacyCommonJSImports: config.emitLegacyCommonJSImports,
327+
importExtension: config.importExtension,
328+
});
329+
324330
const mergedConfig = {
325331
...rootConfig,
326332
...(typeof outputFileTemplateConfig === 'string'
327333
? { value: outputFileTemplateConfig }
328334
: outputFileTemplateConfig),
329-
emitLegacyCommonJSImports: shouldEmitLegacyCommonJSImports(config),
335+
importExtension,
336+
emitLegacyCommonJSImports: config.emitLegacyCommonJSImports ?? true,
330337
};
331338

332339
const documentTransforms = Array.isArray(outputConfig.documentTransforms)
@@ -377,8 +384,8 @@ export async function executeCodegen(
377384
const process = async (outputArgs: Types.GenerateOptions) => {
378385
const output = await codegen({
379386
...outputArgs,
380-
// @ts-expect-error todo: fix 'emitLegacyCommonJSImports' does not exist in type 'GenerateOptions'
381-
emitLegacyCommonJSImports: shouldEmitLegacyCommonJSImports(config, outputArgs.filename),
387+
importExtension,
388+
emitLegacyCommonJSImports: config.emitLegacyCommonJSImports ?? true,
382389
cache,
383390
});
384391
result.push({

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

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export type YamlCliFlags = {
3838
debug?: boolean;
3939
ignoreNoDocuments?: boolean;
4040
emitLegacyCommonJSImports?: boolean;
41+
importExtension?: '' | `.${string}`;
4142
};
4243

4344
export function generateSearchPlaces(moduleName: string) {
@@ -255,6 +256,18 @@ export function buildOptions() {
255256
type: 'boolean' as const,
256257
default: false,
257258
},
259+
'emit-legacy-common-js-imports': {
260+
describe: 'Emit legacy CommonJS imports (deprecated, use import-extension instead)',
261+
type: 'boolean' as const,
262+
},
263+
'import-extension': {
264+
describe: 'Extension to append to imports (e.g., .js, .mjs, or empty string for no extension)',
265+
type: 'string' as const,
266+
},
267+
'ignore-no-documents': {
268+
describe: 'Suppress errors for no documents',
269+
type: 'boolean' as const,
270+
},
258271
};
259272
}
260273

@@ -322,6 +335,10 @@ export function updateContextWithCliFlags(context: CodegenContext, cliFlags: Yam
322335
config.emitLegacyCommonJSImports = cliFlags['emit-legacy-common-js-imports'] === true;
323336
}
324337

338+
if (cliFlags['import-extension'] !== undefined) {
339+
config.importExtension = cliFlags['import-extension'];
340+
}
341+
325342
if (cliFlags.project) {
326343
context.useProject(cliFlags.project);
327344
}
@@ -488,19 +505,3 @@ function addHashToDocumentFiles(documentFilesPromise: Promise<Types.DocumentFile
488505
})
489506
);
490507
}
491-
492-
export function shouldEmitLegacyCommonJSImports(config: Types.Config): boolean {
493-
const globalValue = config.emitLegacyCommonJSImports === undefined ? true : !!config.emitLegacyCommonJSImports;
494-
// const outputConfig = config.generates[outputPath];
495-
496-
// if (!outputConfig) {
497-
// debugLog(`Couldn't find a config of ${outputPath}`);
498-
// return globalValue;
499-
// }
500-
501-
// if (isConfiguredOutput(outputConfig) && typeof outputConfig.emitLegacyCommonJSImports === 'boolean') {
502-
// return outputConfig.emitLegacyCommonJSImports;
503-
// }
504-
505-
return globalValue;
506-
}

packages/graphql-codegen-cli/tests/cli-flags.spec.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,73 @@ describe('CLI Flags', () => {
163163
expect(config.emitLegacyCommonJSImports).toBeFalsy();
164164
});
165165

166+
it('Should set importExtension config using cli flags to .js', async () => {
167+
mockConfig(`
168+
schema: schema.graphql
169+
generates:
170+
file.ts:
171+
- plugin
172+
`);
173+
const args = createArgv('--import-extension .js');
174+
const context = await createContext(parseArgv(args));
175+
const config = context.getConfig();
176+
expect(config.importExtension).toBe('.js');
177+
});
178+
179+
it('Should set importExtension config using cli flags to .mjs', async () => {
180+
mockConfig(`
181+
schema: schema.graphql
182+
generates:
183+
file.ts:
184+
- plugin
185+
`);
186+
const args = createArgv('--import-extension .mjs');
187+
const context = await createContext(parseArgv(args));
188+
const config = context.getConfig();
189+
expect(config.importExtension).toBe('.mjs');
190+
});
191+
192+
it('Should set importExtension config using cli flags to empty string', async () => {
193+
mockConfig(`
194+
schema: schema.graphql
195+
generates:
196+
file.ts:
197+
- plugin
198+
`);
199+
const args = createArgv('--import-extension ""');
200+
const context = await createContext(parseArgv(args));
201+
const config = context.getConfig();
202+
expect(config.importExtension).toBe('');
203+
});
204+
205+
it('Should overwrite importExtension from config using cli flags', async () => {
206+
mockConfig(`
207+
schema: schema.graphql
208+
importExtension: .js
209+
generates:
210+
file.ts:
211+
- plugin
212+
`);
213+
const args = createArgv('--import-extension .mjs');
214+
const context = await createContext(parseArgv(args));
215+
const config = context.getConfig();
216+
expect(config.importExtension).toBe('.mjs');
217+
});
218+
219+
it('Should overwrite importExtension config using cli flags to empty string', async () => {
220+
mockConfig(`
221+
schema: schema.graphql
222+
importExtension: .js
223+
generates:
224+
file.ts:
225+
- plugin
226+
`);
227+
const args = createArgv('--import-extension ""');
228+
const context = await createContext(parseArgv(args));
229+
const config = context.getConfig();
230+
expect(config.importExtension).toBe('');
231+
});
232+
166233
it('Should overwrite ignoreNoDocuments config using cli flags to true', async () => {
167234
mockConfig(`
168235
schema: schema.graphql

packages/plugins/other/visitor-plugin-common/src/base-visitor.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
ScalarsMap,
1414
} from './types.js';
1515
import { DeclarationBlockConfig } from './utils.js';
16+
import { normalizeImportExtension } from '@graphql-codegen/plugin-helpers';
1617

1718
export interface BaseVisitorConvertOptions {
1819
useTypesPrefix?: boolean;
@@ -35,7 +36,8 @@ export interface ParsedConfig {
3536
useTypeImports: boolean;
3637
allowEnumStringTypes: boolean;
3738
inlineFragmentTypes: InlineFragmentTypeOptions;
38-
emitLegacyCommonJSImports: boolean;
39+
emitLegacyCommonJSImports?: boolean;
40+
importExtension: '' | `.${string}`;
3941
printFieldsOnNewLines: boolean;
4042
includeExternalFragments: boolean;
4143
}
@@ -360,11 +362,17 @@ export interface RawConfig {
360362
*/
361363
inlineFragmentTypes?: InlineFragmentTypeOptions;
362364
/**
365+
* @deprecated Please use `importExtension` instead.
363366
* @default true
364367
* @description Emit legacy common js imports.
365368
* 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).
366369
*/
367370
emitLegacyCommonJSImports?: boolean;
371+
/**
372+
* @description Append this extension to all imports.
373+
* Useful for ESM environments that require file extensions in import statements.
374+
*/
375+
importExtension?: '' | `.${string}`;
368376

369377
/**
370378
* @default false
@@ -397,6 +405,10 @@ export class BaseVisitor<TRawConfig extends RawConfig = RawConfig, TPluginConfig
397405
public readonly scalars: NormalizedScalarsMap;
398406

399407
constructor(rawConfig: TRawConfig, additionalConfig: Partial<TPluginConfig>) {
408+
const importExtension = normalizeImportExtension({
409+
emitLegacyCommonJSImports: rawConfig.emitLegacyCommonJSImports,
410+
importExtension: rawConfig.importExtension,
411+
});
400412
this._parsedConfig = {
401413
convert: convertFactory(rawConfig),
402414
typesPrefix: rawConfig.typesPrefix || '',
@@ -408,8 +420,8 @@ export class BaseVisitor<TRawConfig extends RawConfig = RawConfig, TPluginConfig
408420
useTypeImports: !!rawConfig.useTypeImports,
409421
allowEnumStringTypes: !!rawConfig.allowEnumStringTypes,
410422
inlineFragmentTypes: rawConfig.inlineFragmentTypes ?? 'inline',
411-
emitLegacyCommonJSImports:
412-
rawConfig.emitLegacyCommonJSImports === undefined ? true : !!rawConfig.emitLegacyCommonJSImports,
423+
emitLegacyCommonJSImports: rawConfig.emitLegacyCommonJSImports ?? true,
424+
importExtension,
413425
extractAllFieldsToTypes: rawConfig.extractAllFieldsToTypes ?? false,
414426
printFieldsOnNewLines: rawConfig.printFieldsOnNewLines ?? false,
415427
includeExternalFragments: rawConfig.includeExternalFragments ?? false,

packages/plugins/other/visitor-plugin-common/src/client-side-base-visitor.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { basename, extname } from 'path';
2-
import { oldVisit, Types } from '@graphql-codegen/plugin-helpers';
2+
import { normalizeImportExtension, oldVisit, Types } from '@graphql-codegen/plugin-helpers';
33
import { optimizeDocumentNode } from '@graphql-tools/optimize';
44
import autoBind from 'auto-bind';
55
import { pascalCase } from 'change-case-all';
@@ -600,7 +600,12 @@ export class ClientSideBaseVisitor<
600600
private clearExtension(path: string): string {
601601
const extension = extname(path);
602602

603-
if (!this.config.emitLegacyCommonJSImports && extension === '.js') {
603+
const importExtension = normalizeImportExtension({
604+
emitLegacyCommonJSImports: this.config.emitLegacyCommonJSImports,
605+
importExtension: this.config.importExtension,
606+
});
607+
608+
if (extension === importExtension) {
604609
return path;
605610
}
606611

@@ -642,9 +647,10 @@ export class ClientSideBaseVisitor<
642647
if (this._collectedOperations.length > 0) {
643648
if (this.config.importDocumentNodeExternallyFrom === 'near-operation-file' && this._documents.length === 1) {
644649
let documentPath = `./${this.clearExtension(basename(this._documents[0].location))}`;
645-
if (!this.config.emitLegacyCommonJSImports) {
646-
documentPath += '.js';
647-
}
650+
documentPath += normalizeImportExtension({
651+
emitLegacyCommonJSImports: this.config.emitLegacyCommonJSImports,
652+
importExtension: this.config.importExtension,
653+
});
648654

649655
this._imports.add(`import * as Operations from '${documentPath}';`);
650656
} else {
@@ -668,6 +674,10 @@ export class ClientSideBaseVisitor<
668674
options.excludeFragments || this.config.globalNamespace || this.config.documentMode !== DocumentMode.graphQLTag;
669675

670676
if (!excludeFragments) {
677+
const importExtension = normalizeImportExtension({
678+
emitLegacyCommonJSImports: this.config.emitLegacyCommonJSImports,
679+
importExtension: this.config.importExtension,
680+
});
671681
const deduplicatedImports = Object.values(groupBy(this.config.fragmentImports, fi => fi.importSource.path))
672682
.map(
673683
(fragmentImports): ImportDeclaration<FragmentImport> => ({
@@ -680,6 +690,7 @@ export class ClientSideBaseVisitor<
680690
),
681691
},
682692
emitLegacyCommonJSImports: this.config.emitLegacyCommonJSImports,
693+
importExtension,
683694
})
684695
)
685696
.filter(fragmentImport => fragmentImport.outputPath !== fragmentImport.importSource.path);

packages/plugins/other/visitor-plugin-common/src/imports.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { dirname, isAbsolute, join, relative, resolve } from 'path';
22
import parse from 'parse-filepath';
3+
import { normalizeImportExtension } from '@graphql-codegen/plugin-helpers';
34

45
export type ImportDeclaration<T = string> = {
56
outputPath: string;
67
importSource: ImportSource<T>;
78
baseOutputDir: string;
89
baseDir: string;
910
typesImport: boolean;
10-
emitLegacyCommonJSImports: boolean;
11+
emitLegacyCommonJSImports?: boolean;
12+
importExtension: '' | `.${string}`;
1113
};
1214

1315
export type ImportSource<T = string> = {
@@ -57,7 +59,12 @@ export function generateImportStatement(statement: ImportDeclaration): string {
5759
? `{ ${Array.from(new Set(importSource.identifiers)).join(', ')} }`
5860
: '*';
5961
const importExtension =
60-
importPath.startsWith('/') || importPath.startsWith('.') ? (statement.emitLegacyCommonJSImports ? '' : '.js') : '';
62+
importPath.startsWith('/') || importPath.startsWith('.')
63+
? normalizeImportExtension({
64+
emitLegacyCommonJSImports: statement.emitLegacyCommonJSImports,
65+
importExtension: statement.importExtension,
66+
})
67+
: '';
6168
const importAlias = importSource.namespace ? ` as ${importSource.namespace}` : '';
6269
const importStatement = typesImport ? 'import type' : 'import';
6370
return `${importStatement} ${importNames}${importAlias} from '${importPath}${importExtension}';${

0 commit comments

Comments
 (0)