Skip to content

Commit 2923c60

Browse files
committed
wip: implement parser extensions for transforming the fragment argument transform syntax into operations without fragment arguments, which are executable by all graphql.js versions
See graphql/graphql-js#3152 for reference
1 parent 8a2858b commit 2923c60

File tree

4 files changed

+306
-0
lines changed

4 files changed

+306
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "@envelop/fragment-arguments",
3+
"version": "0.0.1",
4+
"author": "Dotan Simha <[email protected]>",
5+
"license": "MIT",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/dotansimha/envelop.git",
9+
"directory": "packages/plugins/fragment-arguments"
10+
},
11+
"sideEffects": false,
12+
"main": "dist/index.cjs.js",
13+
"module": "dist/index.esm.js",
14+
"typings": "dist/index.d.ts",
15+
"typescript": {
16+
"definition": "dist/index.d.ts"
17+
},
18+
"scripts": {
19+
"test": "jest",
20+
"prepack": "bob prepack"
21+
},
22+
"devDependencies": {
23+
"@types/common-tags": "1.8.0",
24+
"@graphql-tools/schema": "7.1.5",
25+
"bob-the-bundler": "1.2.1",
26+
"graphql": "15.5.0",
27+
"typescript": "4.3.2",
28+
"oneline": "1.0.3",
29+
"common-tags": "1.8.0"
30+
},
31+
"peerDependencies": {
32+
"graphql": "^14.0.0 || ^15.0.0"
33+
},
34+
"buildOptions": {
35+
"input": "./src/index.ts"
36+
},
37+
"publishConfig": {
38+
"directory": "dist",
39+
"access": "public"
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { Plugin } from '@envelop/types';
2+
import { Parser, ParseOptions } from 'graphql/language/parser';
3+
import { Lexer } from 'graphql/language/lexer';
4+
5+
import {
6+
TokenKind,
7+
Kind,
8+
Source,
9+
DocumentNode,
10+
visit,
11+
FragmentDefinitionNode,
12+
InlineFragmentNode,
13+
ArgumentNode,
14+
TokenKindEnum,
15+
Token,
16+
} from 'graphql';
17+
18+
declare module 'graphql/language/parser' {
19+
export class Parser {
20+
constructor(source: string | Source, options?: ParseOptions);
21+
_lexer: Lexer;
22+
expectOptionalKeyword(word: string): boolean;
23+
expectToken(token: TokenKindEnum): void;
24+
peek(token: TokenKindEnum): boolean;
25+
parseFragmentName(): string;
26+
parseArguments(flag: boolean): any;
27+
parseDirectives(flag: boolean): any;
28+
loc(start: Token): any;
29+
parseNamedType(): any;
30+
parseSelectionSet(): any;
31+
expectKeyword(keyword: string): void;
32+
parseVariableDefinitions(): void;
33+
parseDocument(): DocumentNode;
34+
}
35+
}
36+
37+
class FragmentArgumentCompatible extends Parser {
38+
parseFragment() {
39+
const start = this._lexer.token;
40+
this.expectToken(TokenKind.SPREAD);
41+
const hasTypeCondition = this.expectOptionalKeyword('on');
42+
if (!hasTypeCondition && this.peek(TokenKind.NAME)) {
43+
const name = this.parseFragmentName();
44+
if (this.peek(TokenKind.PAREN_L)) {
45+
return {
46+
kind: Kind.FRAGMENT_SPREAD,
47+
name,
48+
arguments: this.parseArguments(false),
49+
directives: this.parseDirectives(false),
50+
loc: this.loc(start),
51+
};
52+
}
53+
return {
54+
kind: Kind.FRAGMENT_SPREAD,
55+
name: this.parseFragmentName(),
56+
directives: this.parseDirectives(false),
57+
loc: this.loc(start),
58+
};
59+
}
60+
return {
61+
kind: Kind.INLINE_FRAGMENT,
62+
typeCondition: hasTypeCondition ? this.parseNamedType() : undefined,
63+
directives: this.parseDirectives(false),
64+
selectionSet: this.parseSelectionSet(),
65+
loc: this.loc(start),
66+
};
67+
}
68+
69+
parseFragmentDefinition() {
70+
const start = this._lexer.token;
71+
this.expectKeyword('fragment');
72+
const name = this.parseFragmentName();
73+
if (this.peek(TokenKind.PAREN_L)) {
74+
return {
75+
kind: Kind.FRAGMENT_DEFINITION,
76+
name,
77+
variableDefinitions: this.parseVariableDefinitions(),
78+
typeCondition: (this.expectKeyword('on'), this.parseNamedType()),
79+
directives: this.parseDirectives(false),
80+
selectionSet: this.parseSelectionSet(),
81+
loc: this.loc(start),
82+
};
83+
}
84+
85+
return {
86+
kind: Kind.FRAGMENT_DEFINITION,
87+
name,
88+
typeCondition: (this.expectKeyword('on'), this.parseNamedType()),
89+
directives: this.parseDirectives(false),
90+
selectionSet: this.parseSelectionSet(),
91+
loc: this.loc(start),
92+
};
93+
}
94+
}
95+
96+
function pimpedParse(source: string | Source, options?: ParseOptions): DocumentNode {
97+
const parser = new FragmentArgumentCompatible(source, options);
98+
return parser.parseDocument();
99+
}
100+
101+
export const useFragmentArguments = (): Plugin => {
102+
return {
103+
onParse({ setParseFn }) {
104+
setParseFn(pimpedParse);
105+
106+
return ({ result, replaceParseResult }) => {
107+
if (result && 'kind' in result) {
108+
replaceParseResult(applySelectionSetFragmentArguments(result));
109+
}
110+
};
111+
},
112+
};
113+
};
114+
115+
function applySelectionSetFragmentArguments(document: DocumentNode): DocumentNode | Error {
116+
const fragmentList = new Map<string, FragmentDefinitionNode>();
117+
for (const def of document.definitions) {
118+
if (def.kind !== 'FragmentDefinition') {
119+
continue;
120+
}
121+
fragmentList.set(def.name.value, def);
122+
}
123+
124+
return visit(document, {
125+
FragmentSpread(fragmentNode) {
126+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
127+
// @ts-ignore
128+
if (fragmentNode.arguments != null && fragmentNode.arguments.length) {
129+
const fragmentDef = fragmentList.get(fragmentNode.name.value);
130+
if (!fragmentDef) {
131+
return;
132+
}
133+
134+
const fragmentArguments = new Map<string, ArgumentNode>();
135+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
136+
// @ts-ignore
137+
for (const arg of fragmentNode.arguments) {
138+
fragmentArguments.set(arg.name.value, arg);
139+
}
140+
141+
const selectionSet = visit(fragmentDef.selectionSet, {
142+
Variable(variableNode) {
143+
const fragArg = fragmentArguments.get(variableNode.name.value);
144+
if (fragArg) {
145+
return fragArg.value;
146+
}
147+
148+
return variableNode;
149+
},
150+
});
151+
152+
const inlineFragment: InlineFragmentNode = {
153+
kind: 'InlineFragment',
154+
typeCondition: fragmentDef.typeCondition,
155+
selectionSet,
156+
};
157+
158+
return inlineFragment;
159+
}
160+
return fragmentNode;
161+
},
162+
});
163+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { buildSchema, print } from 'graphql';
2+
import { oneLine, stripIndent } from 'common-tags';
3+
import { diff } from 'jest-diff';
4+
import { envelop, useSchema } from '@envelop/core';
5+
import { useFragmentArguments } from '../src';
6+
7+
function compareStrings(a: string, b: string): boolean {
8+
return a.includes(b);
9+
}
10+
11+
expect.extend({
12+
toBeSimilarStringTo(received: string, expected: string) {
13+
const strippedReceived = oneLine`${received}`.replace(/\s\s+/g, ' ');
14+
const strippedExpected = oneLine`${expected}`.replace(/\s\s+/g, ' ');
15+
16+
if (compareStrings(strippedReceived, strippedExpected)) {
17+
return {
18+
message: () =>
19+
`expected
20+
${received}
21+
not to be a string containing (ignoring indents)
22+
${expected}`,
23+
pass: true,
24+
};
25+
} else {
26+
const diffString = diff(stripIndent`${expected}`, stripIndent`${received}`, {
27+
expand: this.expand,
28+
});
29+
const hasExpect = diffString && diffString.includes('- Expect');
30+
31+
const message = hasExpect
32+
? `Difference:\n\n${diffString}`
33+
: `expected
34+
${received}
35+
to be a string containing (ignoring indents)
36+
${expected}`;
37+
38+
return {
39+
message: () => message,
40+
pass: false,
41+
};
42+
}
43+
},
44+
});
45+
46+
declare global {
47+
// eslint-disable-next-line no-redeclare
48+
namespace jest {
49+
interface Matchers<R, T> {
50+
/**
51+
* Normalizes whitespace and performs string comparisons
52+
*/
53+
toBeSimilarStringTo(expected: string): R;
54+
}
55+
}
56+
}
57+
58+
describe('useFragmentArguments', () => {
59+
const schema = buildSchema(/* GraphQL */ `
60+
type Query {
61+
a: TestType
62+
}
63+
64+
type TestType {
65+
a(b: String): Boolean
66+
}
67+
`);
68+
test('can inline fragment with argument', () => {
69+
const { parse } = envelop({ plugins: [useFragmentArguments(), useSchema(schema)] })();
70+
const result = parse(/* GraphQL */ `
71+
fragment TestFragment($c: String) on Query {
72+
a(b: $c)
73+
}
74+
75+
query TestQuery($a: String) {
76+
...TestFragment(c: $a)
77+
}
78+
`);
79+
expect(print(result)).toBeSimilarStringTo(/* GraphQL */ `
80+
query TestQuery($a: String) {
81+
... on Query {
82+
a(b: $a)
83+
}
84+
}
85+
`);
86+
});
87+
});

yarn.lock

+15
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,11 @@
13001300
"@types/connect" "*"
13011301
"@types/node" "*"
13021302

1303+
1304+
version "1.8.0"
1305+
resolved "https://registry.yarnpkg.com/@types/common-tags/-/common-tags-1.8.0.tgz#79d55e748d730b997be5b7fce4b74488d8b26a6b"
1306+
integrity sha512-htRqZr5qn8EzMelhX/Xmx142z218lLyGaeZ3YR8jlze4TATRU9huKKvuBmAJEW4LCC4pnY1N6JAm6p85fMHjhg==
1307+
13031308
"@types/connect@*":
13041309
version "3.4.34"
13051310
resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz#170a40223a6d666006d93ca128af2beb1d9b1901"
@@ -2565,6 +2570,11 @@ commander@^7.2.0:
25652570
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
25662571
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
25672572

2573+
2574+
version "1.8.0"
2575+
resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937"
2576+
integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==
2577+
25682578
commondir@^1.0.1:
25692579
version "1.0.1"
25702580
resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@@ -5748,6 +5758,11 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
57485758
dependencies:
57495759
wrappy "1"
57505760

5761+
5762+
version "1.0.3"
5763+
resolved "https://registry.yarnpkg.com/oneline/-/oneline-1.0.3.tgz#2f2631bd3a5716a4eeb439291697af2fc7fa39a5"
5764+
integrity sha512-KWLrLloG/ShWvvWuvmOL2jw17++ufGdbkKC2buI2Aa6AaM4AkjCtpeJZg60EK34NQVo2qu1mlPrC2uhvQgCrhQ==
5765+
57515766
onetime@^5.1.0, onetime@^5.1.2:
57525767
version "5.1.2"
57535768
resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"

0 commit comments

Comments
 (0)