Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/sixty-sheep-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@graphql-codegen/gql-tag-operations': minor
'@graphql-codegen/visitor-plugin-common': minor
'@graphql-codegen/graphql-modules-preset': minor
'@graphql-codegen/plugin-helpers': minor
'@graphql-codegen/cli': minor
'@graphql-codegen/client-preset': minor
---

add importExtension configuration option
4 changes: 2 additions & 2 deletions examples/typescript-esm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ yarn start

## Explanation

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

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`).
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.
Expand Down
15 changes: 11 additions & 4 deletions packages/graphql-codegen-cli/src/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import {
CodegenPlugin,
getCachedDocumentNodeFromSchema,
normalizeConfig,
normalizeImportExtension,
normalizeInstanceOrArray,
normalizeOutputParam,
Types,
} from '@graphql-codegen/plugin-helpers';
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 } from './config.js';
import { getPluginByName } from './plugins.js';
import { getPresetByName } from './presets.js';
import { debugLog, printLogs } from './utils/debugging.js';
Expand Down Expand Up @@ -321,12 +322,18 @@ export async function executeCodegen(
})
);

const importExtension = normalizeImportExtension({
emitLegacyCommonJSImports: config.emitLegacyCommonJSImports,
importExtension: config.importExtension,
});

const mergedConfig = {
...rootConfig,
...(typeof outputFileTemplateConfig === 'string'
? { value: outputFileTemplateConfig }
: outputFileTemplateConfig),
emitLegacyCommonJSImports: shouldEmitLegacyCommonJSImports(config),
importExtension,
emitLegacyCommonJSImports: config.emitLegacyCommonJSImports ?? true,
};

const documentTransforms = Array.isArray(outputConfig.documentTransforms)
Expand Down Expand Up @@ -377,8 +384,8 @@ export async function executeCodegen(
const process = async (outputArgs: Types.GenerateOptions) => {
const output = await codegen({
...outputArgs,
// @ts-expect-error todo: fix 'emitLegacyCommonJSImports' does not exist in type 'GenerateOptions'
emitLegacyCommonJSImports: shouldEmitLegacyCommonJSImports(config, outputArgs.filename),
importExtension,
emitLegacyCommonJSImports: config.emitLegacyCommonJSImports ?? true,
cache,
});
result.push({
Expand Down
33 changes: 17 additions & 16 deletions packages/graphql-codegen-cli/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type YamlCliFlags = {
debug?: boolean;
ignoreNoDocuments?: boolean;
emitLegacyCommonJSImports?: boolean;
importExtension?: '' | `.${string}`;
};

export function generateSearchPlaces(moduleName: string) {
Expand Down Expand Up @@ -255,6 +256,18 @@ export function buildOptions() {
type: 'boolean' as const,
default: false,
},
'emit-legacy-common-js-imports': {
describe: 'Emit legacy CommonJS imports (deprecated, use import-extension instead)',
type: 'boolean' as const,
},
'import-extension': {
describe: 'Extension to append to imports (e.g., .js, .mjs, or empty string for no extension)',
type: 'string' as const,
},
'ignore-no-documents': {
describe: 'Suppress errors for no documents',
type: 'boolean' as const,
},
};
}

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

if (cliFlags['import-extension'] !== undefined) {
config.importExtension = cliFlags['import-extension'];
}

if (cliFlags.project) {
context.useProject(cliFlags.project);
}
Expand Down Expand Up @@ -488,19 +505,3 @@ function addHashToDocumentFiles(documentFilesPromise: Promise<Types.DocumentFile
})
);
}

export function shouldEmitLegacyCommonJSImports(config: Types.Config): boolean {
const globalValue = config.emitLegacyCommonJSImports === undefined ? true : !!config.emitLegacyCommonJSImports;
// const outputConfig = config.generates[outputPath];

// if (!outputConfig) {
// debugLog(`Couldn't find a config of ${outputPath}`);
// return globalValue;
// }

// if (isConfiguredOutput(outputConfig) && typeof outputConfig.emitLegacyCommonJSImports === 'boolean') {
// return outputConfig.emitLegacyCommonJSImports;
// }

