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
16 changes: 16 additions & 0 deletions .changeset/strict-visitor-plugin-common-documents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@graphql-codegen/visitor-plugin-common': patch
---

Fix type errors in `visitor-plugin-common` (`selection-set-to-object`, `client-side-base-visitor` and `base-documents-visitor`) under `strict: true`:

- `BaseDocumentsVisitor._selectionSetToObject` is optional, since it is only set by `setSelectionSetHandler`. `FragmentDefinition` and `OperationDefinition` throw a descriptive error when it is not set, and `FragmentDefinition` throws one when the fragment's type condition is not in the schema.
- `BaseDocumentsVisitor.FragmentDefinition` may return `null`, which it already returns when `generateOperationTypes` is `false`.
- `ClientSideBaseVisitor.buildOperation` may return `null`, which its base implementation already returns.
- `SelectionSetToObject.transformSelectionSet` declares its return type, and throws a descriptive error when the instance has no selection set.
- `LinkField`'s `alias` is optional, matching the link fields built for fields without an alias.
- `BaseSelectionSetProcessor.typeCache` is keyed by `Location | undefined`, since selection sets parsed with `noLocation` have no `loc`.
- `getPossibleTypes` accepts any `GraphQLType` or `undefined`, which it already handles by unwrapping lists and non-nulls and returning no types, and returns a copy of the schema's possible types instead of the schema's own readonly array.
- `OperationVariablesToObject.transform` accepts `undefined`, which it already handles by returning `null`.

Generated output is unchanged.
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ export class BaseDocumentsVisitor<
> extends BaseVisitor<TRawConfig, TPluginConfig> {
protected _unnamedCounter = 1;
protected _variablesTransfomer: OperationVariablesToObject;
protected _selectionSetToObject: SelectionSetToObject;
protected _selectionSetToObject?: SelectionSetToObject;
protected _globalDeclarations: Set<string> = new Set<string>();

constructor(
Expand Down Expand Up @@ -469,12 +469,23 @@ export class BaseDocumentsVisitor<
});
}

