Skip to content
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
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
65 changes: 65 additions & 0 deletions packages/babel-plugin-relay/__tests__/printGraphQL-test.js
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 {
Copy link
Contributor

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.

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;
}
4 changes: 2 additions & 2 deletions packages/babel-plugin-relay/compileGraphQLTag.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import type {
OperationDefinitionNode,
} from 'graphql';

const printGraphQL = require('./printGraphQL.js');
const crypto = require('crypto');
const {print} = require('graphql');
const {
dirname,
join: joinPath,
Expand Down Expand Up @@ -125,7 +125,7 @@ function createNode(

const hash = crypto
.createHash('md5')
.update(print(graphqlDefinition), 'utf8')
.update(printGraphQL(graphqlDefinition), 'utf8')
.digest('hex');

let topScope = path.scope;
Expand Down
8 changes: 6 additions & 2 deletions packages/babel-plugin-relay/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@
"url": "https://github.com/facebook/relay.git",
"directory": "packages/babel-plugin-relay"
},
"peerDependencies": {
"@babel/core": "^7.0.0",
"graphql": "^15.0.0 || ^16.0.0"
},
"dependencies": {
"babel-plugin-macros": "^2.0.0",
"cosmiconfig": "^5.0.5",
"graphql": "15.3.0"
"cosmiconfig": "^5.0.5"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"graphql": "^16.8.1",
"prettier": "2.8.8",
"prettier-plugin-hermes-parser": "0.22.0"
}
Expand Down
202 changes: 202 additions & 0 deletions packages/babel-plugin-relay/printGraphQL.js
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;
40 changes: 20 additions & 20 deletions packages/babel-plugin-relay/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -348,27 +348,27 @@ globals@^11.1.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==

graphql@15.3.0:
version "15.3.0"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.3.0.tgz#3ad2b0caab0d110e3be4a5a9b2aa281e362b5278"
integrity sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w==
graphql@^16.8.1:
version "16.8.1"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==

has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=

hermes-estree@0.16.0:
version "0.16.0"
resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.16.0.tgz#e2c76a1e9d5a4d620790b9fe05fb01f2d53da07d"
integrity sha512-XCoTuBU8S+Jg8nFzaqgy6pNEYo0WYkbMmuJldb3svzpJ2SNUYJDg28b1ltoDMo7k3YlJwPRg7ZS3JTWV3DkDZA==
hermes-estree@0.20.1:
version "0.20.1"
resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.20.1.tgz#0b9a544cf883a779a8e1444b915fa365bef7f72d"
integrity sha512-SQpZK4BzR48kuOg0v4pb3EAGNclzIlqMj3Opu/mu7bbAoFw6oig6cEt/RAi0zTFW/iW6Iz9X9ggGuZTAZ/yZHg==

hermes-parser@0.16.0:
version "0.16.0"
resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.16.0.tgz#92d0a34ff4f9b7ffcb04511dfed0cc19df5038e0"
integrity sha512-tdJJntb45DUpv8j7ybHfq8NfIQgz8AgaD+PVFyfjK+O+v2N5zbsSDtlvQN2uxCghoTkQL86BEs9oi8IPrUE9Pg==
hermes-parser@0.20.1:
version "0.20.1"
resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.20.1.tgz#ad10597b99f718b91e283f81cbe636c50c3cff92"
integrity sha512-BL5P83cwCogI8D7rrDCgsFY0tdYUtmFP9XaXtl2IQjC+2Xo+4okjfXintlTxcIwl4qeGddEl28Z11kbVIw0aNA==
dependencies:
hermes-estree "0.16.0"
hermes-estree "0.20.1"

is-arrayish@^0.2.1:
version "0.2.1"
Expand Down Expand Up @@ -443,14 +443,14 @@ picocolors@^1.0.0:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==

prettier-plugin-hermes-parser@0.16.0:
version "0.16.0"
resolved "https://registry.yarnpkg.com/prettier-plugin-hermes-parser/-/prettier-plugin-hermes-parser-0.16.0.tgz#4393d43a4a6f4ed976493dccbbb93f62c44215c3"
integrity sha512-J4HdSmlxf3a0nVHVi0G6JJJ7sDVtSb5a+QR52LpiQonpQzMkqgIqyCg+Gt1sGMTJqn19Z0yTHxwCmUicVYXUVg==
prettier-plugin-hermes-parser@0.20.1:
version "0.20.1"
resolved "https://registry.yarnpkg.com/prettier-plugin-hermes-parser/-/prettier-plugin-hermes-parser-0.20.1.tgz#a5f395ae77170813e631f4538c9ef29311b4db1d"
integrity sha512-T6dfa1++ckTxd3MbLxS6sTv1T3yvTu1drahNt3g34hyCzSwYTKTByocLyhd1A9j9uCUlIPD+ogum7+i1h3+CEw==
dependencies:
hermes-estree "0.16.0"
hermes-parser "0.16.0"
prettier-plugin-hermes-parser "0.16.0"
hermes-estree "0.20.1"
hermes-parser "0.20.1"
prettier-plugin-hermes-parser "0.20.1"

prettier@2.8.8:
version "2.8.8"
Expand Down