return globalValue;
}
67 changes: 67 additions & 0 deletions packages/graphql-codegen-cli/tests/cli-flags.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,73 @@ describe('CLI Flags', () => {
expect(config.emitLegacyCommonJSImports).toBeFalsy();
});

it('Should set importExtension config using cli flags to .js', async () => {
mockConfig(`
schema: schema.graphql
generates:
file.ts:
- plugin
`);
const args = createArgv('--import-extension .js');
const context = await createContext(parseArgv(args));
const config = context.getConfig();
expect(config.importExtension).toBe('.js');
});

it('Should set importExtension config using cli flags to .mjs', async () => {
mockConfig(`
schema: schema.graphql
generates:
file.ts:
- plugin
`);
const args = createArgv('--import-extension .mjs');
const context = await createContext(parseArgv(args));
const config = context.getConfig();
expect(config.importExtension).toBe('.mjs');
});

it('Should set importExtension config using cli flags to empty string', async () => {
mockConfig(`
schema: schema.graphql
generates:
file.ts:
- plugin
`);
const args = createArgv('--import-extension ""');
const context = await createContext(parseArgv(args));
const config = context.getConfig();
expect(config.importExtension).toBe('');
});

it('Should overwrite importExtension from config using cli flags', async () => {
mockConfig(`
schema: schema.graphql
importExtension: .js
generates:
file.ts:
- plugin
`);
const args = createArgv('--import-extension .mjs');
const context = await createContext(parseArgv(args));
const config = context.getConfig();
expect(config.importExtension).toBe('.mjs');
});

it('Should overwrite importExtension config using cli flags to empty string', async () => {
mockConfig(`
schema: schema.graphql
importExtension: .js
generates:
file.ts:
- plugin
`);
const args = createArgv('--import-extension ""');
const context = await createContext(parseArgv(args));
const config = context.getConfig();
expect(config.importExtension).toBe('');
});

