Mirror: The spec-compliant minimum of client-side GraphQL.

fix: Move unit tests over and fix minor discrepancies (#6)

* Fix missing Kind node and add parser unit tests

* Add printer unit tests

* Add unit tests for visitor

* Add changeset

* Apply lint

* Fix type issues

+5
.changeset/orange-dryers-count.md
···
···
+
---
+
'@0no-co/graphql.web': patch
+
---
+
+
Move over unit tests from `graphql-web-lite` and fix minor discrepancies to reference implementation.
+371 -1
src/__tests__/parser.test.ts
···
import { readFileSync } from 'fs';
import { parse as graphql_parse } from 'graphql';
-
import { parse } from '../parser';
describe('print', () => {
it('prints the kitchen sink document like graphql.js does', () => {
···
const doc = parse(sink);
expect(doc).toMatchSnapshot();
expect(doc).toEqual(graphql_parse(sink, { noLocation: true }));
});
});
···
import { readFileSync } from 'fs';
import { parse as graphql_parse } from 'graphql';
+
import { parse, parseType, parseValue } from '../parser';
+
import { Kind } from '../kind';
describe('print', () => {
it('prints the kitchen sink document like graphql.js does', () => {
···
const doc = parse(sink);
expect(doc).toMatchSnapshot();
expect(doc).toEqual(graphql_parse(sink, { noLocation: true }));
+
});
+
+
it('parse provides errors', () => {
+
expect(() => parse('{')).toThrow();
+
});
+
+
it('parses variable inline values', () => {
+
expect(() => {
+
return parse('{ field(complex: { a: { b: [ $var ] } }) }');
+
}).not.toThrow();
+
});
+
+
it('parses constant default values', () => {
+
expect(() => {
+
return parse('query Foo($x: Complex = { a: { b: [ "test" ] } }) { field }');
+
}).not.toThrow();
+
expect(() => {
+
return parse('query Foo($x: Complex = { a: { b: [ $var ] } }) { field }');
+
}).toThrow();
+
});
+
+
it('parses variable definition directives', () => {
+
expect(() => {
+
return parse('query Foo($x: Boolean = false @bar) { field }');
+
}).not.toThrow();
+
});
+
+
it('does not accept fragments spread of "on"', () => {
+
expect(() => {
+
return parse('{ ...on }');
+
}).toThrow();
+
});
+
+
it('parses multi-byte characters', () => {
+
// Note: \u0A0A could be naively interpreted as two line-feed chars.
+
const ast = parse(`
+
# This comment has a \u0A0A multi-byte character.
+
{ field(arg: "Has a \u0A0A multi-byte character.") }
+
`);
+
+
expect(ast).toHaveProperty(
+
'definitions.0.selectionSet.selections.0.arguments.0.value.value',
+
'Has a \u0A0A multi-byte character.'
+
);
+
});
+
+
it('parses anonymous mutation operations', () => {
+
expect(() => {
+
return parse(`
+
mutation {
+
mutationField
+
}
+
`);
+
}).not.toThrow();
+
});
+
+
it('parses anonymous subscription operations', () => {
+
expect(() => {
+
return parse(`
+
subscription {
+
subscriptionField
+
}
+
`);
+
}).not.toThrow();
+
});
+
+
it('parses named mutation operations', () => {
+
expect(() => {
+
return parse(`
+
mutation Foo {
+
mutationField
+
}
+
`);
+
}).not.toThrow();
+
});
+
+
it('parses named subscription operations', () => {
+
expect(() => {
+
return parse(`
+
subscription Foo {
+
subscriptionField
+
}
+
`);
+
}).not.toThrow();
+
});
+
+
it('creates ast', () => {
+
const result = parse(`
+
{
+
node(id: 4) {
+
id,
+
name
+
}
+
}
+
`);
+
+
expect(result).toMatchObject({
+
kind: Kind.DOCUMENT,
+
definitions: [
+
{
+
kind: Kind.OPERATION_DEFINITION,
+
operation: 'query',
+
name: undefined,
+
variableDefinitions: [],
+
directives: [],
+
selectionSet: {
+
kind: Kind.SELECTION_SET,
+
selections: [
+
{
+
kind: Kind.FIELD,
+
alias: undefined,
+
name: {
+
kind: Kind.NAME,
+
value: 'node',
+
},
+
arguments: [
+
{
+
kind: Kind.ARGUMENT,
+
name: {
+
kind: Kind.NAME,
+
value: 'id',
+
},
+
value: {
+
kind: Kind.INT,
+
value: '4',
+
},
+
},
+
],
+
directives: [],
+
selectionSet: {
+
kind: Kind.SELECTION_SET,
+
selections: [
+
{
+
kind: Kind.FIELD,
+
alias: undefined,
+
name: {
+
kind: Kind.NAME,
+
value: 'id',
+
},
+
arguments: [],
+
directives: [],
+
selectionSet: undefined,
+
},
+
{
+
kind: Kind.FIELD,
+
alias: undefined,
+
name: {
+
kind: Kind.NAME,
+
value: 'name',
+
},
+
arguments: [],
+
directives: [],
+
selectionSet: undefined,
+
},
+
],
+
},
+
},
+
],
+
},
+
},
+
],
+
});
+
});
+
+
it('creates ast from nameless query without variables', () => {
+
const result = parse(`
+
query {
+
node {
+
id
+
}
+
}
+
`);
+
+
expect(result).toMatchObject({
+
kind: Kind.DOCUMENT,
+
definitions: [
+
{
+
kind: Kind.OPERATION_DEFINITION,
+
operation: 'query',
+
name: undefined,
+
variableDefinitions: [],
+
directives: [],
+
selectionSet: {
+
kind: Kind.SELECTION_SET,
+
selections: [
+
{
+
kind: Kind.FIELD,
+
alias: undefined,
+
name: {
+
kind: Kind.NAME,
+
value: 'node',
+
},
+
arguments: [],
+
directives: [],
+
selectionSet: {
+
kind: Kind.SELECTION_SET,
+
selections: [
+
{
+
kind: Kind.FIELD,
+
alias: undefined,
+
name: {
+
kind: Kind.NAME,
+
value: 'id',
+
},
+
arguments: [],
+
directives: [],
+
selectionSet: undefined,
+
},
+
],
+
},
+
},
+
],
+
},
+
},
+
],
+
});
+
});
+
+
it('allows parsing without source location information', () => {
+
const result = parse('{ id }', { noLocation: true });
+
expect('loc' in result).toBe(false);
+
});
+
+
describe('parseValue', () => {
+
it('parses null value', () => {
+
const result = parseValue('null');
+
expect(result).toEqual({ kind: Kind.NULL });
+
});
+
+
it('parses list values', () => {
+
const result = parseValue('[123 "abc"]');
+
expect(result).toEqual({
+
kind: Kind.LIST,
+
values: [
+
{
+
kind: Kind.INT,
+
value: '123',
+
},
+
{
+
kind: Kind.STRING,
+
value: 'abc',
+
block: false,
+
},
+
],
+
});
+
});
+
+
it('parses block strings', () => {
+
const result = parseValue('["""long""" "short"]');
+
expect(result).toEqual({
+
kind: Kind.LIST,
+
values: [
+
{
+
kind: Kind.STRING,
+
value: 'long',
+
block: true,
+
},
+
{
+
kind: Kind.STRING,
+
value: 'short',
+
block: false,
+
},
+
],
+
});
+
});
+
+
it('allows variables', () => {
+
const result = parseValue('{ field: $var }');
+
expect(result).toEqual({
+
kind: Kind.OBJECT,
+
fields: [
+
{
+
kind: Kind.OBJECT_FIELD,
+
name: {
+
kind: Kind.NAME,
+
value: 'field',
+
},
+
value: {
+
kind: Kind.VARIABLE,
+
name: {
+
kind: Kind.NAME,
+
value: 'var',
+
},
+
},
+
},
+
],
+
});
+
});
+
+
it('correct message for incomplete variable', () => {
+
expect(() => {
+
return parseValue('$');
+
}).toThrow();
+
});
+
+
it('correct message for unexpected token', () => {
+
expect(() => {
+
return parseValue(':');
+
}).toThrow();
+
});
+
});
+
+
describe('parseType', () => {
+
it('parses well known types', () => {
+
const result = parseType('String');
+
expect(result).toEqual({
+
kind: Kind.NAMED_TYPE,
+
name: {
+
kind: Kind.NAME,
+
value: 'String',
+
},
+
});
+
});
+
+
it('parses custom types', () => {
+
const result = parseType('MyType');
+
expect(result).toEqual({
+
kind: Kind.NAMED_TYPE,
+
name: {
+
kind: Kind.NAME,
+
value: 'MyType',
+
},
+
});
+
});
+
+
it('parses list types', () => {
+
const result = parseType('[MyType]');
+
expect(result).toEqual({
+
kind: Kind.LIST_TYPE,
+
type: {
+
kind: Kind.NAMED_TYPE,
+
name: {
+
kind: Kind.NAME,
+
value: 'MyType',
+
},
+
},
+
});
+
});
+
+
it('parses non-null types', () => {
+
const result = parseType('MyType!');
+
expect(result).toEqual({
+
kind: Kind.NON_NULL_TYPE,
+
type: {
+
kind: Kind.NAMED_TYPE,
+
name: {
+
kind: Kind.NAME,
+
value: 'MyType',
+
},
+
},
+
});
+
});
+
+
it('parses nested types', () => {
+
const result = parseType('[MyType!]');
+
expect(result).toEqual({
+
kind: Kind.LIST_TYPE,
+
type: {
+
kind: Kind.NON_NULL_TYPE,
+
type: {
+
kind: Kind.NAMED_TYPE,
+
name: {
+
kind: Kind.NAME,
+
value: 'MyType',
+
},
+
},
+
},
+
});
+
});
});
});
+96 -1
src/__tests__/printer.test.ts
···
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'fs';
-
import { print as graphql_print } from 'graphql';
import { print } from '../printer';
describe('print', () => {
it('prints the kitchen sink document like graphql.js does', () => {
const sink = JSON.parse(readFileSync(__dirname + '/kitchen_sink.json', { encoding: 'utf8' }));
const doc = print(sink);
expect(doc).toMatchSnapshot();
expect(doc).toEqual(graphql_print(sink));
});
});
···
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'fs';
+
import { parse, print as graphql_print } from 'graphql';
import { print } from '../printer';
+
function dedentString(string) {
+
const trimmedStr = string
+
.replace(/^\n*/m, '') // remove leading newline
+
.replace(/[ \t\n]*$/, ''); // remove trailing spaces and tabs
+
// fixes indentation by removing leading spaces and tabs from each line
+
let indent = '';
+
for (const char of trimmedStr) {
+
if (char !== ' ' && char !== '\t') {
+
break;
+
}
+
indent += char;
+
}
+
+
return trimmedStr.replace(RegExp('^' + indent, 'mg'), ''); // remove indent
+
}
+
+
function dedent(strings, ...values) {
+
let str = strings[0];
+
for (let i = 1; i < strings.length; ++i) str += values[i - 1] + strings[i]; // interpolation
+
return dedentString(str);
+
}
+
describe('print', () => {
it('prints the kitchen sink document like graphql.js does', () => {
const sink = JSON.parse(readFileSync(__dirname + '/kitchen_sink.json', { encoding: 'utf8' }));
const doc = print(sink);
expect(doc).toMatchSnapshot();
expect(doc).toEqual(graphql_print(sink));
+
});
+
+
it('prints minimal ast', () => {
+
const ast = {
+
kind: 'Field',
+
name: { kind: 'Name', value: 'foo' },
+
};
+
expect(print(ast as any)).toBe('foo');
+
});
+
+
// NOTE: The shim won't throw for invalid AST nodes
+
it('returns empty strings for invalid AST', () => {
+
const badAST = { random: 'Data' };
+
expect(print(badAST as any)).toBe('');
+
});
+
+
it('correctly prints non-query operations without name', () => {
+
const queryASTShorthanded = parse('query { id, name }');
+
expect(print(queryASTShorthanded)).toBe(dedent`
+
{
+
id
+
name
+
}
+
`);
+
+
const mutationAST = parse('mutation { id, name }');
+
expect(print(mutationAST)).toBe(dedent`
+
mutation {
+
id
+
name
+
}
+
`);
+
+
const queryASTWithArtifacts = parse('query ($foo: TestType) @testDirective { id, name }');
+
expect(print(queryASTWithArtifacts)).toBe(dedent`
+
query ($foo: TestType) @testDirective {
+
id
+
name
+
}
+
`);
+
+
const mutationASTWithArtifacts = parse('mutation ($foo: TestType) @testDirective { id, name }');
+
expect(print(mutationASTWithArtifacts)).toBe(dedent`
+
mutation ($foo: TestType) @testDirective {
+
id
+
name
+
}
+
`);
+
});
+
+
it('prints query with variable directives', () => {
+
const queryASTWithVariableDirective = parse(
+
'query ($foo: TestType = {a: 123} @testDirective(if: true) @test) { id }'
+
);
+
expect(print(queryASTWithVariableDirective)).toBe(dedent`
+
query ($foo: TestType = {a: 123} @testDirective(if: true) @test) {
+
id
+
}
+
`);
+
});
+
+
it('keeps arguments on one line if line is short (<= 80 chars)', () => {
+
const printed = print(parse('{trip(wheelchair:false arriveBy:false){dateTime}}'));
+
+
expect(printed).toBe(
+
dedent`
+
{
+
trip(wheelchair: false, arriveBy: false) {
+
dateTime
+
}
+
}
+
`
+
);
});
});
+437
src/__tests__/visitor.test.ts
···
···
+
import { describe, it, expect } from 'vitest';
+
import { Kind, parse, print } from 'graphql';
+
import { visit, BREAK } from '../visitor';
+
+
function checkVisitorFnArgs(ast, args, isEdited = false) {
+
const [node, key, parent, path, ancestors] = args;
+
+
expect(node).toBeInstanceOf(Object);
+
expect(Object.values(Kind)).toContain(node.kind);
+
+
const isRoot = key === undefined;
+
if (isRoot) {
+
if (!isEdited) {
+
expect(node).toEqual(ast);
+
}
+
expect(parent).toEqual(undefined);
+
expect(path).toEqual([]);
+
expect(ancestors).toEqual([]);
+
return;
+
}
+
+
expect(typeof key).toMatch(/number|string/);
+
+
expect(parent).toHaveProperty([key]);
+
+
expect(path).toBeInstanceOf(Array);
+
expect(path[path.length - 1]).toEqual(key);
+
+
expect(ancestors).toBeInstanceOf(Array);
+
expect(ancestors.length).toEqual(path.length - 1);
+
+
if (!isEdited) {
+
let currentNode = ast;
+
for (let i = 0; i < ancestors.length; ++i) {
+
expect(ancestors[i]).toEqual(currentNode);
+
+
currentNode = currentNode[path[i]];
+
expect(currentNode).not.toEqual(undefined);
+
}
+
}
+
}
+
+
function getValue(node: any) {
+
return 'value' in node ? node.value : undefined;
+
}
+
+
describe('Visitor', () => {
+
it('handles empty visitor', () => {
+
const ast = parse('{ a }', { noLocation: true });
+
expect(() => visit(ast, {})).not.toThrow();
+
});
+
+
it('validates path argument', () => {
+
const visited: any[] = [];
+
+
const ast = parse('{ a }', { noLocation: true });
+
+
visit(ast, {
+
enter(_node, _key, _parent, path) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', path.slice()]);
+
},
+
leave(_node, _key, _parent, path) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', path.slice()]);
+
},
+
});
+
+
expect(visited).toEqual([
+
['enter', []],
+
['enter', ['definitions', 0]],
+
['enter', ['definitions', 0, 'selectionSet']],
+
['enter', ['definitions', 0, 'selectionSet', 'selections', 0]],
+
['enter', ['definitions', 0, 'selectionSet', 'selections', 0, 'name']],
+
['leave', ['definitions', 0, 'selectionSet', 'selections', 0, 'name']],
+
['leave', ['definitions', 0, 'selectionSet', 'selections', 0]],
+
['leave', ['definitions', 0, 'selectionSet']],
+
['leave', ['definitions', 0]],
+
['leave', []],
+
]);
+
});
+
+
it('validates ancestors argument', () => {
+
const ast = parse('{ a }', { noLocation: true });
+
const visitedNodes: any[] = [];
+
+
visit(ast, {
+
enter(node, key, parent, _path, ancestors) {
+
const inArray = typeof key === 'number';
+
if (inArray) {
+
visitedNodes.push(parent);
+
}
+
visitedNodes.push(node);
+
+
const expectedAncestors = visitedNodes.slice(0, -2);
+
expect(ancestors).toEqual(expectedAncestors);
+
},
+
leave(_node, key, _parent, _path, ancestors) {
+
const expectedAncestors = visitedNodes.slice(0, -2);
+
expect(ancestors).toEqual(expectedAncestors);
+
+
const inArray = typeof key === 'number';
+
if (inArray) {
+
visitedNodes.pop();
+
}
+
visitedNodes.pop();
+
},
+
});
+
});
+
+
it('allows editing a node both on enter and on leave', () => {
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
+
+
let selectionSet;
+
+
const editedAST = visit(ast, {
+
OperationDefinition: {
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
selectionSet = node.selectionSet;
+
return {
+
...node,
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [],
+
},
+
didEnter: true,
+
};
+
},
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
+
return {
+
...node,
+
selectionSet,
+
didLeave: true,
+
};
+
},
+
},
+
});
+
+
expect(editedAST).toEqual({
+
...ast,
+
definitions: [
+
{
+
...ast.definitions[0],
+
didEnter: true,
+
didLeave: true,
+
},
+
],
+
});
+
});
+
+
it('allows editing the root node on enter and on leave', () => {
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
+
+
const { definitions } = ast;
+
+
const editedAST = visit(ast, {
+
Document: {
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
return {
+
...node,
+
definitions: [],
+
didEnter: true,
+
};
+
},
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
+
return {
+
...node,
+
definitions,
+
didLeave: true,
+
};
+
},
+
},
+
});
+
+
expect(editedAST).toEqual({
+
...ast,
+
didEnter: true,
+
didLeave: true,
+
});
+
});
+
+
it('allows for editing on enter', () => {
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
+
const editedAST = visit(ast, {
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
if (node.kind === 'Field' && node.name.value === 'b') {
+
return null;
+
}
+
},
+
});
+
+
expect(ast).toEqual(parse('{ a, b, c { a, b, c } }', { noLocation: true }));
+
+
expect(editedAST).toEqual(parse('{ a, c { a, c } }', { noLocation: true }));
+
});
+
+
it('allows for editing on leave', () => {
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
+
const editedAST = visit(ast, {
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
+
if (node.kind === 'Field' && node.name.value === 'b') {
+
return null;
+
}
+
},
+
});
+
+
expect(ast).toEqual(parse('{ a, b, c { a, b, c } }', { noLocation: true }));
+
+
expect(editedAST).toEqual(parse('{ a, c { a, c } }', { noLocation: true }));
+
});
+
+
it('ignores false returned on leave', () => {
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
+
const returnedAST = visit(ast, {
+
leave() {
+
return false;
+
},
+
});
+
+
expect(returnedAST).toEqual(parse('{ a, b, c { a, b, c } }', { noLocation: true }));
+
});
+
+
it('visits edited node', () => {
+
const addedField = {
+
kind: 'Field',
+
name: {
+
kind: 'Name',
+
value: '__typename',
+
},
+
};
+
+
let didVisitAddedField;
+
+
const ast = parse('{ a { x } }', { noLocation: true });
+
visit(ast, {
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
+
if (node.kind === 'Field' && node.name.value === 'a') {
+
return {
+
kind: 'Field',
+
selectionSet: [addedField, node.selectionSet],
+
};
+
}
+
if (node === addedField) {
+
didVisitAddedField = true;
+
}
+
},
+
});
+
+
expect(didVisitAddedField).toEqual(true);
+
});
+
+
it('allows skipping a sub-tree', () => {
+
const visited: any[] = [];
+
+
const ast = parse('{ a, b { x }, c }', { noLocation: true });
+
visit(ast, {
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
if (node.kind === 'Field' && node.name.value === 'b') {
+
return false;
+
}
+
},
+
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', node.kind, getValue(node)]);
+
},
+
});
+
+
expect(visited).toEqual([
+
['enter', 'Document', undefined],
+
['enter', 'OperationDefinition', undefined],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'a'],
+
['leave', 'Name', 'a'],
+
['leave', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'c'],
+
['leave', 'Name', 'c'],
+
['leave', 'Field', undefined],
+
['leave', 'SelectionSet', undefined],
+
['leave', 'OperationDefinition', undefined],
+
['leave', 'Document', undefined],
+
]);
+
});
+
+
it('allows early exit while visiting', () => {
+
const visited: any[] = [];
+
+
const ast = parse('{ a, b { x }, c }', { noLocation: true });
+
visit(ast, {
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
if (node.kind === 'Name' && node.value === 'x') {
+
return BREAK;
+
}
+
},
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', node.kind, getValue(node)]);
+
},
+
});
+
+
expect(visited).toEqual([
+
['enter', 'Document', undefined],
+
['enter', 'OperationDefinition', undefined],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'a'],
+
['leave', 'Name', 'a'],
+
['leave', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'b'],
+
['leave', 'Name', 'b'],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'x'],
+
]);
+
});
+
+
it('allows early exit while leaving', () => {
+
const visited: any[] = [];
+
+
const ast = parse('{ a, b { x }, c }', { noLocation: true });
+
visit(ast, {
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
},
+
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', node.kind, getValue(node)]);
+
if (node.kind === 'Name' && node.value === 'x') {
+
return BREAK;
+
}
+
},
+
});
+
+
expect(visited).toEqual([
+
['enter', 'Document', undefined],
+
['enter', 'OperationDefinition', undefined],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'a'],
+
['leave', 'Name', 'a'],
+
['leave', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'b'],
+
['leave', 'Name', 'b'],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'x'],
+
['leave', 'Name', 'x'],
+
]);
+
});
+
+
it('allows a named functions visitor API', () => {
+
const visited: any[] = [];
+
+
const ast = parse('{ a, b { x }, c }', { noLocation: true });
+
visit(ast, {
+
Name(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
},
+
SelectionSet: {
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
},
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', node.kind, getValue(node)]);
+
},
+
},
+
});
+
+
expect(visited).toEqual([
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Name', 'a'],
+
['enter', 'Name', 'b'],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Name', 'x'],
+
['leave', 'SelectionSet', undefined],
+
['enter', 'Name', 'c'],
+
['leave', 'SelectionSet', undefined],
+
]);
+
});
+
+
it('handles deep immutable edits correctly when using "enter"', () => {
+
const formatNode = node => {
+
if (
+
node.selectionSet &&
+
!node.selectionSet.selections.some(
+
node => node.kind === Kind.FIELD && node.name.value === '__typename' && !node.alias
+
)
+
) {
+
return {
+
...node,
+
selectionSet: {
+
...node.selectionSet,
+
selections: [
+
...node.selectionSet.selections,
+
{
+
kind: Kind.FIELD,
+
name: {
+
kind: Kind.NAME,
+
value: '__typename',
+
},
+
},
+
],
+
},
+
};
+
}
+
};
+
const ast = parse('{ players { nodes { id } } }');
+
const expected = parse('{ players { nodes { id __typename } __typename } }');
+
const visited = visit(ast, {
+
Field: formatNode,
+
InlineFragment: formatNode,
+
});
+
+
expect(print(visited)).toEqual(print(expected));
+
});
+
});
+7
src/kind.js
···
INLINE_FRAGMENT: 'InlineFragment',
FRAGMENT_DEFINITION: 'FragmentDefinition',
VARIABLE: 'Variable',
OBJECT: 'ObjectValue',
OBJECT_FIELD: 'ObjectField',
DIRECTIVE: 'Directive',
···
INLINE_FRAGMENT: 'InlineFragment',
FRAGMENT_DEFINITION: 'FragmentDefinition',
VARIABLE: 'Variable',
+
INT: 'IntValue',
+
FLOAT: 'FloatValue',
+
STRING: 'StringValue',
+
BOOLEAN: 'BooleanValue',
+
NULL: 'NullValue',
+
ENUM: 'EnumValue',
+
LIST: 'ListValue',
OBJECT: 'ObjectValue',
OBJECT_FIELD: 'ObjectField',
DIRECTIVE: 'Directive',
+2 -2
src/parser.ts
···
const leadingRe = / +(?=[^\s])/y;
export function blockString(string: string) {
let out = '';
let commonIndent = 0;
let firstNonEmptyLine = 0;
-
let lastNonEmptyLine = -1;
-
const lines = string.split('\n');
for (let i = 0; i < lines.length; i++) {
leadingRe.lastIndex = 0;
if (leadingRe.test(lines[i])) {
···
const leadingRe = / +(?=[^\s])/y;
export function blockString(string: string) {
+
const lines = string.split('\n');
let out = '';
let commonIndent = 0;
let firstNonEmptyLine = 0;
+
let lastNonEmptyLine = lines.length - 1;
for (let i = 0; i < lines.length; i++) {
leadingRe.lastIndex = 0;
if (leadingRe.test(lines[i])) {
+7 -2
src/visitor.ts
···
) {
let hasEdited = false;
-
const enter = (visitor[node.kind] && visitor[node.kind].enter) || visitor[node.kind];
const resultEnter = enter && enter.call(visitor, node, key, parent, path, ancestors);
if (resultEnter === false) {
return node;
···
}
if (parent) ancestors.pop();
-
const leave = visitor[node.kind] && visitor[node.kind].leave;
const resultLeave = leave && leave.call(visitor, node, key, parent, path, ancestors);
if (resultLeave === BREAK) {
throw BREAK;
···
) {
let hasEdited = false;
+
const enter =
+
(visitor[node.kind] && visitor[node.kind].enter) ||
+
visitor[node.kind] ||
+
(visitor as EnterLeaveVisitor<ASTNode>).enter;
const resultEnter = enter && enter.call(visitor, node, key, parent, path, ancestors);
if (resultEnter === false) {
return node;
···
}
if (parent) ancestors.pop();
+
const leave =
+
(visitor[node.kind] && visitor[node.kind].leave) ||
+
(visitor as EnterLeaveVisitor<ASTNode>).leave;
const resultLeave = leave && leave.call(visitor, node, key, parent, path, ancestors);
if (resultLeave === BREAK) {
throw BREAK;