From ec702693eae07568610477b80a4e01ae62ee539b Mon Sep 17 00:00:00 2001 From: jdecroock Date: Fri, 22 Mar 2024 15:08:37 +0100 Subject: [PATCH 1/9] Add support for fragment arguments --- .../__snapshots__/parser.test.ts.snap | 5 +- src/__tests__/parser.test.ts | 84 ++++++++++++++++++ src/__tests__/printer.test.ts | 88 +++++++++++++++++++ src/ast.ts | 2 + src/kind.d.ts | 1 + src/parser.ts | 25 ++++-- src/printer.ts | 32 ++++--- 7 files changed, 216 insertions(+), 21 deletions(-) diff --git a/src/__tests__/__snapshots__/parser.test.ts.snap b/src/__tests__/__snapshots__/parser.test.ts.snap index 1fe3a73..5267c66 100644 --- a/src/__tests__/__snapshots__/parser.test.ts.snap +++ b/src/__tests__/__snapshots__/parser.test.ts.snap @@ -185,6 +185,7 @@ exports[`parse > parses the kitchen sink document like graphql.js does 1`] = ` "selectionSet": undefined, }, { + "arguments": undefined, "directives": [ { "arguments": undefined, @@ -644,7 +645,8 @@ exports[`parse > parses the kitchen sink document like graphql.js does 1`] = ` "value": { "block": true, "kind": "StringValue", - "value": "block string uses """", + "value": "block string uses """ +", }, }, ], @@ -669,6 +671,7 @@ exports[`parse > parses the kitchen sink document like graphql.js does 1`] = ` "value": "Friend", }, }, + "variableDefinitions": undefined, }, { "directives": undefined, diff --git a/src/__tests__/parser.test.ts b/src/__tests__/parser.test.ts index 4b690c1..b8eae24 100644 --- a/src/__tests__/parser.test.ts +++ b/src/__tests__/parser.test.ts @@ -174,6 +174,90 @@ describe('parse', () => { expect(() => parse('fragment Name on Type { field }')).not.toThrow(); }); + it('parses fragment variable definitions', () => { + expect(parse('fragment x($var: Int = 1) on Type { field }').definitions[0]).toEqual({ + kind: Kind.FRAGMENT_DEFINITION, + directives: undefined, + name: { + kind: Kind.NAME, + value: 'x', + }, + typeCondition: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'Type', + }, + }, + variableDefinitions: [ + { + kind: Kind.VARIABLE_DEFINITION, + type: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'Int', + }, + }, + variable: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'var', + }, + }, + defaultValue: { + kind: Kind.INT, + value: '1', + }, + directives: undefined, + }, + ], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + alias: undefined, + kind: Kind.FIELD, + directives: undefined, + selectionSet: undefined, + arguments: undefined, + name: { + kind: Kind.NAME, + value: 'field', + }, + }, + ], + }, + }); + }); + + it('parses fragment spread arguments', () => { + expect( + parse('query x { ...x(var: 2) } fragment x($var: Int = 1) on Type { field }').definitions[0] + ).toHaveProperty('selectionSet.selections.0', { + kind: Kind.FRAGMENT_SPREAD, + directives: undefined, + name: { + kind: Kind.NAME, + value: 'x', + }, + arguments: [ + { + kind: 'Argument', + name: { + kind: 'Name', + value: 'var', + }, + value: { + kind: 'IntValue', + value: '2', + }, + }, + ], + }); + }); + it('parses fields', () => { expect(() => parse('{ field: }')).toThrow(); expect(() => parse('{ alias: field() }')).toThrow(); diff --git a/src/__tests__/printer.test.ts b/src/__tests__/printer.test.ts index 4ad014d..d6d85e4 100644 --- a/src/__tests__/printer.test.ts +++ b/src/__tests__/printer.test.ts @@ -117,6 +117,94 @@ describe('print', () => { ).toBe('[Type!]'); }); + it('prints fragment-definition with variables', () => { + expect( + print({ + kind: Kind.FRAGMENT_DEFINITION, + directives: [], + name: { + kind: Kind.NAME, + value: 'x', + }, + typeCondition: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'Type', + }, + }, + variableDefinitions: [ + { + kind: Kind.VARIABLE_DEFINITION, + type: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'Int', + }, + }, + variable: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'var', + }, + }, + defaultValue: { + kind: Kind.INT, + value: '1', + }, + directives: [], + }, + ], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + alias: undefined, + kind: Kind.FIELD, + directives: [], + selectionSet: undefined, + arguments: [], + name: { + kind: Kind.NAME, + value: 'field', + }, + }, + ], + }, + } as any) + ).toBe(`fragment x($var: Int = 1) on Type { + field +}`); + }); + + it('prints fragment-spread with arguments', () => { + expect( + print({ + kind: Kind.FRAGMENT_SPREAD, + directives: [], + name: { + kind: Kind.NAME, + value: 'x', + }, + arguments: [ + { + kind: 'Argument', + name: { + kind: 'Name', + value: 'var', + }, + value: { + kind: 'IntValue', + value: '2', + }, + }, + ], + } as any) + ).toBe(`...x(var: 2)`); + }); + // NOTE: The shim won't throw for invalid AST nodes it('returns empty strings for invalid AST', () => { const badAST = { random: 'Data' }; diff --git a/src/ast.ts b/src/ast.ts index c94024d..9f823e5 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -190,6 +190,7 @@ export type FragmentSpreadNode = Or< { readonly kind: Kind.FRAGMENT_SPREAD; readonly name: NameNode; + readonly arguments?: ReadonlyArray; readonly directives?: ReadonlyArray; readonly loc?: Location; } @@ -212,6 +213,7 @@ export type FragmentDefinitionNode = Or< readonly kind: Kind.FRAGMENT_DEFINITION; readonly name: NameNode; readonly description?: StringValueNode; + readonly variableDefinitions?: ReadonlyArray; readonly typeCondition: NamedTypeNode; readonly directives?: ReadonlyArray; readonly selectionSet: SelectionSetNode; diff --git a/src/kind.d.ts b/src/kind.d.ts index ae9fd06..aa12cf7 100644 --- a/src/kind.d.ts +++ b/src/kind.d.ts @@ -8,6 +8,7 @@ export declare enum Kind { SELECTION_SET = 'SelectionSet', FIELD = 'Field', ARGUMENT = 'Argument', + FRAGMENT_ARGUMENT = 'FragmentArgument', /** Fragments */ FRAGMENT_SPREAD = 'FragmentSpread', INLINE_FRAGMENT = 'InlineFragment', diff --git a/src/parser.ts b/src/parser.ts index afdc0de..9c95dfb 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -25,6 +25,7 @@ function advance(pattern: RegExp) { } const leadingRe = / +(?=[^\s])/y; +const nameRe = /[A-Za-z_][0-9A-Za-z_]*/y; function blockString(string: string) { const lines = string.split('\n'); let out = ''; @@ -357,6 +358,7 @@ function selectionSet(): ast.SelectionSetNode { selections.push({ kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD, name: nameNode(), + arguments: arguments_(false), directives: directives(false), }); } @@ -377,6 +379,7 @@ function selectionSet(): ast.SelectionSetNode { selections.push({ kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD, name: nameNode(), + arguments: arguments_(false), directives: directives(false), }); } @@ -460,19 +463,27 @@ function variableDefinitions(): ast.VariableDefinitionNode[] | undefined { } function fragmentDefinition(description?: ast.StringValueNode): ast.FragmentDefinitionNode { - const name = nameNode(); - if (input.charCodeAt(idx++) !== 111 /*'o'*/ || input.charCodeAt(idx++) !== 110 /*'n'*/) - throw error('FragmentDefinition'); + let _name: string | undefined; + let _condition: string | undefined; + if ((_name = advance(nameRe)) == null) throw error('FragmentDefinition'); + const _variableDefinitions = variableDefinitions(); + if (advance(nameRe) !== 'on') throw error('FragmentDefinition'); + ignored(); + if ((_condition = advance(nameRe)) == null) throw error('FragmentDefinition'); + ignored(); + const _directives = directives(false); + if (input.charCodeAt(idx++) !== 123 /*'{'*/) throw error('FragmentDefinition'); ignored(); const fragDef: ast.FragmentDefinitionNode = { kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION, - name, + name: { kind: 'Name' as Kind.NAME, value: _name }, typeCondition: { kind: 'NamedType' as Kind.NAMED_TYPE, - name: nameNode(), + name: { kind: 'Name' as Kind.NAME, value: _condition }, }, - directives: directives(false), - selectionSet: selectionSetStart(), + variableDefinitions: _variableDefinitions, + directives: _directives, + selectionSet: selectionSet(), }; if (description) { fragDef.description = description; diff --git a/src/printer.ts b/src/printer.ts index 9643600..62d7d91 100644 --- a/src/printer.ts +++ b/src/printer.ts @@ -47,6 +47,19 @@ const MAX_LINE_LENGTH = 80; let LF = '\n'; +function Arguments(length: number, node: readonly ArgumentNode[]): string { + const args = mapJoin(node, ', ', nodes.Argument); + if (length + args.length + 2 > MAX_LINE_LENGTH) { + return '(' + + (LF += ' ') + + mapJoin(node, LF, nodes.Argument) + + (LF = LF.slice(0, -2)) + + ')'; + } else { + return '(' + args + ')'; + } +} + const nodes = { OperationDefinition(node: OperationDefinitionNode): string { let out: string = ''; @@ -77,19 +90,8 @@ const nodes = { }, Field(node: FieldNode): string { let out = node.alias ? node.alias.value + ': ' + node.name.value : node.name.value; - if (node.arguments && node.arguments.length) { - const args = mapJoin(node.arguments, ', ', nodes.Argument); - if (out.length + args.length + 2 > MAX_LINE_LENGTH) { - out += - '(' + - (LF += ' ') + - mapJoin(node.arguments, LF, nodes.Argument) + - (LF = LF.slice(0, -2)) + - ')'; - } else { - out += '(' + args + ')'; - } - } + if (node.arguments && node.arguments.length) + out += Arguments(out.length, node.arguments); if (node.directives && node.directives.length) out += ' ' + mapJoin(node.directives, ' ', nodes.Directive); if (node.selectionSet && node.selectionSet.selections.length) { @@ -146,6 +148,8 @@ const nodes = { }, FragmentSpread(node: FragmentSpreadNode): string { let out = '...' + node.name.value; + if (node.arguments && node.arguments.length) + out += Arguments(out.length, node.arguments); if (node.directives && node.directives.length) out += ' ' + mapJoin(node.directives, ' ', nodes.Directive); return out; @@ -164,6 +168,8 @@ const nodes = { out += nodes.StringValue(node.description) + '\n'; } out += 'fragment ' + node.name.value; + if (node.variableDefinitions && node.variableDefinitions.length) + out += '(' + mapJoin(node.variableDefinitions, ', ', nodes.VariableDefinition) + ')'; out += ' on ' + node.typeCondition.name.value; if (node.directives && node.directives.length) out += ' ' + mapJoin(node.directives, ' ', nodes.Directive); From 6a6e2ae2d2be004bfe448075837625ee5714dcd3 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Fri, 22 Mar 2024 15:14:40 +0000 Subject: [PATCH 2/9] Update types in ast.ts --- src/ast.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ast.ts b/src/ast.ts index 9f823e5..1a3000a 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -190,11 +190,12 @@ export type FragmentSpreadNode = Or< { readonly kind: Kind.FRAGMENT_SPREAD; readonly name: NameNode; - readonly arguments?: ReadonlyArray; readonly directives?: ReadonlyArray; readonly loc?: Location; } ->; +> & { + readonly arguments?: ReadonlyArray; +}; export type InlineFragmentNode = Or< GraphQL.InlineFragmentNode, From 762c0f96912af42f358d8c5b6e10996c3789a6c9 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Tue, 2 Apr 2024 12:23:03 +0100 Subject: [PATCH 3/9] Add test for non-const variable argument on spreads --- src/__tests__/parser.test.ts | 57 +++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/src/__tests__/parser.test.ts b/src/__tests__/parser.test.ts index b8eae24..6c335ce 100644 --- a/src/__tests__/parser.test.ts +++ b/src/__tests__/parser.test.ts @@ -233,29 +233,44 @@ describe('parse', () => { }); it('parses fragment spread arguments', () => { - expect( - parse('query x { ...x(var: 2) } fragment x($var: Int = 1) on Type { field }').definitions[0] - ).toHaveProperty('selectionSet.selections.0', { - kind: Kind.FRAGMENT_SPREAD, - directives: undefined, - name: { - kind: Kind.NAME, - value: 'x', - }, - arguments: [ - { - kind: 'Argument', - name: { - kind: 'Name', - value: 'var', + expect(parse('query x { ...x(varA: 2, varB: $var) }').definitions[0]).toHaveProperty( + 'selectionSet.selections.0', + { + kind: Kind.FRAGMENT_SPREAD, + directives: undefined, + name: { + kind: Kind.NAME, + value: 'x', + }, + arguments: [ + { + kind: 'Argument', + name: { + kind: 'Name', + value: 'varA', + }, + value: { + kind: 'IntValue', + value: '2', + }, }, - value: { - kind: 'IntValue', - value: '2', + { + kind: 'Argument', + name: { + kind: 'Name', + value: 'varB', + }, + value: { + kind: 'Variable', + name: { + kind: 'Name', + value: 'var', + }, + }, }, - }, - ], - }); + ], + } + ); }); it('parses fields', () => { From 08f43d4f2f4d07c0b2aac96df25312b209b4b899 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Tue, 2 Apr 2024 12:24:09 +0100 Subject: [PATCH 4/9] Add changeset --- .changeset/good-spiders-brush.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/good-spiders-brush.md diff --git a/.changeset/good-spiders-brush.md b/.changeset/good-spiders-brush.md new file mode 100644 index 0000000..412f3f2 --- /dev/null +++ b/.changeset/good-spiders-brush.md @@ -0,0 +1,5 @@ +--- +'@0no-co/graphql.web': minor +--- + +Add support for variable definitions on fragments and arguments on fragment spreads (Fragment Arguments Spec Addition) From b1279ee3ddedf7120ef8218e03497a966b4e6b2f Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Tue, 2 Apr 2024 12:30:37 +0100 Subject: [PATCH 5/9] Apply lints --- src/printer.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/printer.ts b/src/printer.ts index 62d7d91..4e3a174 100644 --- a/src/printer.ts +++ b/src/printer.ts @@ -50,11 +50,7 @@ let LF = '\n'; function Arguments(length: number, node: readonly ArgumentNode[]): string { const args = mapJoin(node, ', ', nodes.Argument); if (length + args.length + 2 > MAX_LINE_LENGTH) { - return '(' + - (LF += ' ') + - mapJoin(node, LF, nodes.Argument) + - (LF = LF.slice(0, -2)) + - ')'; + return '(' + (LF += ' ') + mapJoin(node, LF, nodes.Argument) + (LF = LF.slice(0, -2)) + ')'; } else { return '(' + args + ')'; } @@ -90,8 +86,7 @@ const nodes = { }, Field(node: FieldNode): string { let out = node.alias ? node.alias.value + ': ' + node.name.value : node.name.value; - if (node.arguments && node.arguments.length) - out += Arguments(out.length, node.arguments); + if (node.arguments && node.arguments.length) out += Arguments(out.length, node.arguments); if (node.directives && node.directives.length) out += ' ' + mapJoin(node.directives, ' ', nodes.Directive); if (node.selectionSet && node.selectionSet.selections.length) { @@ -148,8 +143,7 @@ const nodes = { }, FragmentSpread(node: FragmentSpreadNode): string { let out = '...' + node.name.value; - if (node.arguments && node.arguments.length) - out += Arguments(out.length, node.arguments); + if (node.arguments && node.arguments.length) out += Arguments(out.length, node.arguments); if (node.directives && node.directives.length) out += ' ' + mapJoin(node.directives, ' ', nodes.Directive); return out; From 197f1d379b29dd90f81483b53ee1ea665cb68a70 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Fri, 12 Sep 2025 10:44:21 +0200 Subject: [PATCH 6/9] Add FragmentArgumentKind --- src/__tests__/__snapshots__/parser.test.ts.snap | 3 +-- src/__tests__/parser.test.ts | 4 ++-- src/ast.ts | 10 +++++++++- src/parser.ts | 17 ++++++++++++----- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/__tests__/__snapshots__/parser.test.ts.snap b/src/__tests__/__snapshots__/parser.test.ts.snap index 5267c66..9c58d44 100644 --- a/src/__tests__/__snapshots__/parser.test.ts.snap +++ b/src/__tests__/__snapshots__/parser.test.ts.snap @@ -645,8 +645,7 @@ exports[`parse > parses the kitchen sink document like graphql.js does 1`] = ` "value": { "block": true, "kind": "StringValue", - "value": "block string uses """ -", + "value": "block string uses """", }, }, ], diff --git a/src/__tests__/parser.test.ts b/src/__tests__/parser.test.ts index 6c335ce..f618e56 100644 --- a/src/__tests__/parser.test.ts +++ b/src/__tests__/parser.test.ts @@ -244,7 +244,7 @@ describe('parse', () => { }, arguments: [ { - kind: 'Argument', + kind: 'FragmentArgument', name: { kind: 'Name', value: 'varA', @@ -255,7 +255,7 @@ describe('parse', () => { }, }, { - kind: 'Argument', + kind: 'FragmentArgument', name: { kind: 'Name', value: 'varB', diff --git a/src/ast.ts b/src/ast.ts index 1a3000a..602218d 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -38,6 +38,7 @@ export type ASTNode = Or< | FieldNode | ArgumentNode | FragmentSpreadNode + | FragmentArgumentNode | InlineFragmentNode | FragmentDefinitionNode | IntValueNode @@ -185,6 +186,13 @@ export type ConstArgumentNode = Or< } >; +export type FragmentArgumentNode = { + readonly kind: Kind.FRAGMENT_ARGUMENT; + readonly name: NameNode; + readonly value: ValueNode; + readonly loc?: Location; +}; + export type FragmentSpreadNode = Or< GraphQL.FragmentSpreadNode, { @@ -194,7 +202,7 @@ export type FragmentSpreadNode = Or< readonly loc?: Location; } > & { - readonly arguments?: ReadonlyArray; + readonly arguments?: ReadonlyArray; }; export type InlineFragmentNode = Or< diff --git a/src/parser.ts b/src/parser.ts index 9c95dfb..fd39243 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -246,9 +246,12 @@ function value(constant: boolean): ast.ValueNode { }; } -function arguments_(constant: boolean): ast.ArgumentNode[] | undefined { +function arguments_( + constant: boolean, + fragmentArgument?: boolean +): ast.ArgumentNode[] | ast.FragmentArgumentNode[] | undefined { if (input.charCodeAt(idx) === 40 /*'('*/) { - const args: ast.ArgumentNode[] = []; + const args: ast.ArgumentNode[] | ast.FragmentArgumentNode[] = []; idx++; ignored(); do { @@ -256,7 +259,9 @@ function arguments_(constant: boolean): ast.ArgumentNode[] | undefined { if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('Argument'); ignored(); args.push({ - kind: 'Argument' as Kind.ARGUMENT, + kind: fragmentArgument + ? ('FragmentArgument' as Kind.FRAGMENT_ARGUMENT) + : ('Argument' as Kind.ARGUMENT), name, value: value(constant), }); @@ -358,7 +363,8 @@ function selectionSet(): ast.SelectionSetNode { selections.push({ kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD, name: nameNode(), - arguments: arguments_(false), + // @ts-expect-error + arguments: arguments_(false, true) as readonly ast.FragmentArgumentNode[], directives: directives(false), }); } @@ -379,7 +385,8 @@ function selectionSet(): ast.SelectionSetNode { selections.push({ kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD, name: nameNode(), - arguments: arguments_(false), + // @ts-expect-error + arguments: arguments_(false, true) as readonly ast.FragmentArgumentNode[], directives: directives(false), }); } From 222cbc135ef2cfdf825a24f8e39e68d7f3e66fa9 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Fri, 12 Sep 2025 10:06:52 +0100 Subject: [PATCH 7/9] Fix type issues --- src/ast.ts | 107 +++++++++++++++++++++++++------------------------- src/kind.d.ts | 1 - src/parser.ts | 23 +++++------ 3 files changed, 65 insertions(+), 66 deletions(-) diff --git a/src/ast.ts b/src/ast.ts index 602218d..5a93641 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -27,53 +27,54 @@ import type { InputObjectTypeExtensionNode, } from './schemaAst'; -export type ASTNode = Or< - GraphQL.ASTNode, - | NameNode - | DocumentNode - | OperationDefinitionNode - | VariableDefinitionNode - | VariableNode - | SelectionSetNode - | FieldNode - | ArgumentNode - | FragmentSpreadNode - | FragmentArgumentNode - | InlineFragmentNode - | FragmentDefinitionNode - | IntValueNode - | FloatValueNode - | StringValueNode - | BooleanValueNode - | NullValueNode - | EnumValueNode - | ListValueNode - | ObjectValueNode - | ObjectFieldNode - | DirectiveNode - | NamedTypeNode - | ListTypeNode - | NonNullTypeNode - | SchemaDefinitionNode - | OperationTypeDefinitionNode - | ScalarTypeDefinitionNode - | ObjectTypeDefinitionNode - | FieldDefinitionNode - | InputValueDefinitionNode - | InterfaceTypeDefinitionNode - | UnionTypeDefinitionNode - | EnumTypeDefinitionNode - | EnumValueDefinitionNode - | InputObjectTypeDefinitionNode - | DirectiveDefinitionNode - | SchemaExtensionNode - | ScalarTypeExtensionNode - | ObjectTypeExtensionNode - | InterfaceTypeExtensionNode - | UnionTypeExtensionNode - | EnumTypeExtensionNode - | InputObjectTypeExtensionNode ->; +export type ASTNode = + | Or< + GraphQL.ASTNode, + | NameNode + | DocumentNode + | OperationDefinitionNode + | VariableDefinitionNode + | VariableNode + | SelectionSetNode + | FieldNode + | ArgumentNode + | FragmentSpreadNode + | InlineFragmentNode + | FragmentDefinitionNode + | IntValueNode + | FloatValueNode + | StringValueNode + | BooleanValueNode + | NullValueNode + | EnumValueNode + | ListValueNode + | ObjectValueNode + | ObjectFieldNode + | DirectiveNode + | NamedTypeNode + | ListTypeNode + | NonNullTypeNode + | SchemaDefinitionNode + | OperationTypeDefinitionNode + | ScalarTypeDefinitionNode + | ObjectTypeDefinitionNode + | FieldDefinitionNode + | InputValueDefinitionNode + | InterfaceTypeDefinitionNode + | UnionTypeDefinitionNode + | EnumTypeDefinitionNode + | EnumValueDefinitionNode + | InputObjectTypeDefinitionNode + | DirectiveDefinitionNode + | SchemaExtensionNode + | ScalarTypeExtensionNode + | ObjectTypeExtensionNode + | InterfaceTypeExtensionNode + | UnionTypeExtensionNode + | EnumTypeExtensionNode + | InputObjectTypeExtensionNode + > + | FragmentArgumentNode; export type NameNode = Or< GraphQL.NameNode, @@ -148,10 +149,7 @@ export type SelectionSetNode = Or< } >; -export declare type SelectionNode = Or< - GraphQL.SelectionNode, - FieldNode | FragmentSpreadNode | InlineFragmentNode ->; +export declare type SelectionNode = FieldNode | FragmentSpreadNode | InlineFragmentNode; export type FieldNode = Or< GraphQL.FieldNode, @@ -187,7 +185,7 @@ export type ConstArgumentNode = Or< >; export type FragmentArgumentNode = { - readonly kind: Kind.FRAGMENT_ARGUMENT; + readonly kind: 'FragmentArgument'; readonly name: NameNode; readonly value: ValueNode; readonly loc?: Location; @@ -222,13 +220,14 @@ export type FragmentDefinitionNode = Or< readonly kind: Kind.FRAGMENT_DEFINITION; readonly name: NameNode; readonly description?: StringValueNode; - readonly variableDefinitions?: ReadonlyArray; readonly typeCondition: NamedTypeNode; readonly directives?: ReadonlyArray; readonly selectionSet: SelectionSetNode; readonly loc?: Location; } ->; +> & { + readonly variableDefinitions?: ReadonlyArray; +}; export type ValueNode = Or< GraphQL.ValueNode, diff --git a/src/kind.d.ts b/src/kind.d.ts index aa12cf7..ae9fd06 100644 --- a/src/kind.d.ts +++ b/src/kind.d.ts @@ -8,7 +8,6 @@ export declare enum Kind { SELECTION_SET = 'SelectionSet', FIELD = 'Field', ARGUMENT = 'Argument', - FRAGMENT_ARGUMENT = 'FragmentArgument', /** Fragments */ FRAGMENT_SPREAD = 'FragmentSpread', INLINE_FRAGMENT = 'InlineFragment', diff --git a/src/parser.ts b/src/parser.ts index fd39243..3bcc069 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -246,12 +246,17 @@ function value(constant: boolean): ast.ValueNode { }; } +function arguments_(constant: boolean, fragmentArgument: false): ast.ArgumentNode[] | undefined; function arguments_( constant: boolean, - fragmentArgument?: boolean + fragmentArgument: true +): ast.FragmentArgumentNode[] | undefined; +function arguments_( + constant: boolean, + fragmentArgument: boolean ): ast.ArgumentNode[] | ast.FragmentArgumentNode[] | undefined { if (input.charCodeAt(idx) === 40 /*'('*/) { - const args: ast.ArgumentNode[] | ast.FragmentArgumentNode[] = []; + const args: (ast.ArgumentNode | ast.FragmentArgumentNode)[] = []; idx++; ignored(); do { @@ -259,16 +264,14 @@ function arguments_( if (input.charCodeAt(idx++) !== 58 /*':'*/) throw error('Argument'); ignored(); args.push({ - kind: fragmentArgument - ? ('FragmentArgument' as Kind.FRAGMENT_ARGUMENT) - : ('Argument' as Kind.ARGUMENT), + kind: fragmentArgument ? 'FragmentArgument' : ('Argument' as Kind.ARGUMENT), name, value: value(constant), }); } while (input.charCodeAt(idx) !== 41 /*')'*/); idx++; ignored(); - return args; + return args as ast.ArgumentNode[] | ast.FragmentArgumentNode[]; } } @@ -283,7 +286,7 @@ function directives(constant: boolean): ast.DirectiveNode[] | undefined { directives.push({ kind: 'Directive' as Kind.DIRECTIVE, name: nameNode(), - arguments: arguments_(constant), + arguments: arguments_(constant, false), }); } while (input.charCodeAt(idx) === 64 /*'@'*/); return directives; @@ -363,7 +366,6 @@ function selectionSet(): ast.SelectionSetNode { selections.push({ kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD, name: nameNode(), - // @ts-expect-error arguments: arguments_(false, true) as readonly ast.FragmentArgumentNode[], directives: directives(false), }); @@ -385,8 +387,7 @@ function selectionSet(): ast.SelectionSetNode { selections.push({ kind: 'FragmentSpread' as Kind.FRAGMENT_SPREAD, name: nameNode(), - // @ts-expect-error - arguments: arguments_(false, true) as readonly ast.FragmentArgumentNode[], + arguments: arguments_(false, true), directives: directives(false), }); } @@ -399,7 +400,7 @@ function selectionSet(): ast.SelectionSetNode { alias = name; name = nameNode(); } - const _arguments = arguments_(false); + const _arguments = arguments_(false, false); const _directives = directives(false); let _selectionSet: ast.SelectionSetNode | undefined; if (input.charCodeAt(idx) === 123 /*'{'*/) { From 72eca4a38958aadb7dd774811cb858d77c7ce63d Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Fri, 12 Sep 2025 10:09:39 +0100 Subject: [PATCH 8/9] Revert conflict resolution with main version --- src/parser.ts | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 3bcc069..dc15d85 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -25,7 +25,6 @@ function advance(pattern: RegExp) { } const leadingRe = / +(?=[^\s])/y; -const nameRe = /[A-Za-z_][0-9A-Za-z_]*/y; function blockString(string: string) { const lines = string.split('\n'); let out = ''; @@ -471,27 +470,21 @@ function variableDefinitions(): ast.VariableDefinitionNode[] | undefined { } function fragmentDefinition(description?: ast.StringValueNode): ast.FragmentDefinitionNode { - let _name: string | undefined; - let _condition: string | undefined; - if ((_name = advance(nameRe)) == null) throw error('FragmentDefinition'); + const name = nameNode(); const _variableDefinitions = variableDefinitions(); - if (advance(nameRe) !== 'on') throw error('FragmentDefinition'); - ignored(); - if ((_condition = advance(nameRe)) == null) throw error('FragmentDefinition'); - ignored(); - const _directives = directives(false); - if (input.charCodeAt(idx++) !== 123 /*'{'*/) throw error('FragmentDefinition'); + if (input.charCodeAt(idx++) !== 111 /*'o'*/ || input.charCodeAt(idx++) !== 110 /*'n'*/) + throw error('FragmentDefinition'); ignored(); const fragDef: ast.FragmentDefinitionNode = { kind: 'FragmentDefinition' as Kind.FRAGMENT_DEFINITION, - name: { kind: 'Name' as Kind.NAME, value: _name }, + name, typeCondition: { kind: 'NamedType' as Kind.NAMED_TYPE, - name: { kind: 'Name' as Kind.NAME, value: _condition }, + name: nameNode(), }, variableDefinitions: _variableDefinitions, - directives: _directives, - selectionSet: selectionSet(), + directives: directives(false), + selectionSet: selectionSetStart(), }; if (description) { fragDef.description = description; From a48b9cd92a4d52fb45ec1b283198b506d949f856 Mon Sep 17 00:00:00 2001 From: jdecroock Date: Fri, 12 Sep 2025 11:20:17 +0200 Subject: [PATCH 9/9] Fix print --- src/__tests__/printer.test.ts | 2 +- src/printer.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/__tests__/printer.test.ts b/src/__tests__/printer.test.ts index d6d85e4..9dfd643 100644 --- a/src/__tests__/printer.test.ts +++ b/src/__tests__/printer.test.ts @@ -190,7 +190,7 @@ describe('print', () => { }, arguments: [ { - kind: 'Argument', + kind: 'FragmentArgument', name: { kind: 'Name', value: 'var', diff --git a/src/printer.ts b/src/printer.ts index 4e3a174..2efdebf 100644 --- a/src/printer.ts +++ b/src/printer.ts @@ -24,6 +24,7 @@ import type { NamedTypeNode, ListTypeNode, NonNullTypeNode, + FragmentArgumentNode, } from './ast'; function mapJoin(value: readonly T[], joiner: string, mapper: (value: T) => string): string { @@ -47,7 +48,10 @@ const MAX_LINE_LENGTH = 80; let LF = '\n'; -function Arguments(length: number, node: readonly ArgumentNode[]): string { +function Arguments( + length: number, + node: readonly ArgumentNode[] | readonly FragmentArgumentNode[] +): string { const args = mapJoin(node, ', ', nodes.Argument); if (length + args.length + 2 > MAX_LINE_LENGTH) { return '(' + (LF += ' ') + mapJoin(node, LF, nodes.Argument) + (LF = LF.slice(0, -2)) + ')'; @@ -138,7 +142,7 @@ const nodes = { SelectionSet(node: SelectionSetNode): string { return '{' + (LF += ' ') + mapJoin(node.selections, LF, _print) + (LF = LF.slice(0, -2)) + '}'; }, - Argument(node: ArgumentNode): string { + Argument(node: ArgumentNode | FragmentArgumentNode): string { return node.name.value + ': ' + _print(node.value); }, FragmentSpread(node: FragmentSpreadNode): string {