FragmentDefinition(node: FragmentDefinitionNode): string {
FragmentDefinition(node: FragmentDefinitionNode): string | null {
if (!this.config.generateOperationTypes) {
return null;
}

if (!this._selectionSetToObject) {
throw new Error('Unable to visit fragments: call "setSelectionSetHandler" first');
}

const fragmentRootType = this._schema.getType(node.typeCondition.name.value);

if (!fragmentRootType) {
throw new Error(
`Unable to find schema type "${node.typeCondition.name.value}" for fragment "${node.name.value}"!`,
);
}

const selectionSet = this._selectionSetToObject.createNext(fragmentRootType, node.selectionSet);
const fragmentSuffix = this.getFragmentSuffix(node);
return [
Expand Down Expand Up @@ -518,6 +529,10 @@ export class BaseDocumentsVisitor<
throw new Error(`Unable to find root schema type for operation type "${node.operation}"!`);
}

if (!this._selectionSetToObject) {
throw new Error('Unable to visit operations: call "setSelectionSetHandler" first');
}

const selectionSet = this._selectionSetToObject.createNext(
operationRootType,
node.selectionSet,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export class ClientSideBaseVisitor<
fragments: LoadedFragment[],
rawConfig: TRawConfig,
additionalConfig: Partial<TPluginConfig>,
documents?: Types.DocumentFile[],
documents: Types.DocumentFile[] = [],
) {
super(rawConfig, {
scalars: buildScalarsFromConfig(_schema, rawConfig),
Expand Down Expand Up @@ -464,10 +464,10 @@ export class ClientSideBaseVisitor<
): Record<string, any> | void | undefined {
let meta: Record<string, any> | void | undefined;

if (node.kind === Kind.OPERATION_DEFINITION) {
if (node.kind === Kind.OPERATION_DEFINITION && this._onExecutableDocumentNode) {
meta = this._onExecutableDocumentNode({
kind: Kind.DOCUMENT,
definitions,
definitions: definitions || [],

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: definitions is an optional param of this protected method, so a subclass that doesn't pass it gets an empty document here instead of one with definitions: undefined.


Generated by Claude Code


Generated by Claude Code

});
}

Expand All @@ -487,8 +487,8 @@ export class ClientSideBaseVisitor<
} {
const deferredFields: { [fargmentName: string]: string[] } = {};
const queue: SelectionNode[] = [...node.selectionSet.selections];
while (queue.length) {
const selection = queue.shift();
// `for...of` also visits the selections pushed to `queue` while iterating
for (const selection of queue) {
if (
selection.kind === Kind.FRAGMENT_SPREAD &&
selection.directives?.some((d: DirectiveNode) => d.name.value === 'defer')
Expand Down Expand Up @@ -675,7 +675,7 @@ export class ClientSideBaseVisitor<
this.config.importDocumentNodeExternallyFrom === 'near-operation-file' &&
this._documents.length === 1
) {
let documentPath = `./${this.clearExtension(basename(this._documents[0].location))}`;
let documentPath = `./${this.clearExtension(basename(this._documents[0].location || ''))}`;
documentPath += normalizeImportExtension({
emitLegacyCommonJSImports: this.config.emitLegacyCommonJSImports,
importExtension: this.config.importExtension,
Expand All @@ -692,7 +692,7 @@ export class ClientSideBaseVisitor<

this._imports.add(
`import * as Operations from '${this.clearExtension(
this.config.importDocumentNodeExternallyFrom,
this.config.importDocumentNodeExternallyFrom || '',

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This branch is reached right after the warning above when importDocumentNodeExternallyFrom isn't set. With || '', codegen carries on past the warning and emits from '', where before extname(undefined) threw. That is a small runtime change. Same for location || '' at L678, for a document with no location.


Generated by Claude Code


Generated by Claude Code

)}';`,
);
}
Expand Down Expand Up @@ -722,7 +722,7 @@ export class ClientSideBaseVisitor<
importSource: {
...fragmentImports[0].importSource,
identifiers: unique(
flatten(fragmentImports.map(fi => fi.importSource.identifiers)),
flatten(fragmentImports.map(fi => fi.importSource.identifiers || [])),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: ImportSource.identifiers is optional, so a fragment import without identifiers adds none to the merged import instead of making flatten fail on undefined.


Generated by Claude Code


Generated by Claude Code

identifier => identifier.name,
),
},
Expand All @@ -747,7 +747,7 @@ export class ClientSideBaseVisitor<
_operationResultType: string,
_operationVariablesTypes: string,
_hasRequiredVariables: boolean,
): string {
): string | null {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type PrimitiveAliasedFields = {
fieldName: string;
};
export type LinkField = {
alias: string;
alias?: string;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this optional now?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Link fields have always been built with alias: undefined when the field has no alias (selection-set-to-object.ts L972-977: `alias: field.alias ? formatNamedField(...) : undefined`). Without `strictNullChecks` that still matched `alias: string`. With it turned on, that object fails TS2322. The processors that read `LinkField` already treat the alias as optional, using `field.alias || field.name` ([`pre-resolve-types.ts` L125](https://github.com/dotansimha/graphql-code-generator/blob/37e83a27ce324092c7e2beb909dccd1fc2123910/packages/plugins/other/visitor-plugin-common/src/selection-set-processor/pre-resolve-types.ts#L125)`` and ts-selection-set-processor.ts L85),`` so alias?: string makes the type match what the code already does.


Generated by Claude Code


Generated by Claude Code

name: string;
type: string;
selectionSet: string;
Expand All @@ -34,7 +34,8 @@ export type SelectionSetProcessorConfig = {
};

export class BaseSelectionSetProcessor<Config extends SelectionSetProcessorConfig> {
typeCache = new Map<Location, Map<string, [string, string]>>();
// Keyed by the selection set's `loc`, which is `undefined` for documents parsed with `noLocation`
typeCache = new Map<Location | undefined, Map<string, [string, string]>>();

constructor(public config: Config) {}

Expand Down
Loading
Loading