it('Should overwrite ignoreNoDocuments config using cli flags to true', async () => {
mockConfig(`
schema: schema.graphql
Expand Down
18 changes: 15 additions & 3 deletions packages/plugins/other/visitor-plugin-common/src/base-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ScalarsMap,
} from './types.js';
import { DeclarationBlockConfig } from './utils.js';
import { normalizeImportExtension } from '@graphql-codegen/plugin-helpers';

export interface BaseVisitorConvertOptions {
useTypesPrefix?: boolean;
Expand All @@ -35,7 +36,8 @@ export interface ParsedConfig {
useTypeImports: boolean;
allowEnumStringTypes: boolean;
inlineFragmentTypes: InlineFragmentTypeOptions;
emitLegacyCommonJSImports: boolean;
emitLegacyCommonJSImports?: boolean;
importExtension: '' | `.${string}`;
printFieldsOnNewLines: boolean;
includeExternalFragments: boolean;
}
Expand Down Expand Up @@ -360,11 +362,17 @@ export interface RawConfig {
*/
inlineFragmentTypes?: InlineFragmentTypeOptions;
/**
* @deprecated Please use `importExtension` instead.
* @default true
* @description Emit legacy common js imports.
* 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 Append this extension to all imports.
* Useful for ESM environments that require file extensions in import statements.
*/
importExtension?: '' | `.${string}`;

/**
* @default false
Expand Down Expand Up @@ -397,6 +405,10 @@ export class BaseVisitor<TRawConfig extends RawConfig = RawConfig, TPluginConfig
public readonly scalars: NormalizedScalarsMap;

constructor(rawConfig: TRawConfig, additionalConfig: Partial<TPluginConfig>) {
const importExtension = normalizeImportExtension({
emitLegacyCommonJSImports: rawConfig.emitLegacyCommonJSImports,
importExtension: rawConfig.importExtension,
});
this._parsedConfig = {
convert: convertFactory(rawConfig),
typesPrefix: rawConfig.typesPrefix || '',
Expand All @@ -408,8 +420,8 @@ export class BaseVisitor<TRawConfig extends RawConfig = RawConfig, TPluginConfig
useTypeImports: !!rawConfig.useTypeImports,
allowEnumStringTypes: !!rawConfig.allowEnumStringTypes,
inlineFragmentTypes: rawConfig.inlineFragmentTypes ?? 'inline',
emitLegacyCommonJSImports:
rawConfig.emitLegacyCommonJSImports === undefined ? true : !!rawConfig.emitLegacyCommonJSImports,
emitLegacyCommonJSImports: rawConfig.emitLegacyCommonJSImports ?? true,
importExtension,
extractAllFieldsToTypes: rawConfig.extractAllFieldsToTypes ?? false,
printFieldsOnNewLines: rawConfig.printFieldsOnNewLines ?? false,
includeExternalFragments: rawConfig.includeExternalFragments ?? false,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { basename, extname } from 'path';
import { oldVisit, Types } from '@graphql-codegen/plugin-helpers';
import { normalizeImportExtension, oldVisit, Types } from '@graphql-codegen/plugin-helpers';
import { optimizeDocumentNode } from '@graphql-tools/optimize';
import autoBind from 'auto-bind';
import { pascalCase } from 'change-case-all';
Expand Down Expand Up @@ -600,7 +600,12 @@ export class ClientSideBaseVisitor<
private clearExtension(path: string): string {
const extension = extname(path);

if (!this.config.emitLegacyCommonJSImports && extension === '.js') {
const importExtension = normalizeImportExtension({
emitLegacyCommonJSImports: this.config.emitLegacyCommonJSImports,
importExtension: this.config.importExtension,
});

if (extension === importExtension) {
return path;
}

Expand Down Expand Up @@ -642,9 +647,10 @@ export class ClientSideBaseVisitor<
if (this._collectedOperations.length > 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) {
documentPath += '.js';
}
documentPath += normalizeImportExtension({
emitLegacyCommonJSImports: this.config.emitLegacyCommonJSImports,
importExtension: this.config.importExtension,
});

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

if (!excludeFragments) {
const importExtension = normalizeImportExtension({
emitLegacyCommonJSImports: this.config.emitLegacyCommonJSImports,
importExtension: this.config.importExtension,
});
const deduplicatedImports = Object.values(groupBy(this.config.fragmentImports, fi => fi.importSource.path))
.map(
(fragmentImports): ImportDeclaration<FragmentImport> => ({
Expand All @@ -680,6 +690,7 @@ export class ClientSideBaseVisitor<
),
},
emitLegacyCommonJSImports: this.config.emitLegacyCommonJSImports,
importExtension,
})
)
.filter(fragmentImport => fragmentImport.outputPath !== fragmentImport.importSource.path);
Expand Down
11 changes: 9 additions & 2 deletions packages/plugins/other/visitor-plugin-common/src/imports.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { dirname, isAbsolute, join, relative, resolve } from 'path';
import parse from 'parse-filepath';
import { normalizeImportExtension } from '@graphql-codegen/plugin-helpers';

export type ImportDeclaration<T = string> = {
outputPath: string;
importSource: ImportSource<T>;
baseOutputDir: string;
baseDir: string;
typesImport: boolean;
emitLegacyCommonJSImports: boolean;
emitLegacyCommonJSImports?: boolean;
importExtension: '' | `.${string}`;
};

export type ImportSource<T = string> = {
Expand Down Expand Up @@ -57,7 +59,12 @@ export function generateImportStatement(statement: ImportDeclaration): string {
? `{ ${Array.from(new Set(importSource.identifiers)).join(', ')} }`
: '*';
const importExtension =
importPath.startsWith('/') || importPath.startsWith('.') ? (statement.emitLegacyCommonJSImports ? '' : '.js') : '';
importPath.startsWith('/') || importPath.startsWith('.')
? normalizeImportExtension({
emitLegacyCommonJSImports: statement.emitLegacyCommonJSImports,
importExtension: statement.importExtension,
})
: '';
const importAlias = importSource.namespace ? ` as ${importSource.namespace}` : '';
const importStatement = typesImport ? 'import type' : 'import';
return `${importStatement} ${importNames}${importAlias} from '${importPath}${importExtension}';${
Expand Down
Loading
Loading