-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
inline graphql-js' printer #4692
Open
cometkim
wants to merge
7
commits into
facebook:main
Choose a base branch
from
cometkim:js-printer
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
7cea78e
inline graphql-js' printer
cometkim e3df678
add test to check printer idempotence
cometkim 1b154f7
fix impl
cometkim 9617678
fix lint and typecheck
cometkim 0f1ce19
fix lint
cometkim 824d55d
avoid deprecated top-level leave visitor
cometkim e8e7298
fix format
cometkim File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
65 changes: 65 additions & 0 deletions
65
packages/babel-plugin-relay/__tests__/printGraphQL-test.js
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,65 @@ | ||
/** | ||
* @flow | ||
* @format | ||
*/ | ||
|
||
'use strict'; | ||
|
||
const print = require('../printGraphQL'); | ||
const fs = require('fs'); | ||
const {parse} = require('graphql'); | ||
const path = require('path'); | ||
|
||
type OutputFixture = {name: string, input: string, output: string}; | ||
type ErrorFixture = {name: string, input: string, error: string}; | ||
type PrinterFixture = OutputFixture | ErrorFixture; | ||
|
||
describe('printGraphQL', () => { | ||
const outputFixtures = loadPrinterFixtures() | ||
.filter(fixture => fixture.output) | ||
// object key format doesn't work | ||
.map(fixture => [fixture.name, fixture.input, fixture.output]); | ||
|
||
it.each(outputFixtures)( | ||
'tests printer idempotence: %s', | ||
(_name, input, expected) => { | ||
expect(print(parse(input))).toEqual(expected); | ||
}, | ||
); | ||
}); | ||
|
||
function loadPrinterFixtures(): PrinterFixture[] { | ||
const fixturesPath = path.join( | ||
__dirname, | ||
'../../../compiler/crates/graphql-text-printer/tests/print_ast/fixtures', | ||
); | ||
const fixtures = []; | ||
for (const file of fs.readdirSync(fixturesPath)) { | ||
if (!file.endsWith('.expected')) { | ||
continue; | ||
} | ||
const content = fs.readFileSync(path.join(fixturesPath, file), 'utf8'); | ||
try { | ||
const fixture = parsePrintFixture(file, content); | ||
fixtures.push(fixture); | ||
} catch (err) { | ||
console.error(err); | ||
} | ||
} | ||
return fixtures; | ||
} | ||
|
||
function parsePrintFixture(name: string, content: string): PrinterFixture { | ||
const successPatttern = | ||
/^=+ INPUT =+\n(?<input>[\s\S]*)\n=+ OUTPUT =+\n(?<output>[\s\S]*)$/; | ||
const failurePattern = | ||
/^=+ INPUT =+\n(?<input>[\s\S]*)\n=+ ERROR =+\n(?<error>[\s\S]*)$/; | ||
|
||
const match = content.match(successPatttern) ?? content.match(failurePattern); | ||
if (!match) { | ||
throw new Error( | ||
`Failed to parse ${name}. Unknown fixture format from the graphql-text-printer crate!`, | ||
); | ||
} | ||
return {...(match.groups as any), name} as PrinterFixture; | ||
} |
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
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
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,202 @@ | ||
/** | ||
* Inlined GraphQL printer, | ||
* forked from https://github.com/graphql/graphql-js/blob/v15.3.0/src/language/printer.js | ||
* | ||
* This ensure compatibility with document hash generated by the Relay compiler. | ||
* graphql-js' printer is incompatible with Relay's one since v15.4 | ||
* | ||
* TODO: Canonicalize printer spec for Relay projects | ||
* | ||
* @see https://github.com/facebook/relay/pull/3628 | ||
* @see https://github.com/facebook/relay/issues/4226 | ||
*/ | ||
|
||
const { visit } = require('graphql'); | ||
|
||
/** | ||
* Converts an AST into a string, using one set of reasonable | ||
* formatting rules. | ||
*/ | ||
function print(ast: any): string { | ||
return visit(ast, { | ||
Name: { leave: (node) => node.value }, | ||
Variable: { leave: (node) => '$' + node.name }, | ||
|
||
// Document | ||
|
||
Document: { leave: (node) => join(node.definitions, '\n') + '\n' }, | ||
|
||
OperationDefinition: { | ||
leave: (node) => { | ||
const op = node.operation; | ||
const name = node.name; | ||
const varDefs = wrap('(', join(node.variableDefinitions, ', '), ')'); | ||
const directives = join(node.directives, ' '); | ||
const selectionSet = node.selectionSet; | ||
// Anonymous queries with no directives or variable definitions can use | ||
// the query short form. | ||
return !name && !directives && !varDefs && op === 'query' | ||
? selectionSet | ||
: join([op, join([name, varDefs]), directives, selectionSet], ' '); | ||
}, | ||
}, | ||
|
||
VariableDefinition: { | ||
leave: ({ variable, type, defaultValue, directives }) => | ||
variable + | ||
': ' + | ||
type + | ||
wrap(' = ', defaultValue) + | ||
wrap(' ', join(directives, ' ')), | ||
}, | ||
SelectionSet: { | ||
leave: ({ selections }) => block(selections), | ||
}, | ||
|
||
Field: { | ||
leave: ({ alias, name, arguments: args, directives, selectionSet }) => | ||
join( | ||
[ | ||
wrap('', alias, ': ') + name + wrap('(', join(args, ', '), ')'), | ||
join(directives, ' '), | ||
selectionSet, | ||
], | ||
' ', | ||
), | ||
}, | ||
|
||
Argument: { | ||
leave: ({ name, value }) => name + ': ' + value, | ||
}, | ||
|
||
// Fragments | ||
|
||
FragmentSpread: { | ||
leave: ({ name, directives }) => | ||
'...' + name + wrap(' ', join(directives, ' ')), | ||
}, | ||
|
||
InlineFragment: { | ||
leave: ({ typeCondition, directives, selectionSet }) => | ||
join( | ||
['...', wrap('on ', typeCondition), join(directives, ' '), selectionSet], | ||
' ', | ||
), | ||
}, | ||
|
||
FragmentDefinition: { | ||
leave: ({ | ||
name, | ||
typeCondition, | ||
variableDefinitions, | ||
directives, | ||
selectionSet, | ||
}) => | ||
// Note: fragment variable definitions are experimental and may be changed | ||
// or removed in the future. | ||
`fragment ${name}${wrap('(', join(variableDefinitions, ', '), ')')} ` + | ||
`on ${typeCondition} ${wrap('', join(directives, ' '), ' ')}` + | ||
selectionSet, | ||
}, | ||
|
||
// Value | ||
|
||
IntValue: { leave: ({ value }) => value }, | ||
FloatValue: { leave: ({ value }) => value }, | ||
StringValue: { | ||
leave: ({ value, block: isBlockString }, key) => | ||
isBlockString | ||
? printBlockString(value, key === 'description' ? '' : ' ') | ||
: JSON.stringify(value), | ||
}, | ||
BooleanValue: { leave: ({ value }) => (value ? 'true' : 'false') }, | ||
NullValue: { leave: () => 'null' }, | ||
EnumValue: { leave: ({ value }) => value }, | ||
ListValue: { leave: ({ values }) => '[' + join(values, ', ') + ']' }, | ||
ObjectValue: { leave: ({ fields }) => '{' + join(fields, ', ') + '}' }, | ||
ObjectField: { leave: ({ name, value }) => name + ': ' + value }, | ||
|
||
// Directive | ||
|
||
Directive: { | ||
leave: ({ name, arguments: args }) => | ||
'@' + name + wrap('(', join(args, ', '), ')'), | ||
}, | ||
|
||
// Type | ||
|
||
NamedType: { leave: ({ name }) => name }, | ||
ListType: { leave: ({ type }) => '[' + type + ']' }, | ||
NonNullType: { leave: ({ type }) => type + '!' }, | ||
|
||
// Type System Definitions | ||
// Removed since that never included in the Relay documents | ||
}); | ||
} | ||
|
||
/** | ||
* Given maybeArray, print an empty string if it is null or empty, otherwise | ||
* print all items together separated by separator if provided | ||
*/ | ||
function join(maybeArray: ?Array<string>, separator = '') { | ||
return maybeArray?.filter((x) => x).join(separator) ?? ''; | ||
} | ||
|
||
/** | ||
* Given array, print each item on its own line, wrapped in an | ||
* indented "{ }" block. | ||
*/ | ||
function block(array) { | ||
return array && array.length !== 0 | ||
? '{\n' + indent(join(array, '\n')) + '\n}' | ||
: ''; | ||
} | ||
|
||
/** | ||
* If maybeString is not null or empty, then wrap with start and end, otherwise | ||
* print an empty string. | ||
*/ | ||
function wrap(start, maybeString, end = '') { | ||
return maybeString ? start + maybeString + end : ''; | ||
} | ||
|
||
function indent(maybeString) { | ||
return maybeString && ' ' + maybeString.replace(/\n/g, '\n '); | ||
} | ||
|
||
/** | ||
* Print a block string in the indented block form by adding a leading and | ||
* trailing blank line. However, if a block string starts with whitespace and is | ||
* a single-line, adding a leading blank line would strip that whitespace. | ||
* | ||
* @internal | ||
*/ | ||
function printBlockString( | ||
value: string, | ||
indentation?: string = '', | ||
preferMultipleLines?: boolean = false, | ||
): string { | ||
const isSingleLine = value.indexOf('\n') === -1; | ||
const hasLeadingSpace = value[0] === ' ' || value[0] === '\t'; | ||
const hasTrailingQuote = value[value.length - 1] === '"'; | ||
const hasTrailingSlash = value[value.length - 1] === '\\'; | ||
const printAsMultipleLines = | ||
!isSingleLine || | ||
hasTrailingQuote || | ||
hasTrailingSlash || | ||
preferMultipleLines; | ||
|
||
let result = ''; | ||
// Format a multi-line block quote to account for leading space. | ||
if (printAsMultipleLines && !(isSingleLine && hasLeadingSpace)) { | ||
result += '\n' + indentation; | ||
} | ||
result += indentation ? value.replace(/\n/g, '\n' + indentation) : value; | ||
if (printAsMultipleLines) { | ||
result += '\n'; | ||
} | ||
|
||
return '"""' + result.replace(/"""/g, '\\"""') + '"""'; | ||
} | ||
|
||
module.exports = print; |
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
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love this. Brilliant solution. When I merge I think I'll add a note in the Rust tests so that people know the fixtures are serving double duty.