-
Notifications
You must be signed in to change notification settings - Fork 129
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
wip: implement parser extensions for transforming the fragment argume…
…nt transform syntax into operations without fragment arguments, which are executable by all graphql.js versions See graphql/graphql-js#3152 for reference
- Loading branch information
Showing
4 changed files
with
306 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
{ | ||
"name": "@envelop/fragment-arguments", | ||
"version": "0.0.1", | ||
"author": "Dotan Simha <[email protected]>", | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/dotansimha/envelop.git", | ||
"directory": "packages/plugins/fragment-arguments" | ||
}, | ||
"sideEffects": false, | ||
"main": "dist/index.cjs.js", | ||
"module": "dist/index.esm.js", | ||
"typings": "dist/index.d.ts", | ||
"typescript": { | ||
"definition": "dist/index.d.ts" | ||
}, | ||
"scripts": { | ||
"test": "jest", | ||
"prepack": "bob prepack" | ||
}, | ||
"devDependencies": { | ||
"@types/common-tags": "1.8.0", | ||
"@graphql-tools/schema": "7.1.5", | ||
"bob-the-bundler": "1.2.1", | ||
"graphql": "15.5.0", | ||
"typescript": "4.3.2", | ||
"oneline": "1.0.3", | ||
"common-tags": "1.8.0" | ||
}, | ||
"peerDependencies": { | ||
"graphql": "^14.0.0 || ^15.0.0" | ||
}, | ||
"buildOptions": { | ||
"input": "./src/index.ts" | ||
}, | ||
"publishConfig": { | ||
"directory": "dist", | ||
"access": "public" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import { Plugin } from '@envelop/types'; | ||
import { Parser, ParseOptions } from 'graphql/language/parser'; | ||
import { Lexer } from 'graphql/language/lexer'; | ||
|
||
import { | ||
TokenKind, | ||
Kind, | ||
Source, | ||
DocumentNode, | ||
visit, | ||
FragmentDefinitionNode, | ||
InlineFragmentNode, | ||
ArgumentNode, | ||
TokenKindEnum, | ||
Token, | ||
} from 'graphql'; | ||
|
||
declare module 'graphql/language/parser' { | ||
export class Parser { | ||
constructor(source: string | Source, options?: ParseOptions); | ||
_lexer: Lexer; | ||
expectOptionalKeyword(word: string): boolean; | ||
expectToken(token: TokenKindEnum): void; | ||
peek(token: TokenKindEnum): boolean; | ||
parseFragmentName(): string; | ||
parseArguments(flag: boolean): any; | ||
parseDirectives(flag: boolean): any; | ||
loc(start: Token): any; | ||
parseNamedType(): any; | ||
parseSelectionSet(): any; | ||
expectKeyword(keyword: string): void; | ||
parseVariableDefinitions(): void; | ||
parseDocument(): DocumentNode; | ||
} | ||
} | ||
|
||
class FragmentArgumentCompatible extends Parser { | ||
parseFragment() { | ||
const start = this._lexer.token; | ||
this.expectToken(TokenKind.SPREAD); | ||
const hasTypeCondition = this.expectOptionalKeyword('on'); | ||
if (!hasTypeCondition && this.peek(TokenKind.NAME)) { | ||
const name = this.parseFragmentName(); | ||
if (this.peek(TokenKind.PAREN_L)) { | ||
return { | ||
kind: Kind.FRAGMENT_SPREAD, | ||
name, | ||
arguments: this.parseArguments(false), | ||
directives: this.parseDirectives(false), | ||
loc: this.loc(start), | ||
}; | ||
} | ||
return { | ||
kind: Kind.FRAGMENT_SPREAD, | ||
name: this.parseFragmentName(), | ||
directives: this.parseDirectives(false), | ||
loc: this.loc(start), | ||
}; | ||
} | ||
return { | ||
kind: Kind.INLINE_FRAGMENT, | ||
typeCondition: hasTypeCondition ? this.parseNamedType() : undefined, | ||
directives: this.parseDirectives(false), | ||
selectionSet: this.parseSelectionSet(), | ||
loc: this.loc(start), | ||
}; | ||
} | ||
|
||
parseFragmentDefinition() { | ||
const start = this._lexer.token; | ||
this.expectKeyword('fragment'); | ||
const name = this.parseFragmentName(); | ||
if (this.peek(TokenKind.PAREN_L)) { | ||
return { | ||
kind: Kind.FRAGMENT_DEFINITION, | ||
name, | ||
variableDefinitions: this.parseVariableDefinitions(), | ||
typeCondition: (this.expectKeyword('on'), this.parseNamedType()), | ||
directives: this.parseDirectives(false), | ||
selectionSet: this.parseSelectionSet(), | ||
loc: this.loc(start), | ||
}; | ||
} | ||
|
||
return { | ||
kind: Kind.FRAGMENT_DEFINITION, | ||
name, | ||
typeCondition: (this.expectKeyword('on'), this.parseNamedType()), | ||
directives: this.parseDirectives(false), | ||
selectionSet: this.parseSelectionSet(), | ||
loc: this.loc(start), | ||
}; | ||
} | ||
} | ||
|
||
function pimpedParse(source: string | Source, options?: ParseOptions): DocumentNode { | ||
const parser = new FragmentArgumentCompatible(source, options); | ||
return parser.parseDocument(); | ||
} | ||
|
||
export const useFragmentArguments = (): Plugin => { | ||
return { | ||
onParse({ setParseFn }) { | ||
setParseFn(pimpedParse); | ||
|
||
return ({ result, replaceParseResult }) => { | ||
if (result && 'kind' in result) { | ||
replaceParseResult(applySelectionSetFragmentArguments(result)); | ||
} | ||
}; | ||
}, | ||
}; | ||
}; | ||
|
||
function applySelectionSetFragmentArguments(document: DocumentNode): DocumentNode | Error { | ||
const fragmentList = new Map<string, FragmentDefinitionNode>(); | ||
for (const def of document.definitions) { | ||
if (def.kind !== 'FragmentDefinition') { | ||
continue; | ||
} | ||
fragmentList.set(def.name.value, def); | ||
} | ||
|
||
return visit(document, { | ||
FragmentSpread(fragmentNode) { | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
if (fragmentNode.arguments != null && fragmentNode.arguments.length) { | ||
const fragmentDef = fragmentList.get(fragmentNode.name.value); | ||
if (!fragmentDef) { | ||
return; | ||
} | ||
|
||
const fragmentArguments = new Map<string, ArgumentNode>(); | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
for (const arg of fragmentNode.arguments) { | ||
fragmentArguments.set(arg.name.value, arg); | ||
} | ||
|
||
const selectionSet = visit(fragmentDef.selectionSet, { | ||
Variable(variableNode) { | ||
const fragArg = fragmentArguments.get(variableNode.name.value); | ||
if (fragArg) { | ||
return fragArg.value; | ||
} | ||
|
||
return variableNode; | ||
}, | ||
}); | ||
|
||
const inlineFragment: InlineFragmentNode = { | ||
kind: 'InlineFragment', | ||
typeCondition: fragmentDef.typeCondition, | ||
selectionSet, | ||
}; | ||
|
||
return inlineFragment; | ||
} | ||
return fragmentNode; | ||
}, | ||
}); | ||
} |
87 changes: 87 additions & 0 deletions
87
packages/plugins/fragment-arguments/test/use-fragment-arguments.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { buildSchema, print } from 'graphql'; | ||
import { oneLine, stripIndent } from 'common-tags'; | ||
import { diff } from 'jest-diff'; | ||
import { envelop, useSchema } from '@envelop/core'; | ||
import { useFragmentArguments } from '../src'; | ||
|
||
function compareStrings(a: string, b: string): boolean { | ||
return a.includes(b); | ||
} | ||
|
||
expect.extend({ | ||
toBeSimilarStringTo(received: string, expected: string) { | ||
const strippedReceived = oneLine`${received}`.replace(/\s\s+/g, ' '); | ||
const strippedExpected = oneLine`${expected}`.replace(/\s\s+/g, ' '); | ||
|
||
if (compareStrings(strippedReceived, strippedExpected)) { | ||
return { | ||
message: () => | ||
`expected | ||
${received} | ||
not to be a string containing (ignoring indents) | ||
${expected}`, | ||
pass: true, | ||
}; | ||
} else { | ||
const diffString = diff(stripIndent`${expected}`, stripIndent`${received}`, { | ||
expand: this.expand, | ||
}); | ||
const hasExpect = diffString && diffString.includes('- Expect'); | ||
|
||
const message = hasExpect | ||
? `Difference:\n\n${diffString}` | ||
: `expected | ||
${received} | ||
to be a string containing (ignoring indents) | ||
${expected}`; | ||
|
||
return { | ||
message: () => message, | ||
pass: false, | ||
}; | ||
} | ||
}, | ||
}); | ||
|
||
declare global { | ||
// eslint-disable-next-line no-redeclare | ||
namespace jest { | ||
interface Matchers<R, T> { | ||
/** | ||
* Normalizes whitespace and performs string comparisons | ||
*/ | ||
toBeSimilarStringTo(expected: string): R; | ||
} | ||
} | ||
} | ||
|
||
describe('useFragmentArguments', () => { | ||
const schema = buildSchema(/* GraphQL */ ` | ||
type Query { | ||
a: TestType | ||
} | ||
type TestType { | ||
a(b: String): Boolean | ||
} | ||
`); | ||
test('can inline fragment with argument', () => { | ||
const { parse } = envelop({ plugins: [useFragmentArguments(), useSchema(schema)] })(); | ||
const result = parse(/* GraphQL */ ` | ||
fragment TestFragment($c: String) on Query { | ||
a(b: $c) | ||
} | ||
query TestQuery($a: String) { | ||
...TestFragment(c: $a) | ||
} | ||
`); | ||
expect(print(result)).toBeSimilarStringTo(/* GraphQL */ ` | ||
query TestQuery($a: String) { | ||
... on Query { | ||
a(b: $a) | ||
} | ||
} | ||
`); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1300,6 +1300,11 @@ | |
"@types/connect" "*" | ||
"@types/node" "*" | ||
|
||
"@types/[email protected]": | ||
version "1.8.0" | ||
resolved "https://registry.yarnpkg.com/@types/common-tags/-/common-tags-1.8.0.tgz#79d55e748d730b997be5b7fce4b74488d8b26a6b" | ||
integrity sha512-htRqZr5qn8EzMelhX/Xmx142z218lLyGaeZ3YR8jlze4TATRU9huKKvuBmAJEW4LCC4pnY1N6JAm6p85fMHjhg== | ||
|
||
"@types/connect@*": | ||
version "3.4.34" | ||
resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz#170a40223a6d666006d93ca128af2beb1d9b1901" | ||
|
@@ -2565,6 +2570,11 @@ commander@^7.2.0: | |
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" | ||
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== | ||
|
||
[email protected]: | ||
version "1.8.0" | ||
resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" | ||
integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw== | ||
|
||
commondir@^1.0.1: | ||
version "1.0.1" | ||
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: | |
dependencies: | ||
wrappy "1" | ||
|
||
[email protected]: | ||
version "1.0.3" | ||
resolved "https://registry.yarnpkg.com/oneline/-/oneline-1.0.3.tgz#2f2631bd3a5716a4eeb439291697af2fc7fa39a5" | ||
integrity sha512-KWLrLloG/ShWvvWuvmOL2jw17++ufGdbkKC2buI2Aa6AaM4AkjCtpeJZg60EK34NQVo2qu1mlPrC2uhvQgCrhQ== | ||
|
||
onetime@^5.1.0, onetime@^5.1.2: | ||
version "5.1.2" | ||
resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" | ||
|